@sabaiway/agent-workflow-kit 10.5.0 → 11.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +139 -0
- package/SKILL.md +9 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +1 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +6 -6
- package/bridges/antigravity-cli-bridge/capability.json +1 -1
- package/bridges/codex-cli-bridge/SKILL.md +1 -1
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +1 -1
- package/bridges/codex-cli-bridge/bin/codex-review.sh +6 -6
- package/bridges/codex-cli-bridge/capability.json +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/commit-guard.md +2 -2
- package/references/modes/control-bytes.md +91 -0
- package/references/modes/core-evidence.md +1 -1
- package/references/modes/flow-writer.md +16 -4
- package/references/modes/procedures.md +5 -1
- package/references/modes/review-state.md +3 -3
- package/references/modes/robustness-brief.md +61 -0
- package/references/modes/velocity.md +2 -0
- package/references/robustness-literals.json +107 -0
- package/tools/changed-surface.mjs +1 -1
- package/tools/commands.mjs +14 -0
- package/tools/commit-guard.mjs +24 -12
- package/tools/control-bytes.mjs +264 -0
- package/tools/core-evidence.mjs +33 -20
- package/tools/coverage-check.mjs +1 -1
- package/tools/dispatch-store-read.mjs +269 -0
- package/tools/dispatch-store.mjs +21 -357
- package/tools/flow-check-git-lane.mjs +2 -1
- package/tools/flow-delta-proof.mjs +4 -4
- package/tools/flow-record-shape.mjs +44 -2
- package/tools/flow-round-gates.mjs +151 -0
- package/tools/flow-store-read.mjs +1 -1
- package/tools/flow-writer.mjs +235 -82
- package/tools/fold-scope.mjs +7 -3
- package/tools/fs-read-nofollow.mjs +56 -4
- package/tools/git-env.mjs +120 -0
- package/tools/grounding.mjs +4 -3
- package/tools/held-session.mjs +165 -0
- package/tools/plan-shape-facts.mjs +23 -10
- package/tools/plan-shape.mjs +8 -0
- package/tools/procedures.mjs +47 -4
- package/tools/repo-lex.mjs +29 -0
- package/tools/review-state.mjs +127 -39
- package/tools/robustness-brief.mjs +209 -0
- package/tools/robustness-literals.mjs +101 -0
- package/tools/sandbox-masks.mjs +2 -1
- package/tools/set-flow.mjs +2 -1
- package/tools/source-size-scope.mjs +19 -25
- package/tools/tracked-tree-census.mjs +3 -3
- package/tools/velocity-profile.mjs +5 -2
- package/tools/worktree-handoff-return.mjs +2 -2
- package/tools/worktrees.mjs +2 -2
package/tools/review-state.mjs
CHANGED
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
// `--check` turns the report into a gate exit code (declare it in docs/ai/gates.json).
|
|
10
10
|
//
|
|
11
11
|
// Normative `--check` exit contract (the single home of this list — SKILL.md points here):
|
|
12
|
+
// Before the review-recipe exits, configured plan-execution.execute=delegated with a plan and a
|
|
13
|
+
// dirty, fingerprintable tree audits the delegation ledger. An absent ledger is inert; an
|
|
14
|
+
// unavailable ledger, a HEAD error or a SUBSTITUTED held session refuses unless a codex-exec degrade at the
|
|
15
|
+
// substituted return's tree, standing after every later fold, accepts it, or the fold of that thread's
|
|
16
|
+
// retry lifts it (codex never grants it).
|
|
12
17
|
// exit 0 when the CONFIGURED plan-execution.review recipe is solo (or the computed default is —
|
|
13
18
|
// absent config with no reviewer backend ready); when no plan is in flight (docs/plans/
|
|
14
19
|
// holds no top-level .md that is not queue.md and not scratch by the naming convention:
|
|
@@ -16,8 +21,8 @@
|
|
|
16
21
|
// the tree is clean (nothing to review — under a non-solo review obligation the PASS
|
|
17
22
|
// still NAMES every plan in flight and states that the gate arms as soon as the tree
|
|
18
23
|
// turns dirty, so a latent arm is discoverable before it blocks a pending commit); when
|
|
19
|
-
//
|
|
20
|
-
//
|
|
24
|
+
// git ITSELF answers not-a-repository under both discoveries (the ONLY non-work-tree
|
|
25
|
+
// location that passes; the five other states refuse by name); and when
|
|
21
26
|
// the obligations are SATISFIED: under `reviewed`, >=1 backend's latest normal receipt
|
|
22
27
|
// attests SHIP-CLASS (ship / ship with nits) for the current tree; under `council`,
|
|
23
28
|
// EVERY review-capable backend attests ship-class OR carries an explicit current-tree
|
|
@@ -104,7 +109,12 @@ import { selectReliedOnReceipt, evaluateVetoOverride, evaluateInternalAttestatio
|
|
|
104
109
|
// in leaf modules so the procedures read surface reaches them without this module's write-API
|
|
105
110
|
// graph; re-exported here so every historical consumer keeps its import site.
|
|
106
111
|
import { PLANS_REL, isScratchPlanName, plansInFlight } from './plan-files.mjs';
|
|
107
|
-
import { shellQuoteArg } from './repo-lex.mjs';
|
|
112
|
+
import { escapeForDisplay, shellQuoteArg } from './repo-lex.mjs';
|
|
113
|
+
import { GIT_MAX_BUFFER, resolveGitLocation, stripGitLocationEnv, withGitPath } from './git-env.mjs';
|
|
114
|
+
import { readDelegationLedger } from './dispatch-store-read.mjs';
|
|
115
|
+
import {
|
|
116
|
+
decideHeldSession, HELD_EXECUTE_WRAPPER, HELD_RECEIPT_BACKEND, judgeLedger,
|
|
117
|
+
} from './held-session.mjs';
|
|
108
118
|
|
|
109
119
|
export { PLANS_REL, isScratchPlanName, plansInFlight, shellQuoteArg };
|
|
110
120
|
// The canonical review-domain primitives live in the core-evidence DAG bottom (ONE home for the
|
|
@@ -138,7 +148,7 @@ export {
|
|
|
138
148
|
|
|
139
149
|
const ACTIVITY = 'plan-execution';
|
|
140
150
|
const SLOT = 'review';
|
|
141
|
-
const
|
|
151
|
+
const CORE_EVIDENCE_TOOL = shellQuoteArg(join(dirname(fileURLToPath(import.meta.url)), 'core-evidence.mjs'));
|
|
142
152
|
// --await (BUGFREE-3 / AD-049, item (d)) bounds + poll cadence. The default timeout is generous —
|
|
143
153
|
// a real grounded bridge review can take minutes — and every value is overridable (--timeout / the
|
|
144
154
|
// injectable clock) so hermetic tests never spend wall-clock.
|
|
@@ -147,18 +157,18 @@ export const AWAIT_POLL_MS = 5000;
|
|
|
147
157
|
|
|
148
158
|
// ── git plumbing (read-only queries; injectable for tests) ─────────────────────────
|
|
149
159
|
|
|
150
|
-
const gitRaw = (args, cwd) =>
|
|
151
|
-
spawnSync('git', args, { cwd, maxBuffer: GIT_MAX_BUFFER, windowsHide: true });
|
|
160
|
+
const gitRaw = (args, cwd, env) =>
|
|
161
|
+
spawnSync('git', args, { cwd, env: withGitPath(env ?? process.env), maxBuffer: GIT_MAX_BUFFER, windowsHide: true });
|
|
152
162
|
|
|
153
163
|
// stdout Buffer of a git query, or null when git fails (not a repo / git absent).
|
|
154
|
-
const gitBuf = (args, cwd) => {
|
|
155
|
-
const r = gitRaw(args, cwd);
|
|
164
|
+
const gitBuf = (args, cwd, env) => {
|
|
165
|
+
const r = gitRaw(args, cwd, env);
|
|
156
166
|
if (r.error || r.status !== 0) return null;
|
|
157
167
|
return r.stdout;
|
|
158
168
|
};
|
|
159
169
|
|
|
160
|
-
const gitLine = (args, cwd) => {
|
|
161
|
-
const buf = gitBuf(args, cwd);
|
|
170
|
+
const gitLine = (args, cwd, env) => {
|
|
171
|
+
const buf = gitBuf(args, cwd, env);
|
|
162
172
|
return buf == null ? null : buf.toString('utf8').replace(/\r?\n$/, '');
|
|
163
173
|
};
|
|
164
174
|
|
|
@@ -171,10 +181,10 @@ const gitLine = (args, cwd) => {
|
|
|
171
181
|
// ignores them by construction; this count only feeds ONE non-failing advisory line naming the
|
|
172
182
|
// cosmetic sandbox-masks apply — an applied managed block hides the paths from --exclude-standard,
|
|
173
183
|
// so the advisory disappears exactly when the status noise does (no standing detector).
|
|
174
|
-
export const countNeverCommittableUntracked = (cwd, { lstat = lstatSync } = {}) => {
|
|
175
|
-
const top = gitLine(['rev-parse', '--show-toplevel'], cwd);
|
|
184
|
+
export const countNeverCommittableUntracked = (cwd, { lstat = lstatSync, env } = {}) => {
|
|
185
|
+
const top = gitLine(['rev-parse', '--show-toplevel'], cwd, env);
|
|
176
186
|
if (top == null) return 0;
|
|
177
|
-
const untrackedZ = gitBuf(['ls-files', '--others', '--exclude-standard', '-z'], top);
|
|
187
|
+
const untrackedZ = gitBuf(['ls-files', '--others', '--exclude-standard', '-z'], top, env);
|
|
178
188
|
if (untrackedZ == null) return 0;
|
|
179
189
|
return untrackedZ
|
|
180
190
|
.toString('utf8')
|
|
@@ -296,10 +306,35 @@ export const degradeRecordSet = ({ cwd, env = process.env, fingerprint }) => {
|
|
|
296
306
|
const storePath = resolveEvidencePath(cwd, env);
|
|
297
307
|
const read = storePath ? readEvidence(storePath) : { records: [], malformed: 0, malformedReasons: [] };
|
|
298
308
|
const unavailable = (read.malformed ?? 0) > 0 || read.readError != null;
|
|
309
|
+
const records = unavailable ? [] : authoritativeOfKind(read.records, 'degrade');
|
|
299
310
|
const set = unavailable || fingerprint == null
|
|
300
311
|
? new Set()
|
|
301
|
-
: new Set(
|
|
302
|
-
return { set, storePath, malformed: read.malformed ?? 0, readError: read.readError ?? null, unavailable };
|
|
312
|
+
: new Set(records.filter((r) => r.fingerprint === fingerprint).map((r) => r.backend));
|
|
313
|
+
return { set, records, storePath, malformed: read.malformed ?? 0, readError: read.readError ?? null, unavailable };
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
export const selectHeldSessionDegrades = (records) =>
|
|
317
|
+
records.filter((record) => record.kind === 'degrade' && record.backend === HELD_EXECUTE_WRAPPER);
|
|
318
|
+
|
|
319
|
+
export const shouldReadHeldSession = ({ configuredExecute, plans, fingerprint, clean }) =>
|
|
320
|
+
configuredExecute === 'delegated' && plans.length > 0 && fingerprint !== null && clean === false;
|
|
321
|
+
|
|
322
|
+
export const buildHeldSessionState = ({
|
|
323
|
+
cwd,
|
|
324
|
+
env,
|
|
325
|
+
configuredExecute,
|
|
326
|
+
plans,
|
|
327
|
+
fingerprint,
|
|
328
|
+
clean,
|
|
329
|
+
degrades,
|
|
330
|
+
resolveStore,
|
|
331
|
+
readStore,
|
|
332
|
+
audit,
|
|
333
|
+
readHead,
|
|
334
|
+
}) => {
|
|
335
|
+
if (!shouldReadHeldSession({ configuredExecute, plans, fingerprint, clean })) return null;
|
|
336
|
+
const ledger = readDelegationLedger(cwd, env, { resolveStore, readStore, audit, readHead });
|
|
337
|
+
return judgeLedger(ledger, { backend: HELD_RECEIPT_BACKEND, degrades });
|
|
303
338
|
};
|
|
304
339
|
|
|
305
340
|
// ── the check + report core ─────────────────────────────────────────────────────────
|
|
@@ -310,7 +345,10 @@ export const degradeRecordSet = ({ cwd, env = process.env, fingerprint }) => {
|
|
|
310
345
|
// must read the same config/plans or a dirty unreceipted tree could false-PASS as "no plan in
|
|
311
346
|
// flight". Outside a git tree the cwd is the only anchor (and --check exits 0).
|
|
312
347
|
export const buildState = ({ cwd, env = process.env, detect = detectBackends, surveyVehicle, lstat = lstatSync, readFile = readFileSync } = {}) => {
|
|
313
|
-
|
|
348
|
+
// WHICH repository, first; the config/plan anchor is the stripped discovery's top (from a subdirectory the bare cwd would miss docs/plans — decideCheck refuses on an unanchored root).
|
|
349
|
+
const location = resolveGitLocation(cwd, { env });
|
|
350
|
+
const anchoredTop = location.top ?? gitLine(['rev-parse', '--show-toplevel'], cwd, stripGitLocationEnv(env));
|
|
351
|
+
const root = anchoredTop ?? cwd;
|
|
314
352
|
const { config, source: configSource } = loadConfig(root);
|
|
315
353
|
// A bridge-detector throw reaches the hook, never a catch that would also lose the surveyed
|
|
316
354
|
// executor vehicle: bridge readiness goes unknown (fail closed below), the carrier stays known.
|
|
@@ -319,16 +357,15 @@ export const buildState = ({ cwd, env = process.env, detect = detectBackends, su
|
|
|
319
357
|
detectionWarning = `backend detection failed (${(err && err.message) || err}) — bridge readiness unknown.`;
|
|
320
358
|
};
|
|
321
359
|
const detection = composeReadiness(root, { detect, surveyVehicle, onDetectError });
|
|
322
|
-
// The resolver stays for DISPLAY
|
|
323
|
-
// recipe (never the readiness-degraded effective one — no silent solo).
|
|
360
|
+
// The resolver stays for DISPLAY only; the OBLIGATIONS come from the configured recipe (never the readiness-degraded effective one — no silent solo).
|
|
324
361
|
const resolved = resolveActivityRecipe({ config: config ?? {}, readiness: detection, activity: ACTIVITY, slot: SLOT });
|
|
325
362
|
const obligations = requiredBackendsForConfiguredRecipe({ config: config ?? {}, readiness: detection, detectionFailed: detectionWarning != null });
|
|
326
363
|
const requiredBackends = obligations.backends;
|
|
327
364
|
const plans = plansInFlight(root);
|
|
328
365
|
// The injected lstat threads through EVERY stat-dependent computation (fingerprint, clean, the
|
|
329
366
|
// mask count) — a partial injection would let a test observe an inconsistent state.
|
|
330
|
-
const fingerprint = computeTreeFingerprint(cwd, { lstat });
|
|
331
|
-
const clean = fingerprint == null ? null : isTreeClean(cwd, { lstat });
|
|
367
|
+
const fingerprint = location.state === 'work-tree' ? computeTreeFingerprint(cwd, { lstat, env }) : null;
|
|
368
|
+
const clean = fingerprint == null ? null : isTreeClean(cwd, { lstat, env });
|
|
332
369
|
const receiptsPath = resolveReceiptsPath(cwd, env);
|
|
333
370
|
const receiptsRead = receiptsPath ? readReceipts(receiptsPath) : { receipts: [], malformed: 0 };
|
|
334
371
|
const { receipts, malformed } = receiptsRead;
|
|
@@ -340,6 +377,15 @@ export const buildState = ({ cwd, env = process.env, detect = detectBackends, su
|
|
|
340
377
|
const base = resolveBase(cwd);
|
|
341
378
|
const degrade = degradeRecordSet({ cwd, env, fingerprint });
|
|
342
379
|
const degradedExempt = requiredBackends.filter((b) => degrade.set.has(b));
|
|
380
|
+
const heldSession = buildHeldSessionState({
|
|
381
|
+
cwd,
|
|
382
|
+
env,
|
|
383
|
+
configuredExecute: config?.[ACTIVITY]?.execute,
|
|
384
|
+
plans,
|
|
385
|
+
fingerprint,
|
|
386
|
+
clean,
|
|
387
|
+
degrades: selectHeldSessionDegrades(degrade.records),
|
|
388
|
+
});
|
|
343
389
|
// ── the Phase-2 flow arms' state (two-tier activation, P3) ──
|
|
344
390
|
// Tier 1 is store-file PRESENCE: no file ⇒ every field below stays inert and the decision is
|
|
345
391
|
// byte-identical to the pre-flow checker (the unarmed fast path also reads NO plan file).
|
|
@@ -425,6 +471,8 @@ export const buildState = ({ cwd, env = process.env, detect = detectBackends, su
|
|
|
425
471
|
backends: armedBackends,
|
|
426
472
|
plans,
|
|
427
473
|
root,
|
|
474
|
+
rootAnchored: anchoredTop !== null,
|
|
475
|
+
location,
|
|
428
476
|
fingerprint,
|
|
429
477
|
clean,
|
|
430
478
|
receiptsPath,
|
|
@@ -437,7 +485,8 @@ export const buildState = ({ cwd, env = process.env, detect = detectBackends, su
|
|
|
437
485
|
evidenceReadError: degrade.readError,
|
|
438
486
|
evidenceUnavailable: degrade.unavailable,
|
|
439
487
|
degradedExempt,
|
|
440
|
-
|
|
488
|
+
heldSession,
|
|
489
|
+
maskedUntracked: countNeverCommittableUntracked(cwd, { lstat, env }),
|
|
441
490
|
detectionWarning,
|
|
442
491
|
flowPresent,
|
|
443
492
|
flowArmed,
|
|
@@ -474,9 +523,29 @@ const backendFailurePart = (b, state) => {
|
|
|
474
523
|
if (b.state === 'unrecognized-verdict') return `${b.backend}: the latest normal receipt carries an unrecognized verdict (${JSON.stringify(b.verdict)}) — an unknown verdict never attests (fail-closed); re-run the review`;
|
|
475
524
|
if (b.state === 'ungrounded') return `${b.backend}: the latest normal receipt is ungrounded — re-run grounded (--facts)`;
|
|
476
525
|
if (b.state === 'probe') return `${b.backend}: only probe receipts for the current tree (CODEX_PROBE=1 / AGY_PROBE=1 relaxes the quality guards) — a probe review never attests; re-run a real one`;
|
|
477
|
-
if (b.state === 'rejected') return `${b.backend}: current-tree receipts rejected — ${rejectionCause(b)} (fail-closed); inspect ${state.receiptsPath}`;
|
|
526
|
+
if (b.state === 'rejected') return `${b.backend}: current-tree receipts rejected — ${rejectionCause(b)} (fail-closed); inspect ${escapeForDisplay(state.receiptsPath)}`;
|
|
478
527
|
if (b.state === 'stale') return `${b.backend}: receipts exist but none matches the current tree (edited after review) — run a fresh review`;
|
|
479
|
-
return `${b.backend}: no receipt — run its review wrapper, or record an explicit degrade (node ${
|
|
528
|
+
return `${b.backend}: no receipt — run its review wrapper, or record an explicit degrade (node ${CORE_EVIDENCE_TOOL} degrade --backend ${b.backend} --reason "...")`;
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
const describesHeldRecovery = (state) => {
|
|
532
|
+
const substitution = state.heldSession.substitution;
|
|
533
|
+
if (substitution === null) return '';
|
|
534
|
+
const canRetry = substitution.folded === false;
|
|
535
|
+
if (state.evidenceUnavailable === true) {
|
|
536
|
+
const retry = canRetry ? '; or fold the retry of that thread' : '';
|
|
537
|
+
return ` — the evidence store is unavailable, so the degrade lane cannot be judged: repair it (inspect ${escapeForDisplay(state.evidenceStorePath)}) and re-check${retry}`;
|
|
538
|
+
}
|
|
539
|
+
const canDegrade = typeof substitution.postTreeDigest === 'string'
|
|
540
|
+
&& state.fingerprint === substitution.postTreeDigest;
|
|
541
|
+
if (!canDegrade && !canRetry) {
|
|
542
|
+
return ' — no recovery lane remains: the tree moved after the substituted return and that thread is folded; re-dispatch the work under the held session';
|
|
543
|
+
}
|
|
544
|
+
const degrade = canDegrade
|
|
545
|
+
? ` — mint the accepted-replacement record now, before the next edit: node ${CORE_EVIDENCE_TOOL} degrade --backend ${HELD_EXECUTE_WRAPPER} --reason "..."`
|
|
546
|
+
: '';
|
|
547
|
+
const retry = canRetry ? `${canDegrade ? '; or' : ' —'} fold the retry of that thread` : '';
|
|
548
|
+
return `${degrade}${retry}`;
|
|
480
549
|
};
|
|
481
550
|
|
|
482
551
|
// The normative --check decision (the header contract, in order). → { code, reason }.
|
|
@@ -488,11 +557,18 @@ export const decideCheck = (state) => {
|
|
|
488
557
|
// Store diagnostics ride EVERY check line, early exits (and the unknowable arm) included — a
|
|
489
558
|
// malformed receipt line, an unavailable evidence store, or an unreadable receipts store is
|
|
490
559
|
// never hidden behind any exit.
|
|
491
|
-
const malformedNote = state.malformed > 0 ? ` — ${state.malformed} malformed receipt line(s) ignored; inspect ${state.receiptsPath}` : '';
|
|
560
|
+
const malformedNote = state.malformed > 0 ? ` — ${state.malformed} malformed receipt line(s) ignored; inspect ${escapeForDisplay(state.receiptsPath)}` : '';
|
|
492
561
|
const evidenceNote = state.evidenceUnavailable
|
|
493
|
-
? ` — evidence store unavailable (${state.evidenceMalformed} malformed line(s)${state.evidenceReadError ? `, read error: ${state.evidenceReadError}` : ''}); the degrade escape is denied (fail-closed) — inspect ${state.evidenceStorePath}`
|
|
562
|
+
? ` — evidence store unavailable (${state.evidenceMalformed} malformed line(s)${state.evidenceReadError ? `, read error: ${state.evidenceReadError}` : ''}); the degrade escape is denied (fail-closed) — inspect ${escapeForDisplay(state.evidenceStorePath)}`
|
|
494
563
|
: '';
|
|
495
|
-
const earlyNotes = `${malformedNote}${evidenceNote}${state.receiptsReadError ? ` — receipts store unreadable (${state.receiptsReadError}); inspect ${state.receiptsPath}` : ''}`;
|
|
564
|
+
const earlyNotes = `${malformedNote}${evidenceNote}${state.receiptsReadError ? ` — receipts store unreadable (${state.receiptsReadError}); inspect ${escapeForDisplay(state.receiptsPath)}` : ''}`;
|
|
565
|
+
// The git LOCATION (hand-built states carry none; the fingerprint decides there) rules below the solo arm: not-a-repository passes, the four other non-work-tree states refuse by name.
|
|
566
|
+
const location = state.location ?? { state: state.fingerprint == null ? 'not-a-repository' : 'work-tree', cause: 'no location was resolved for a hand-built state' };
|
|
567
|
+
if (state.heldSession !== null && state.heldSession !== undefined) {
|
|
568
|
+
const held = decideHeldSession(state.heldSession);
|
|
569
|
+
const heldRecovery = describesHeldRecovery(state);
|
|
570
|
+
if (held.code !== 0) return { ...held, reason: `${held.reason}${heldRecovery}${earlyNotes}`, terminal: true, heldSession: true };
|
|
571
|
+
}
|
|
496
572
|
// Detector failure with NO configured recipe: the computed default is unknowable → fail closed.
|
|
497
573
|
// An EXPLICIT configured recipe needs no detector — its obligations are readiness-independent.
|
|
498
574
|
if (state.obligations.unknowable) {
|
|
@@ -504,11 +580,14 @@ export const decideCheck = (state) => {
|
|
|
504
580
|
: `no reviewer backend is ready — the computed ${ACTIVITY}.${SLOT} default is solo`;
|
|
505
581
|
return { code: 0, reason: `${why} — no receipt required${earlyNotes}` };
|
|
506
582
|
}
|
|
583
|
+
// An UNANCHORED root (git dead everywhere): the bare-cwd plan inventory cannot say "no plan".
|
|
584
|
+
if (location.state !== 'work-tree' && location.state !== 'not-a-repository' && state.rootAnchored === false) return { code: 1, reason: `the git location is ${location.state} — ${escapeForDisplay(location.cause)} — and the project root cannot be anchored, so the plan arm cannot decide (fail closed)${earlyNotes}` };
|
|
507
585
|
if (state.plans.length === 0) return { code: 0, reason: `no plan in flight (docs/plans/ holds no active plan) — no receipt required${earlyNotes}` };
|
|
508
|
-
if (state.
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
//
|
|
586
|
+
if (location.state !== 'work-tree' && location.state !== 'not-a-repository') return { code: 1, reason: `the git location is ${location.state} — ${escapeForDisplay(location.cause)} — refusing to judge receipts for a tree git cannot name (fail closed)${earlyNotes}` };
|
|
587
|
+
if (location.state === 'not-a-repository') return { code: 0, reason: `not a git repository (git's own answer: ${escapeForDisplay(location.cause)}) — nothing to fingerprint${earlyNotes}` };
|
|
588
|
+
if (state.fingerprint == null || state.clean === null) return { code: 1, reason: `the tree ${state.fingerprint == null ? 'fingerprint' : 'clean check'} is undecidable in a work tree (a git query failed or was killed) — refusing to judge receipts (fail closed)${earlyNotes}` };
|
|
589
|
+
// A clean tree PASSES, but never silently: >=1 plan IS in flight here, and this gate arms the
|
|
590
|
+
// moment the tree turns dirty — naming each file makes the arm discoverable BEFORE it blocks.
|
|
512
591
|
if (state.clean === true) {
|
|
513
592
|
// Each name is byte-preserving and quoted because this reason is one gate-report line.
|
|
514
593
|
const named = state.plans.map((p) => quoteReportName(p)).join(', ');
|
|
@@ -525,7 +604,7 @@ export const decideCheck = (state) => {
|
|
|
525
604
|
// whatever the recipe consults, so a recipe flip to solo can never bury one.
|
|
526
605
|
if (state.obligations.recipe === 'solo') {
|
|
527
606
|
if (state.receiptsReadError != null) {
|
|
528
|
-
return { code: 1, reason: `the receipts store is unreadable (${state.receiptsReadError}) — "no standing veto" cannot be established, so the internal-only floor fails closed; inspect ${state.receiptsPath}${earlyNotes}` };
|
|
607
|
+
return { code: 1, reason: `the receipts store is unreadable (${state.receiptsReadError}) — "no standing veto" cannot be established, so the internal-only floor fails closed; inspect ${escapeForDisplay(state.receiptsPath)}${earlyNotes}` };
|
|
529
608
|
}
|
|
530
609
|
const standing = state.soloVetoRows.filter((b) => (b.state === 'current' && !b.shipClass && !b.overrideLabel) || b.state === 'unrecognized-verdict');
|
|
531
610
|
if (standing.length > 0) {
|
|
@@ -571,7 +650,7 @@ export const decideCheck = (state) => {
|
|
|
571
650
|
.filter((b) => !(printed.has(b.backend) && b.state === 'rejected'))
|
|
572
651
|
.reduce((n, b) => n + (b.markerRejected ?? 0) + (b.unmarkedRejected ?? 0), 0);
|
|
573
652
|
const markerNote = total > 0
|
|
574
|
-
? ` — ${total} receipt(s) rejected: an untrustworthy probe marker (malformed, or absent — silence is not a declaration) — fail-closed; inspect ${state.receiptsPath}`
|
|
653
|
+
? ` — ${total} receipt(s) rejected: an untrustworthy probe marker (malformed, or absent — silence is not a declaration) — fail-closed; inspect ${escapeForDisplay(state.receiptsPath)}`
|
|
575
654
|
: '';
|
|
576
655
|
return `${markerNote}${earlyNotes}`;
|
|
577
656
|
};
|
|
@@ -646,11 +725,12 @@ const formatHuman = (state, check) => {
|
|
|
646
725
|
];
|
|
647
726
|
if (state.detectionWarning) lines.push(` ⚠ ${state.detectionWarning}`);
|
|
648
727
|
lines.push(` plan in flight: ${state.plans.length ? state.plans.map((name) => quoteReportName(name)).join(', ') : '(none)'}`);
|
|
649
|
-
if (state.fingerprint == null) lines.push(
|
|
728
|
+
if (state.fingerprint == null) lines.push(` tree: ${state.location != null && state.location.state !== 'work-tree' ? `git location ${state.location.state} — ${escapeForDisplay(state.location.cause)}` : 'the fingerprint is undecidable (a git query failed or was killed)'}`);
|
|
650
729
|
else if (state.clean === true) lines.push(' tree: clean (nothing to review)');
|
|
651
730
|
else lines.push(` tree fingerprint: ${state.fingerprint}`);
|
|
652
|
-
|
|
653
|
-
|
|
731
|
+
if (state.heldSession != null) lines.push(` ${decideHeldSession(state.heldSession).line}`);
|
|
732
|
+
lines.push(` receipts: ${state.receiptsPath == null ? '(unresolvable — no git dir)' : escapeForDisplay(state.receiptsPath)} (${state.receiptCount} line(s)${state.malformed ? `, ${state.malformed} malformed — inspect the file` : ''})`);
|
|
733
|
+
if (state.receiptsReadError) lines.push(` ⚠ receipts store unreadable (${state.receiptsReadError}) — inspect ${escapeForDisplay(state.receiptsPath)}`);
|
|
654
734
|
if (state.evidenceUnavailable) lines.push(` ⚠ evidence store unavailable (${state.evidenceMalformed} malformed line(s)${state.evidenceReadError ? `, read error: ${state.evidenceReadError}` : ''}) — the degrade escape is denied (fail-closed)`);
|
|
655
735
|
const exempt = new Set(state.degradedExempt);
|
|
656
736
|
for (const b of state.backends) {
|
|
@@ -700,9 +780,14 @@ attests; a malformed OR absent probe marker is rejected fail-closed — silence
|
|
|
700
780
|
The ONLY escape for an unavailable backend under council is an explicit current-tree degrade
|
|
701
781
|
record (node core-evidence.mjs degrade) — and never all backends.
|
|
702
782
|
|
|
703
|
-
--check exits 0/1 per the normative contract in the tool header
|
|
783
|
+
--check exits 0/1 per the normative contract in the tool header. FIRST, before any of the arms
|
|
784
|
+
below, a configured plan-execution.execute = delegated with a plan in flight and a dirty,
|
|
785
|
+
fingerprintable tree audits the delegation ledger: an absent ledger is inert; an unreadable,
|
|
786
|
+
malformed, foreign or audit-refused ledger, a failed HEAD read, or a SUBSTITUTED held session
|
|
787
|
+
exits 1 (a substitution is lifted only by a codex-exec degrade at the substituted return's own
|
|
788
|
+
tree, or the fold of that thread's retry). THEN: 0 for solo / no plan in flight /
|
|
704
789
|
a clean tree (under a non-solo review obligation the PASS names every plan in flight and its arm) /
|
|
705
|
-
not-a-
|
|
790
|
+
a cwd where git ITSELF answers not-a-repository (the only passing non-work-tree state; the four others exit 1 by name) / obligations satisfied (reviewed: >=1 ship-class attestation;
|
|
706
791
|
council: every backend ship-class or degrade-recorded, >=1 real ship); 1 on a veto, an
|
|
707
792
|
unrecognized verdict, a missing/stale/ungrounded/probe-only backend without a degrade record,
|
|
708
793
|
an all-degraded tree, or a down detector with no configured recipe.
|
|
@@ -728,11 +813,12 @@ export const main = (argv, ctx = {}) => {
|
|
|
728
813
|
const cwd = ctx.cwd ?? process.cwd();
|
|
729
814
|
const env = ctx.env ?? process.env;
|
|
730
815
|
const detect = ctx.detect ?? detectBackends;
|
|
816
|
+
const build = ctx.buildState ?? buildState;
|
|
731
817
|
try {
|
|
732
818
|
if (argv.includes('--help') || argv.includes('-h')) return { code: 0, stdout: HELP, stderr: '' };
|
|
733
819
|
const unknown = argv.find((a) => !KNOWN_ARGS.has(a));
|
|
734
820
|
if (unknown !== undefined) throw fail(2, `unknown argument: ${unknown}`);
|
|
735
|
-
const state =
|
|
821
|
+
const state = build({ cwd, env, detect, surveyVehicle: ctx.surveyVehicle, lstat: ctx.lstat, readFile: ctx.readFile });
|
|
736
822
|
const check = decideCheck(state);
|
|
737
823
|
// The mask advisory is NON-FAILING by contract: one notice line, never an exit-code arm.
|
|
738
824
|
const advisory = maskAdvisoryLine(state);
|
|
@@ -779,6 +865,7 @@ export const mainAwait = async (argv, ctx = {}) => {
|
|
|
779
865
|
const now = ctx.now ?? (() => Date.now());
|
|
780
866
|
const sleep = ctx.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
781
867
|
const pollMs = ctx.pollMs ?? AWAIT_POLL_MS;
|
|
868
|
+
const build = ctx.buildState ?? buildState;
|
|
782
869
|
try {
|
|
783
870
|
for (let i = 0; i < argv.length; i += 1) {
|
|
784
871
|
const a = argv[i];
|
|
@@ -798,9 +885,10 @@ export const mainAwait = async (argv, ctx = {}) => {
|
|
|
798
885
|
for (;;) {
|
|
799
886
|
const elapsed = now() - start;
|
|
800
887
|
if (elapsed >= timeoutMs) return { code: 1, stdout: '', stderr: `review-state --await: TIMEOUT after ${timeoutS}s — ${lastReason}` };
|
|
801
|
-
const check = decideCheck(
|
|
888
|
+
const check = decideCheck(build({ cwd, env, detect }));
|
|
802
889
|
lastReason = check.reason;
|
|
803
890
|
if (check.code === 0) return { code: 0, stdout: `review-state --await: READY — ${check.reason}`, stderr: '' };
|
|
891
|
+
if (check.heldSession === true) return { code: 1, stdout: '', stderr: `review-state --await: HELD SESSION — ${check.reason}` };
|
|
804
892
|
// Decision 4 (#50): an AUTHORITATIVE veto ends the wait loudly BEFORE the deadline — the
|
|
805
893
|
// dispatched review answered with a landed negative; classifying that as a timeout was the
|
|
806
894
|
// misclassification this amendment closes. Only a fresh review can move it, never waiting.
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { realpathSync } from 'node:fs';
|
|
4
|
+
import { isAbsolute, resolve, sep } from 'node:path';
|
|
5
|
+
import { readShippedRobustnessLiterals, parseRobustTag } from './robustness-literals.mjs';
|
|
6
|
+
import { checkPlanStructure, formatFindings, isSweep } from './plan-shape.mjs';
|
|
7
|
+
import { expandSweepPaths } from './plan-shape-facts.mjs';
|
|
8
|
+
import { readFileBytesNoFollowCapped } from './fs-read-nofollow.mjs';
|
|
9
|
+
import { escapeForDisplay, lexicalRepoRelative } from './repo-lex.mjs';
|
|
10
|
+
import { isDirectRun } from './direct-run.mjs';
|
|
11
|
+
|
|
12
|
+
const PLAN_READ_CAP = 1_048_576;
|
|
13
|
+
const ROW_ID = /^[A-Za-z0-9._-]+$/u;
|
|
14
|
+
const SEARCHABLE_KINDS = new Set(['env', 'flag', 'ref', 'syscall', 'errno', 'argv']);
|
|
15
|
+
const SHIPPED_LIST_PATH = 'agent-workflow-kit/references/robustness-literals.json';
|
|
16
|
+
const USAGE = 'Usage: node robustness-brief.mjs --plan <plan> [--row <id>] [--coverage]';
|
|
17
|
+
const usage = (message) => Object.assign(new Error(message), { usage: true });
|
|
18
|
+
const isInside = (root, path) => path === root || path.startsWith(`${root}${sep}`);
|
|
19
|
+
const decodePlan = (bytes) => new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
20
|
+
|
|
21
|
+
const parseArgs = (argv, index = 0, state = { plan: null, row: null, coverage: false }) => {
|
|
22
|
+
if (index === argv.length) {
|
|
23
|
+
if (state.plan === null) throw usage('absent --plan');
|
|
24
|
+
if (state.row !== null && state.coverage) throw usage('--row cannot be combined with --coverage');
|
|
25
|
+
return state;
|
|
26
|
+
}
|
|
27
|
+
const flag = argv[index];
|
|
28
|
+
if (flag === '--coverage') {
|
|
29
|
+
if (state.coverage) throw usage('duplicate --coverage');
|
|
30
|
+
return parseArgs(argv, index + 1, { ...state, coverage: true });
|
|
31
|
+
}
|
|
32
|
+
const key = flag === '--plan' ? 'plan' : flag === '--row' ? 'row' : null;
|
|
33
|
+
const value = argv[index + 1];
|
|
34
|
+
if (key === null) throw usage(`unknown argument ${flag}`);
|
|
35
|
+
if (state[key] !== null) throw usage(`duplicate ${flag}`);
|
|
36
|
+
if (value === undefined || value.startsWith('--') || value.length === 0) throw usage(`absent operand for ${flag}`);
|
|
37
|
+
if (key === 'row' && !ROW_ID.test(value)) throw usage(`invalid --row ${value}`);
|
|
38
|
+
return parseArgs(argv, index + 2, { ...state, [key]: value });
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const renderClass = (entry) => [
|
|
42
|
+
`- **${entry.id}** — ${entry.prove}`,
|
|
43
|
+
...entry.members.map((member) => ` - \`${member.literal}\` — ${member.note} (${member.source})`),
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const isIdentifierByte = (byte) => (byte >= 0x30 && byte <= 0x39) ||
|
|
47
|
+
(byte >= 0x41 && byte <= 0x5a) || byte === 0x5f || (byte >= 0x61 && byte <= 0x7a);
|
|
48
|
+
const hasBoundedLiteral = (bytes, literal) => {
|
|
49
|
+
const token = Buffer.from(literal, 'utf8');
|
|
50
|
+
const cursor = { from: 0 };
|
|
51
|
+
while (cursor.from <= bytes.length - token.length) {
|
|
52
|
+
const hit = bytes.indexOf(token, cursor.from);
|
|
53
|
+
if (hit === -1) return false;
|
|
54
|
+
const leftBounded = hit === 0 || !isIdentifierByte(bytes[hit - 1]);
|
|
55
|
+
const rightBounded = literal.endsWith('_') || hit + token.length === bytes.length || !isIdentifierByte(bytes[hit + token.length]);
|
|
56
|
+
if (leftBounded && rightBounded) return true;
|
|
57
|
+
cursor.from = hit + 1;
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
};
|
|
61
|
+
export const isCoverageDocument = (path) => /\.(?:md|txt)$/u.test(path) || path === SHIPPED_LIST_PATH;
|
|
62
|
+
const describeReadOutcome = (read) => read.outcome === 'foreign'
|
|
63
|
+
? `non-regular ${read.className ?? 'file'}`
|
|
64
|
+
: read.outcome === 'over-cap' ? `over-cap${read.cap ? ` (cap ${read.cap})` : ''}`
|
|
65
|
+
: read.outcome === 'error' ? `error${read.code ? ` (${read.code})` : ''}` : String(read.outcome ?? 'unknown');
|
|
66
|
+
const readCoverageFile = (readRowFile, path) => {
|
|
67
|
+
try {
|
|
68
|
+
const read = readRowFile(path);
|
|
69
|
+
if (read == null) return { outcome: 'error', code: 'empty read outcome' };
|
|
70
|
+
if (read.outcome === 'ok' && !Buffer.isBuffer(read.bytes)) return { outcome: 'error', code: 'ok outcome carried no Buffer' };
|
|
71
|
+
return read;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
return { outcome: 'error', code: error?.code ?? error?.message ?? 'read threw' };
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
export const computeRowCoverage = (planText, list, { expandRowPath, readRowFile }) => {
|
|
77
|
+
const shape = checkPlanStructure(planText, {});
|
|
78
|
+
const classes = new Set(list.classes.map(({ id }) => id));
|
|
79
|
+
const entries = shape.parsed.rows.filter((entry) => entry.valid && !isCoverageDocument(entry.path));
|
|
80
|
+
return entries.reduce((answer, entry) => {
|
|
81
|
+
if (answer.refusal !== null) return answer;
|
|
82
|
+
const robust = parseRobustTag(entry.responsibility);
|
|
83
|
+
if (robust.refusal) return { ...answer, refusal: { id: entry.id, reason: robust.refusal } };
|
|
84
|
+
const unknown = robust.classes.find((classId) => !classes.has(classId));
|
|
85
|
+
if (unknown) return { ...answer, refusal: { id: entry.id, reason: `unknown class ${unknown}` } };
|
|
86
|
+
const tagged = robust.classes;
|
|
87
|
+
const emptyRow = { id: entry.id, path: entry.path, tagged, present: [], uncovered: [], absent: false, deleted: false };
|
|
88
|
+
if (entry.verb === 'delete') {
|
|
89
|
+
return { ...answer, rows: [...answer.rows, { ...emptyRow, deleted: true }] };
|
|
90
|
+
}
|
|
91
|
+
const paths = isSweep(entry.path) ? expandRowPath(entry.path) : [entry.path];
|
|
92
|
+
const reads = paths.map((path) => ({ path, read: readCoverageFile(readRowFile, path) }));
|
|
93
|
+
const refused = reads.find(({ read }) => !['ok', 'absent'].includes(read.outcome));
|
|
94
|
+
if (refused) {
|
|
95
|
+
return { ...answer, refusal: { id: entry.id, path: refused.path, outcome: describeReadOutcome(refused.read) } };
|
|
96
|
+
}
|
|
97
|
+
if (paths.length === 0 || reads.some(({ read }) => read.outcome === 'absent')) {
|
|
98
|
+
return { ...answer, rows: [...answer.rows, { ...emptyRow, absent: true }] };
|
|
99
|
+
}
|
|
100
|
+
const matches = list.classes.flatMap((classEntry) => {
|
|
101
|
+
const member = classEntry.members.find((candidate) => SEARCHABLE_KINDS.has(candidate.kind) &&
|
|
102
|
+
reads.some(({ read }) => hasBoundedLiteral(read.bytes, candidate.literal)));
|
|
103
|
+
return member ? [{ id: entry.id, classId: classEntry.id, literal: member.literal }] : [];
|
|
104
|
+
});
|
|
105
|
+
const present = matches.map(({ classId }) => classId);
|
|
106
|
+
const uncovered = present.filter((classId) => !tagged.includes(classId));
|
|
107
|
+
return {
|
|
108
|
+
rows: [...answer.rows, { ...emptyRow, present, uncovered }],
|
|
109
|
+
evidence: [...answer.evidence, ...matches.filter(({ classId }) => uncovered.includes(classId))],
|
|
110
|
+
refusal: null,
|
|
111
|
+
};
|
|
112
|
+
}, { rows: [], evidence: [], refusal: null });
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const renderCell = (value) => escapeForDisplay(value).replaceAll('|', '\\|');
|
|
116
|
+
export const renderCoverage = (rows, displayPath) => [
|
|
117
|
+
`## Robustness coverage — ${escapeForDisplay(displayPath)}`,
|
|
118
|
+
'| row | path | tagged | present | uncovered |',
|
|
119
|
+
'| --- | --- | --- | --- | --- |',
|
|
120
|
+
...rows.map((row) => row.deleted
|
|
121
|
+
? `| ${row.id} | ${renderCell(row.path)} | delete | — | — |`
|
|
122
|
+
: row.absent ? `| ${row.id} | ${renderCell(row.path)} | absent | — | — |`
|
|
123
|
+
: `| ${row.id} | ${renderCell(row.path)} | ${row.tagged.join(',') || '—'} | ${row.present.join(',') || '—'} | ${row.uncovered.join(',') || '—'} |`),
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
export const renderBrief = (planText, list, { row, displayPath }) => {
|
|
127
|
+
const shape = checkPlanStructure(planText, {});
|
|
128
|
+
if (shape.findings.length > 0) return { code: 1, lines: formatFindings(shape, escapeForDisplay(displayPath)).split('\n').map(escapeForDisplay) };
|
|
129
|
+
const classes = new Map(list.classes.map((entry) => [entry.id, entry]));
|
|
130
|
+
const rows = shape.parsed.rows.filter((entry) => entry.valid).map((entry) => ({
|
|
131
|
+
...entry,
|
|
132
|
+
robust: parseRobustTag(entry.responsibility),
|
|
133
|
+
}));
|
|
134
|
+
const refused = rows.find((entry) => entry.robust.refusal || entry.robust.classes.some((classId) => !classes.has(classId)));
|
|
135
|
+
if (refused) {
|
|
136
|
+
const reason = refused.robust.refusal ?? `unknown class ${refused.robust.classes.find((classId) => !classes.has(classId))}`;
|
|
137
|
+
return { code: 1, lines: [`robustness-brief: ${refused.id}: robust-class — ${reason}`] };
|
|
138
|
+
}
|
|
139
|
+
const selected = row === null ? rows.filter((entry) => entry.robust.classes.length > 0) : rows.filter((entry) => entry.id === row);
|
|
140
|
+
if (row !== null && selected.length === 0) return { code: 1, lines: [`robustness-brief: ${row}: row is absent`] };
|
|
141
|
+
if (row !== null && selected[0].robust.classes.length === 0) return { code: 1, lines: [`robustness-brief: ${row}: row is untagged`] };
|
|
142
|
+
if (selected.length === 0) return { code: 0, lines: [`Robustness literals: ${escapeForDisplay(displayPath)} has no robust tags.`] };
|
|
143
|
+
return {
|
|
144
|
+
code: 0,
|
|
145
|
+
lines: [
|
|
146
|
+
`## Robustness literals — list version ${list.version} · ${escapeForDisplay(displayPath)}`,
|
|
147
|
+
...selected.flatMap((entry) => [
|
|
148
|
+
'',
|
|
149
|
+
`### ${entry.id} — ${escapeForDisplay(entry.path)}`,
|
|
150
|
+
...entry.robust.classes.flatMap((classId) => renderClass(classes.get(classId))),
|
|
151
|
+
]),
|
|
152
|
+
],
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export const main = (argv = process.argv.slice(2), deps = {}) => {
|
|
157
|
+
try {
|
|
158
|
+
const parsed = parseArgs(argv);
|
|
159
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
160
|
+
const realpath = deps.realpath ?? realpathSync;
|
|
161
|
+
const cwdReal = realpath(cwd);
|
|
162
|
+
if (!isAbsolute(parsed.plan) && !lexicalRepoRelative(parsed.plan).ok) throw usage(`uncontained --plan ${parsed.plan}`);
|
|
163
|
+
const planPath = resolve(cwd, parsed.plan);
|
|
164
|
+
let planReal;
|
|
165
|
+
try {
|
|
166
|
+
planReal = realpath(planPath);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (error?.code === 'ENOENT') throw usage(`absent --plan ${parsed.plan}`);
|
|
169
|
+
throw usage(`unreadable --plan ${parsed.plan} (${error?.code ?? error.message})`);
|
|
170
|
+
}
|
|
171
|
+
if (!isInside(cwdReal, planReal)) throw usage(`uncontained --plan ${parsed.plan}`);
|
|
172
|
+
const read = (deps.readPlan ?? readFileBytesNoFollowCapped)(planPath, PLAN_READ_CAP);
|
|
173
|
+
if (read.outcome !== 'ok') {
|
|
174
|
+
const outcome = read.outcome === 'foreign' ? `non-regular ${read.className ?? 'file'}` : read.outcome;
|
|
175
|
+
throw usage(`${outcome} --plan ${parsed.plan}${read.cap ? ` (cap ${read.cap})` : ''}`);
|
|
176
|
+
}
|
|
177
|
+
let planText;
|
|
178
|
+
try {
|
|
179
|
+
planText = decodePlan(read.bytes);
|
|
180
|
+
} catch {
|
|
181
|
+
throw usage(`malformed-utf8 --plan ${parsed.plan}`);
|
|
182
|
+
}
|
|
183
|
+
const list = (deps.readList ?? readShippedRobustnessLiterals)();
|
|
184
|
+
const brief = renderBrief(planText, list, { row: parsed.row, displayPath: parsed.plan });
|
|
185
|
+
if (!parsed.coverage || checkPlanStructure(planText, {}).findings.length > 0) return brief;
|
|
186
|
+
const sweepPaths = checkPlanStructure(planText, {}).parsed.rows
|
|
187
|
+
.filter((entry) => entry.valid && isSweep(entry.path) && !isCoverageDocument(entry.path))
|
|
188
|
+
.map((entry) => entry.path);
|
|
189
|
+
const expansions = deps.expandRowPath === undefined && sweepPaths.length > 0 ? expandSweepPaths(cwd, sweepPaths) : null;
|
|
190
|
+
const coverage = computeRowCoverage(planText, list, {
|
|
191
|
+
expandRowPath: deps.expandRowPath ?? ((path) => expansions[path] ?? []),
|
|
192
|
+
readRowFile: deps.readRowFile ?? ((path) => readFileBytesNoFollowCapped(resolve(cwd, path), PLAN_READ_CAP)),
|
|
193
|
+
});
|
|
194
|
+
if (coverage.refusal) {
|
|
195
|
+
const { id, path, outcome, reason } = coverage.refusal;
|
|
196
|
+
throw usage(reason === undefined ? `${id}: ${escapeForDisplay(path)} — ${outcome}` : `${id}: robust-class — ${reason}`);
|
|
197
|
+
}
|
|
198
|
+
const lines = [...renderCoverage(coverage.rows, parsed.plan), ...coverage.evidence.map(({ id, classId, literal }) => `${id}:${classId}:${literal}`)];
|
|
199
|
+
return { code: coverage.evidence.length > 0 ? 1 : 0, lines };
|
|
200
|
+
} catch (error) {
|
|
201
|
+
return { code: 2, lines: [`robustness-brief: usage — ${escapeForDisplay(error.message)}`, USAGE] };
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
if (isDirectRun(import.meta.url)) {
|
|
206
|
+
const result = main();
|
|
207
|
+
(result.code === 2 ? console.error : console.log)(result.lines.join('\n'));
|
|
208
|
+
process.exitCode = result.code;
|
|
209
|
+
}
|