fix(docs): remove stale hashline-{read,edit}.ts rows post-fold

Hashline read/edit tool wrappers were folded into Edit({match}) and
Read({format}) modes in commit ffdec0fee. The two rows in FILE-SYSTEM-MAP.md
pointed to files that no longer exist. Updated the surviving hashline.ts row
to note its new consumer relationship with Edit/Read.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mikael Hugo 2026-05-17 18:48:34 +02:00
parent 781a7e7319
commit cc32ab79d9
14 changed files with 625 additions and 9 deletions

View file

@ -282,9 +282,7 @@
| core/tools/grep.ts | Tool System, File Search | Pattern search tool |
| core/tools/ls.ts | Tool System | Directory listing tool |
| core/tools/truncate.ts | Tool System, Text Processing | Output truncation utility |
| core/tools/hashline.ts | Tool System | Hash-based line identification |
| core/tools/hashline-read.ts | Tool System | File reading with hash-based line ranges |
| core/tools/hashline-edit.ts | Tool System | File editing with hash-based line identification |
| core/tools/hashline.ts | Tool System | Hash-based line identification (consumed by `Edit({match: "anchor"})` and `Read({format: "tagged"})` modes since 2026-05-17 fold) |
| core/tools/path-utils.ts | Tool System | Path normalization and validation |
| core/bash-executor.ts | Tool System | High-level bash execution with event handling |
| core/exec.ts | Tool System | Utility functions for command execution |

View file

@ -331,6 +331,7 @@ export {
type EditToolDetails,
type EditToolInput,
type EditToolOptions,
createAstGrepTool,
editTool,
type FindOperations,
type FindToolDetails,
@ -345,6 +346,7 @@ export {
getAllToolCompatibility,
getToolCompatibility,
grepTool,
createInsertAroundSymbolTool,
type LsOperations,
type LsToolDetails,
type LsToolInput,
@ -355,6 +357,7 @@ export {
type ReadToolInput,
type ReadToolOptions,
readTool,
createReplaceSymbolTool,
registerMcpToolCompatibility,
// Tool compatibility registry (ADR-005)
registerToolCompatibility,

View file

@ -30,6 +30,7 @@ import {
import { DispatchLayer } from "../dispatch/dispatch-layer.js";
import { isInlineEligible } from "../dispatch/run-unit-inline.js";
import { swarmDispatchAndWait } from "../uok/swarm-dispatch.js";
import { inlineRuntimeGate } from "../uok/inline-runtime-gate.js";
/**
* #M010/S05: Try inline-scope dispatch via DispatchLayer.
@ -53,6 +54,29 @@ import { swarmDispatchAndWait } from "../uok/swarm-dispatch.js";
async function tryInlineDispatch(ctx, s, unitType, unitId, _prompt, options) {
if (!isInlineEligible(unitType)) return null;
const basePath = s.basePath ?? ctx.basePath ?? process.cwd();
const gateResult = inlineRuntimeGate.execute({
...ctx,
unitType,
unitId,
basePath,
});
if (gateResult.outcome !== "pass") {
debugLog("runUnit", {
phase: "inline-route-refused",
unitType,
unitId,
rationale: gateResult.rationale,
});
return {
status: "cancelled",
errorContext: {
message: `[inline-runtime-gate] ${gateResult.rationale}`,
category: "policy",
isTransient: false,
},
_via: "inline-gate",
};
}
debugLog("runUnit", {
phase: "inline-route",
unitType,
@ -1258,12 +1282,10 @@ async function runUnitViaSwarm(ctx, _pi, s, unitType, unitId, prompt, options) {
* Default: false (each new unit starts with a clean session).
*/
export async function runUnit(ctx, pi, s, unitType, unitId, prompt, options) {
// #M010/S05: Inline-scope dispatch is default-on for inline-eligible unit types.
// SF_INLINE_DISPATCH=0 is the escape hatch for operators who need to force the
// swarm/legacy path. validate-milestone, complete-milestone, and reassess-roadmap
// are inline-eligible (R013). This was the S03 feature-flag path; S05 makes it
// the default and adds SF_INLINE_DISPATCH=0 as the off-switch.
if (isInlineEligible(unitType) && process.env.SF_INLINE_DISPATCH !== "0") {
// #M010/S05 + R074: inline dispatch is attempted for eligible unit types,
// but the inlineRuntimeGate refuses it until R020 and R066 are validated
// unless SF_INLINE_DISPATCH=1 is set as an audited bypass.
if (isInlineEligible(unitType)) {
const inline = await tryInlineDispatch(
ctx,
s,

View file

@ -438,6 +438,23 @@ export function registerHooks(pi, ecosystemHandlers = []) {
"../self-feedback-drain.js"
);
dispatchSelfFeedbackInlineFixIfNeeded(process.cwd(), ctx, pi);
try {
const { drainAdversarialFindingsBridge } = await import(
"../safety/adversarial-finding-bridge.js"
);
const bridged = drainAdversarialFindingsBridge(triage.stillBlocked, {
basePath: process.cwd(),
});
if (bridged > 0) {
ctx.ui?.notify?.(
`adversarial-finding bridge: smoke_gate quarantine applied for ${bridged} high-severity finding${bridged === 1 ? "" : "s"}.`,
"warning",
{ noticeKind: NOTICE_KIND.SYSTEM_NOTICE },
);
}
} catch {
/* non-fatal — adversarial bridge must never block session start */
}
} catch {
/* non-fatal — self-feedback drain must never block session start */
}

View file

@ -8,6 +8,16 @@
export const DEFAULT_COMMAND_TIMEOUT_MS = 120_000;
/** Default timeout for the dynamic bash tool (seconds). */
export const DEFAULT_BASH_TIMEOUT_SECS = 120;
/**
* Maximum output tokens allowed for one adversarial/challenge review turn.
*
* Purpose: cap adversarial review cost/runaway risk while still allowing a
* substantive critique of milestone or slice evidence.
*
* Consumer: uok/auto-dispatch.js challenge envelopes and
* uok/adversarial-budget.js enforcement helpers.
*/
export const ADVERSARIAL_REVIEW_MAX_TOKENS = 60_000;
// ─── Cache Sizes ──────────────────────────────────────────────────────────────
/** Max directory-listing cache entries before eviction (#611). */
export const DIR_CACHE_MAX = 200;

View file

@ -0,0 +1,123 @@
/**
* adversarial-finding-bridge.js quarantine smoke_gate on severe findings.
*
* Purpose: connect high-severity adversarial findings to the existing
* smoke_gate quarantine control so a challenge result can stop promotion until
* an operator reviews the finding.
*
* Consumer: session-start self-feedback drain in bootstrap/register-hooks.js.
*/
import { setExperimentalFlag } from "../experimental.js";
import { recordSelfFeedback } from "../self-feedback.js";
import { isDbAvailable, listSelfFeedbackEntries } from "../sf-db.js";
/**
* Return true for adversarial findings severe enough to quarantine smoke_gate.
*
* Purpose: keep the bridge predicate explicit and testable.
*
* Consumer: bridgeAdversarialFindingToQuarantine and drainAdversarialFindingsBridge.
*/
export function isHighSeverityAdversarialFinding(entry) {
if (!entry || typeof entry !== "object") return false;
const kind = String(entry.kind ?? "");
const severity = String(entry.severity ?? "");
return (
(kind === "adversarial-finding" ||
kind.startsWith("adversarial-finding:")) &&
severity === "high"
);
}
function quarantineKeyFor(sourceEntry) {
return `quarantine:adversarial-finding:${sourceEntry.id ?? "unknown"}`;
}
function alreadyQuarantinedByKey(key) {
if (!isDbAvailable()) return false;
return listSelfFeedbackEntries().some(
(entry) =>
entry.kind === "smoke-gate-quarantined" &&
entry.evidence?.quarantineKey === key,
);
}
/**
* Bridge one high-severity adversarial finding to smoke_gate quarantine.
*
* Purpose: make adversarial review actionable by flipping the promotion gate
* and recording a child self-feedback entry with a durable source reference.
*
* Consumer: drainAdversarialFindingsBridge.
*/
export function bridgeAdversarialFindingToQuarantine(entry, opts = {}) {
if (!isHighSeverityAdversarialFinding(entry)) {
return {
ok: false,
reason: "entry is not a high-severity adversarial-finding",
};
}
const basePath = opts.basePath ?? process.cwd();
const quarantineKey = quarantineKeyFor(entry);
const alreadyQuarantined =
opts.alreadyQuarantined ?? ((key) => alreadyQuarantinedByKey(key));
if (alreadyQuarantined(quarantineKey, basePath)) {
return { ok: true, quarantined: false, reason: "already-quarantined" };
}
const setFlag =
opts.setExperimentalFlag ??
((name, value) => setExperimentalFlag(name, value));
const record =
opts.recordSelfFeedback ?? ((child, bp) => recordSelfFeedback(child, bp));
try {
setFlag("smoke_gate", false);
} catch {
/* child entry below still records the attempted quarantine */
}
const childEntry = {
kind: "smoke-gate-quarantined",
severity: "high",
summary: `smoke_gate disabled by adversarial-finding bridge. Source: ${entry.id ?? "unknown"} - ${String(entry.summary ?? "").slice(0, 200)}`,
evidence: {
sourceEntryId: entry.id,
sourceKind: entry.kind,
sourceSeverity: entry.severity,
quarantineKey,
bridgeReason:
"high-severity adversarial-finding triggered smoke_gate quarantine",
},
suggestedFix:
"Review the source adversarial-finding entry and decide whether the finding warrants quarantine. Re-enable smoke_gate after the issue is resolved.",
};
let childEntryId;
try {
childEntryId = record(childEntry, basePath)?.entry?.id;
} catch {
/* quarantine flag decision already happened */
}
return { ok: true, quarantined: true, childEntryId };
}
/**
* Process blocked self-feedback entries and quarantine severe adversarial finds.
*
* Purpose: make findings from previous runs effective at the next session
* boundary, even if the bridge was not loaded when the finding was filed.
*
* Consumer: bootstrap/register-hooks.js self-feedback drain.
*/
export function drainAdversarialFindingsBridge(entries, opts = {}) {
if (!Array.isArray(entries)) return 0;
let count = 0;
for (const entry of entries) {
if (!isHighSeverityAdversarialFinding(entry)) continue;
bridgeAdversarialFindingToQuarantine(entry, opts);
count += 1;
}
return count;
}

View file

@ -489,6 +489,8 @@ const ALLOWED_KIND_DOMAINS = new Set([
// this, R075 challenge units have no kind to file their findings under
// (gap reported by sf-mp9u4i25-fczmcj on 2026-05-17).
"adversarial-finding",
"adversarial-budget-exceeded",
"smoke-gate-quarantined",
]);
const KIND_SEGMENT_RE = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;

View file

@ -0,0 +1,63 @@
/**
* adversarial-budget.test.mjs R075 adversarial review token budget.
*
* Purpose: verify challenge review budget calculation and feedback filing
* behavior before wiring the cap into dispatch envelopes.
*/
import assert from "node:assert/strict";
import { afterEach, test } from "vitest";
import { ADVERSARIAL_REVIEW_MAX_TOKENS } from "../constants.js";
import {
enforceAdversarialBudget,
isAdversarialBudgetExceeded,
resolveAdversarialBudget,
} from "../uok/adversarial-budget.js";
let savedBudget;
afterEach(() => {
if (savedBudget === undefined) delete process.env.SF_ADVERSARIAL_MAX_TOKENS;
else process.env.SF_ADVERSARIAL_MAX_TOKENS = savedBudget;
});
test("resolveAdversarialBudget_when_env_unset_returns_constant", () => {
savedBudget = process.env.SF_ADVERSARIAL_MAX_TOKENS;
delete process.env.SF_ADVERSARIAL_MAX_TOKENS;
assert.equal(resolveAdversarialBudget(), ADVERSARIAL_REVIEW_MAX_TOKENS);
});
test("resolveAdversarialBudget_when_env_positive_uses_override", () => {
savedBudget = process.env.SF_ADVERSARIAL_MAX_TOKENS;
process.env.SF_ADVERSARIAL_MAX_TOKENS = "12345";
assert.equal(resolveAdversarialBudget(), 12345);
});
test("isAdversarialBudgetExceeded_when_at_limit_returns_true", () => {
savedBudget = process.env.SF_ADVERSARIAL_MAX_TOKENS;
delete process.env.SF_ADVERSARIAL_MAX_TOKENS;
assert.equal(
isAdversarialBudgetExceeded(ADVERSARIAL_REVIEW_MAX_TOKENS),
true,
);
assert.equal(
isAdversarialBudgetExceeded(ADVERSARIAL_REVIEW_MAX_TOKENS - 1),
false,
);
});
test("enforceAdversarialBudget_when_over_limit_records_feedback", () => {
savedBudget = process.env.SF_ADVERSARIAL_MAX_TOKENS;
delete process.env.SF_ADVERSARIAL_MAX_TOKENS;
const entries = [];
const result = enforceAdversarialBudget(
ADVERSARIAL_REVIEW_MAX_TOKENS + 1,
{ unitId: "challenge-M001", milestoneId: "M001" },
{ recordSelfFeedback: (entry) => entries.push(entry) },
);
assert.equal(result.shortCircuit, true);
assert.equal(result.reason, "adversarial-budget-exceeded");
assert.equal(entries.length, 1);
assert.equal(entries[0].kind, "adversarial-budget-exceeded");
assert.equal(entries[0].evidence.unitId, "challenge-M001");
});

View file

@ -0,0 +1,98 @@
/**
* adversarial-finding-bridge.test.mjs R075 finding-to-quarantine bridge.
*
* Purpose: verify high-severity adversarial findings disable smoke_gate once
* and create a child self-feedback entry with a stable source reference.
*/
import assert from "node:assert/strict";
import { test } from "vitest";
import {
bridgeAdversarialFindingToQuarantine,
drainAdversarialFindingsBridge,
isHighSeverityAdversarialFinding,
} from "../safety/adversarial-finding-bridge.js";
test("isHighSeverityAdversarialFinding_when_high_adversarial_returns_true", () => {
assert.equal(
isHighSeverityAdversarialFinding({
kind: "adversarial-finding:red-team",
severity: "high",
}),
true,
);
assert.equal(
isHighSeverityAdversarialFinding({
kind: "adversarial-finding",
severity: "medium",
}),
false,
);
});
test("bridgeAdversarialFindingToQuarantine_when_high_finding_disables_smoke_gate", () => {
const flags = [];
const entries = [];
const result = bridgeAdversarialFindingToQuarantine(
{
id: "sf-finding-1",
kind: "adversarial-finding",
severity: "high",
summary: "Promotion bypass found.",
},
{
setExperimentalFlag: (name, value) => flags.push({ name, value }),
recordSelfFeedback: (entry) => {
entries.push(entry);
return { entry: { id: "child-1" } };
},
alreadyQuarantined: () => false,
},
);
assert.equal(result.ok, true);
assert.equal(result.quarantined, true);
assert.deepEqual(flags, [{ name: "smoke_gate", value: false }]);
assert.equal(entries.length, 1);
assert.equal(entries[0].kind, "smoke-gate-quarantined");
assert.equal(entries[0].evidence.sourceEntryId, "sf-finding-1");
});
test("bridgeAdversarialFindingToQuarantine_when_already_quarantined_is_idempotent", () => {
const flags = [];
const entries = [];
const result = bridgeAdversarialFindingToQuarantine(
{
id: "sf-finding-1",
kind: "adversarial-finding",
severity: "high",
},
{
setExperimentalFlag: (name, value) => flags.push({ name, value }),
recordSelfFeedback: (entry) => entries.push(entry),
alreadyQuarantined: () => true,
},
);
assert.equal(result.reason, "already-quarantined");
assert.equal(flags.length, 0);
assert.equal(entries.length, 0);
});
test("drainAdversarialFindingsBridge_when_mixed_entries_processes_only_high_findings", () => {
const flags = [];
const count = drainAdversarialFindingsBridge(
[
{ id: "one", kind: "adversarial-finding", severity: "high" },
{ id: "two", kind: "adversarial-finding", severity: "medium" },
{ id: "three", kind: "gap", severity: "high" },
],
{
setExperimentalFlag: (name, value) => flags.push({ name, value }),
recordSelfFeedback: () => {},
alreadyQuarantined: () => false,
},
);
assert.equal(count, 1);
assert.deepEqual(flags, [{ name: "smoke_gate", value: false }]);
});

View file

@ -0,0 +1,115 @@
/**
* inline-runtime-gate.test.mjs R074 inline dispatch safety gate.
*
* Purpose: verify inline dispatch fails closed until R020 and R066 are
* validated, with SF_INLINE_DISPATCH=1 as the explicit audited bypass.
*/
import assert from "node:assert/strict";
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, test } from "vitest";
import { closeDatabase, openDatabase, upsertRequirement } from "../sf-db.js";
import { inlineRuntimeGate } from "../uok/inline-runtime-gate.js";
const roots = [];
let savedBypass;
function makeProject() {
const root = mkdtempSync(join(tmpdir(), "sf-inline-gate-"));
mkdirSync(join(root, ".sf"), { recursive: true });
roots.push(root);
openDatabase(join(root, ".sf", "sf.db"));
return root;
}
function seedRequirement(id, status) {
upsertRequirement({
id,
class: "R",
status,
description: `Test requirement ${id}`,
why: "test",
source: "test",
primary_owner: "test",
supporting_slices: null,
validation: null,
notes: null,
full_content: `# ${id}\n- Status: ${status}`,
superseded_by: null,
});
}
beforeEach(() => {
savedBypass = process.env.SF_INLINE_DISPATCH;
delete process.env.SF_INLINE_DISPATCH;
});
afterEach(() => {
closeDatabase();
for (const root of roots.splice(0))
rmSync(root, { recursive: true, force: true });
if (savedBypass === undefined) delete process.env.SF_INLINE_DISPATCH;
else process.env.SF_INLINE_DISPATCH = savedBypass;
});
describe("inlineRuntimeGate", () => {
test("execute_when_requirements_missing_fails_closed", () => {
const root = makeProject();
const result = inlineRuntimeGate.execute({
basePath: root,
unitType: "validate-milestone",
unitId: "M001",
});
assert.equal(result.outcome, "fail");
assert.equal(result.failureClass, "policy");
assert.match(result.rationale, /R020=unknown R066=unknown/);
});
test("execute_when_one_requirement_active_fails", () => {
const root = makeProject();
seedRequirement("R020", "validated");
seedRequirement("R066", "active");
const result = inlineRuntimeGate.execute({
basePath: root,
unitType: "complete-milestone",
unitId: "M001",
});
assert.equal(result.outcome, "fail");
assert.match(result.rationale, /R020=validated R066=active/);
});
test("execute_when_both_requirements_validated_passes", () => {
const root = makeProject();
seedRequirement("R020", "validated");
seedRequirement("R066", "validated");
const result = inlineRuntimeGate.execute({
basePath: root,
unitType: "reassess-roadmap",
unitId: "M001/S01",
});
assert.equal(result.outcome, "pass");
assert.match(result.rationale, /R020=validated R066=validated/);
});
test("execute_when_bypass_set_passes_with_audited_rationale", () => {
const root = makeProject();
seedRequirement("R020", "active");
seedRequirement("R066", "active");
process.env.SF_INLINE_DISPATCH = "1";
const result = inlineRuntimeGate.execute({
basePath: root,
unitType: "validate-milestone",
unitId: "M001",
});
assert.equal(result.outcome, "pass");
assert.match(result.rationale, /audited bypass/);
});
});

View file

@ -0,0 +1,86 @@
/**
* adversarial-budget.js token budget policy for adversarial review units.
*
* Purpose: prevent challenge/adversarial review turns from consuming unbounded
* tokens while still allowing the review to file structured feedback when the
* cap is reached.
*
* Consumer: uok/auto-dispatch.js challenge dispatch envelopes and future
* streaming budget checks.
*/
import { ADVERSARIAL_REVIEW_MAX_TOKENS } from "../constants.js";
/**
* Resolve the effective adversarial review token budget.
*
* Purpose: allow an explicit operator override without forking dispatch code.
*
* Consumer: challenge dispatch and focused tests.
*/
export function resolveAdversarialBudget() {
const override = Number(process.env.SF_ADVERSARIAL_MAX_TOKENS ?? "");
if (Number.isFinite(override) && override > 0) return Math.floor(override);
return ADVERSARIAL_REVIEW_MAX_TOKENS;
}
/**
* Return true when the observed token count has reached the adversarial budget.
*
* Purpose: provide a cheap predicate for both pre-dispatch and streaming checks.
*
* Consumer: enforceAdversarialBudget.
*/
export function isAdversarialBudgetExceeded(tokenCount) {
return (
Number.isFinite(tokenCount) && tokenCount >= resolveAdversarialBudget()
);
}
/**
* File structured self-feedback and short-circuit when the budget is exceeded.
*
* Purpose: turn runaway adversarial review into a durable, reviewable feedback
* item instead of allowing silent token burn.
*
* Consumer: future challenge streaming enforcement.
*/
export function enforceAdversarialBudget(tokenCount, context = {}, opts = {}) {
const budget = resolveAdversarialBudget();
if (!isAdversarialBudgetExceeded(tokenCount)) return null;
const { unitId, milestoneId, basePath } = context;
const entry = {
kind: "adversarial-budget-exceeded",
reason: "adversarial-budget-exceeded",
severity: "medium",
summary: `Adversarial review unit${unitId ? ` "${unitId}"` : ""} exceeded token budget (${tokenCount} >= ${budget}). Review was short-circuited.`,
evidence: {
tokenCount,
budget,
...(unitId ? { unitId } : {}),
...(milestoneId ? { milestoneId } : {}),
},
suggestedFix:
"Tune the challenge prompt or reduce inlined context so the review fits within the budget. Adjust SF_ADVERSARIAL_MAX_TOKENS only when the higher cap is intentional.",
};
const record =
opts.recordSelfFeedback ??
(async () => {
try {
const { recordSelfFeedback } = await import("../self-feedback.js");
recordSelfFeedback(entry, basePath ?? process.cwd());
} catch {
/* feedback filing must never mask the budget decision */
}
});
const result = record(entry, basePath ?? process.cwd());
if (result && typeof result.catch === "function") result.catch(() => {});
return {
shortCircuit: true,
reason: "adversarial-budget-exceeded",
tokenCount,
budget,
};
}

View file

@ -115,6 +115,7 @@ import {
decideUnitRuntimeDispatch,
readUnitRuntimeRecord,
} from "./unit-runtime.js";
import { resolveAdversarialBudget } from "./adversarial-budget.js";
const MAX_PARALLEL_RESEARCH_SLICES = 8;
const PARALLEL_RESEARCH_BLOCKING_PHASES = new Set([
@ -963,6 +964,7 @@ export const DISPATCH_RULES = [
action: "dispatch",
unitType: "challenge",
unitId: challengeUnitId(mid, slice.id),
maxOutputTokens: resolveAdversarialBudget(),
prompt: await buildChallengePrompt(
mid,
midTitle,
@ -1894,6 +1896,7 @@ export const DISPATCH_RULES = [
action: "dispatch",
unitType: "challenge",
unitId: challengeUnitId(mid),
maxOutputTokens: resolveAdversarialBudget(),
prompt: await buildChallengePrompt(
mid,
midTitle,

View file

@ -11,6 +11,7 @@ import { repeatedFeedbackKindGate } from "../detectors/repeated-feedback-kind.js
import { artifactFlapGate } from "../detectors/artifact-flap.js";
import { staleLockGate } from "../detectors/stale-lock.js";
import { periodicDetectorSweepGate } from "../detectors/periodic-runner.js";
import { inlineRuntimeGate } from "./inline-runtime-gate.js";
/**
* gate-registry-bootstrap.js register ADR-0075 UOK gates that are liftable.
@ -41,5 +42,6 @@ registry.register(repeatedFeedbackKindGate);
registry.register(artifactFlapGate);
registry.register(staleLockGate);
registry.register(periodicDetectorSweepGate);
registry.register(inlineRuntimeGate);
export { registry as gateRegistry };

View file

@ -0,0 +1,74 @@
/**
* inline-runtime-gate.js policy gate for default-on inline dispatch.
*
* Purpose: refuse inline dispatch until R020 (inline equivalence proof) and
* R066 (regression firewall) are validated, unless the operator explicitly
* uses the audited `SF_INLINE_DISPATCH=1` bypass.
*
* Consumer: auto/run-unit.js before DispatchLayer enters the inline execution
* path for validate/complete/reassess units.
*/
import { debugLog } from "../debug-logger.js";
import { getRequirementById, isDbAvailable } from "../sf-db.js";
export const INLINE_RUNTIME_GATE_ID = "inline-runtime-gate";
export const INLINE_RUNTIME_GATE_REQUIREMENTS = ["R020", "R066"];
function readRequirementStatus(id) {
if (!isDbAvailable()) return "unknown";
try {
return getRequirementById(id)?.status ?? "unknown";
} catch {
return "unknown";
}
}
/**
* Decide whether inline dispatch may run for the current unit.
*
* Purpose: make the unsafe default-on inline path fail closed until its
* contract requirements are validated, while retaining an explicit audited
* escape hatch for operator-driven development.
*
* Consumer: tryInlineDispatch in auto/run-unit.js.
*/
export const inlineRuntimeGate = {
id: INLINE_RUNTIME_GATE_ID,
type: "policy",
execute(ctx = {}) {
const statuses = Object.fromEntries(
INLINE_RUNTIME_GATE_REQUIREMENTS.map((id) => [
id,
readRequirementStatus(id),
]),
);
const allValidated = INLINE_RUNTIME_GATE_REQUIREMENTS.every(
(id) => statuses[id] === "validated",
);
if (allValidated) {
return {
outcome: "pass",
rationale: `inline dispatch allowed: R020=${statuses.R020} R066=${statuses.R066}`,
};
}
if (process.env.SF_INLINE_DISPATCH === "1") {
debugLog(INLINE_RUNTIME_GATE_ID, {
event: "audited-bypass",
statuses,
unitType: ctx.unitType,
unitId: ctx.unitId,
});
return {
outcome: "pass",
rationale: `inline dispatch allowed via audited bypass (SF_INLINE_DISPATCH=1): R020=${statuses.R020} R066=${statuses.R066}`,
};
}
return {
outcome: "fail",
failureClass: "policy",
rationale: `inline dispatch refused: R020=${statuses.R020} R066=${statuses.R066}; both must be validated or SF_INLINE_DISPATCH=1 must be set`,
};
},
};