@sabaiway/agent-workflow-kit 1.39.0 → 1.40.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 +39 -0
- package/README.md +4 -3
- package/SKILL.md +7 -3
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/bootstrap.md +1 -1
- package/references/modes/doc-parity.md +18 -0
- package/references/modes/fold-completeness.md +8 -4
- package/references/modes/grounding.md +4 -3
- package/references/modes/review-ledger.md +1 -1
- package/references/modes/review-state.md +1 -0
- package/references/modes/upgrade.md +3 -1
- package/references/scripts/archive-decisions.mjs +39 -2
- package/references/scripts/archive-decisions.test.mjs +118 -1
- package/references/scripts/check-docs-size.mjs +50 -24
- package/references/scripts/check-docs-size.test.mjs +73 -1
- package/references/templates/agent_rules.md +1 -0
- package/references/templates/verification-profile.json +10 -0
- package/tools/commands.mjs +7 -0
- package/tools/doc-parity.mjs +139 -0
- package/tools/fold-completeness-run.mjs +485 -72
- package/tools/fold-completeness.mjs +108 -15
- package/tools/grounding.mjs +92 -5
- package/tools/lcov.mjs +127 -0
- package/tools/review-ledger-write.mjs +63 -4
- package/tools/review-ledger.mjs +5 -3
- package/tools/review-state.mjs +69 -3
- package/tools/run-gates.mjs +43 -4
- package/tools/sarif.mjs +52 -0
- package/tools/verification-profile.mjs +219 -0
|
@@ -15,14 +15,16 @@
|
|
|
15
15
|
// exit 0 when the resolved plan-execution.review recipe is solo (configured, or degraded there);
|
|
16
16
|
// when no plan is in flight; when the tree is clean; when the cwd is not a git work tree;
|
|
17
17
|
// and when the in-flight plan-execution SEGMENT — (loop, base = git rev-parse HEAD),
|
|
18
|
-
// schema v3
|
|
19
|
-
//
|
|
18
|
+
// schema v3+ (v4 adds the (a) suite-execution evidence + the (c) reattest custody anchor,
|
|
19
|
+
// BUGFREE-3/AD-049), AD-048 D7 — has a CURRENT run record (kind-aware: the latest RUN, never a
|
|
20
|
+
// later red-probe or reattest) whose BOTH bindings match — the tree fingerprint AND the SEGMENT's
|
|
20
21
|
// sorted fixable-bug testId set recorded in the run — with, per bound testId: an
|
|
21
22
|
// N/N-green probe (D4), an observed-red receipt in this SEGMENT (a committed phase's
|
|
22
23
|
// obligations closed with its commit; a receipt never crosses a commit boundary), that receipt
|
|
23
24
|
// PRECEDING the latest run in ledger order (anti-post-hoc), and content CUSTODY — the run's
|
|
24
|
-
// recorded test-file hash equals the latest custody-eligible red-probe hash on that
|
|
25
|
-
// (eligible =
|
|
25
|
+
// recorded test-file hash equals the latest custody-eligible red-probe OR reattest hash on that
|
|
26
|
+
// file (eligible = its own testId is bound AND it precedes the run, D5; a green-only append is
|
|
27
|
+
// re-anchored by a recorded --reattest, not a red-proof waiver); plus 0
|
|
26
28
|
// uncovered changed lines, 0 changed unsupported-source files, a recorded tamper surface
|
|
27
29
|
// with every tampered test-surface file covered by a recorded oracle-change override
|
|
28
30
|
// (review ledger v3), and the reserved EMPTY mutation shape (no mutation ships). A
|
|
@@ -84,8 +86,17 @@ export { probeVerdict, RESULTS_BASENAME, resolveResultsPath };
|
|
|
84
86
|
// v1→v2 precedent), so v1/v2 ledgers never retroactively become malformed — but only a v3 record
|
|
85
87
|
// can enter a segment, so a dirty segment whose loop holds only pre-v3 records fails with a reason
|
|
86
88
|
// naming the schema upgrade (D7 legacy rule).
|
|
87
|
-
|
|
88
|
-
|
|
89
|
+
// SCHEMA v4 (BUGFREE-3 / AD-049): a v4 run record carries a `suite` execution-evidence block (the (a)
|
|
90
|
+
// one-suite-run credit — cmd + exit + pre/post fingerprints) AND a new record kind `reattest` joins
|
|
91
|
+
// `run`/`red-probe`: an operator-minted, RECORDED custody re-attestation that anchors a bound test
|
|
92
|
+
// file's custody at its CURRENT bytes WITHOUT fabricating a red-observe (the honest replacement for
|
|
93
|
+
// mis-using red-proof on a green-only test-file append, item (c)). A placed pre-v4 checker (SUPPORTED
|
|
94
|
+
// = {1,2,3}) rejects a v4 record outright — the version-skew guard; v1/v2/v3 rules are untouched.
|
|
95
|
+
export const RESULT_SCHEMA_VERSION = 4;
|
|
96
|
+
export const SUPPORTED_RESULT_SCHEMAS = new Set([1, 2, 3, 4]);
|
|
97
|
+
// The v4 record kind (c): a recorded re-attestation. Named as a const so the runner, the checker, and
|
|
98
|
+
// the doc-parity lint share one token (no drift).
|
|
99
|
+
export const REATTEST_KIND = 'reattest';
|
|
89
100
|
const ACTIVITY = 'plan-execution';
|
|
90
101
|
const SLOT = 'review';
|
|
91
102
|
|
|
@@ -202,6 +213,30 @@ const validateRedProbe = (obj) => {
|
|
|
202
213
|
return null;
|
|
203
214
|
};
|
|
204
215
|
|
|
216
|
+
// The (c) re-attest receipt (v4 only): the operator asserts a bound test file's CURRENT bytes are a
|
|
217
|
+
// legitimate custody anchor (a green-only append after a --red receipt — there is no red to observe).
|
|
218
|
+
// It mirrors the red-probe receipt MINUS the observation counts: testId + the file's current hash.
|
|
219
|
+
// Minted only by the runner's --reattest verb (self-discipline, the same trust model as the overrides).
|
|
220
|
+
const validateReattest = (obj) => {
|
|
221
|
+
if (!isWellFormedTestId(obj.testId)) return 'reattest testId must be "<test-file>#<test-name-pattern>" (a "#" separator, both halves non-empty)';
|
|
222
|
+
if (!(typeof obj.fileHash === 'string' && HASH_RE.test(obj.fileHash))) return 'reattest fileHash must be a 64-hex content hash';
|
|
223
|
+
return null;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// The (a) suite-execution evidence (v4 run records): the ONE suite spawn per fingerprint, recorded so
|
|
227
|
+
// run-gates --record can CREDIT the unit-tests gate from it instead of re-spawning. Here only the SHAPE
|
|
228
|
+
// is checked (a non-empty cmd, an integer-or-null exit, null-or-string before/after fingerprints); the
|
|
229
|
+
// credit's fingerprint/tree/cmd-identity/exit-0 conditions live in the ledger writer (review-ledger.mjs).
|
|
230
|
+
const validateSuiteEvidence = (suite) => {
|
|
231
|
+
if (!isPlainObject(suite)) return 'a v4 run records its suite-execution evidence ({ cmd, exit, fingerprintBefore, fingerprintAfter })';
|
|
232
|
+
if (!isNonEmptyString(suite.cmd)) return 'suite.cmd must be a non-empty string';
|
|
233
|
+
if (!(suite.exit === null || Number.isInteger(suite.exit))) return 'suite.exit must be an integer or null';
|
|
234
|
+
for (const k of ['fingerprintBefore', 'fingerprintAfter']) {
|
|
235
|
+
if (!(suite[k] === null || isNonEmptyString(suite[k]))) return `suite.${k} must be null or a non-empty fingerprint string`;
|
|
236
|
+
}
|
|
237
|
+
return null;
|
|
238
|
+
};
|
|
239
|
+
|
|
205
240
|
// validateRunRecord(obj) → { ok, reason }. Per-version, per-kind (D2): v1 records (no kind) keep the
|
|
206
241
|
// AD-046 single-run rules; v2 records carry the kind discriminator. The `reason` names the exact
|
|
207
242
|
// failed check so the malformed-line surface and the per-check named tests can assert it. Mutation
|
|
@@ -228,13 +263,27 @@ export const validateRunRecord = (obj) => {
|
|
|
228
263
|
if (obj.tamper !== undefined && (!isPlainObject(obj.tamper) || !isStringArray(obj.tamper.tampered))) {
|
|
229
264
|
return { ok: false, reason: 'tamper.tampered must be an array of strings (when the tamper surface is recorded)' };
|
|
230
265
|
}
|
|
266
|
+
// v4 (a): a v4 runner ALWAYS records the suite-execution evidence, so a v4 run without one is not
|
|
267
|
+
// this runner's output (the tamper-on-v3 precedent). v1/v2/v3 runs never carry it.
|
|
268
|
+
if (obj.schema >= 4) {
|
|
269
|
+
const sr = validateSuiteEvidence(obj.suite);
|
|
270
|
+
if (sr) return { ok: false, reason: sr };
|
|
271
|
+
} else if (obj.suite !== undefined) {
|
|
272
|
+
return { ok: false, reason: `suite is a v4 field — a schema-${obj.schema} run never carries it` };
|
|
273
|
+
}
|
|
231
274
|
return { ok: true };
|
|
232
275
|
}
|
|
233
276
|
if (obj.kind === 'red-probe') {
|
|
234
277
|
const r = validateRedProbe(obj);
|
|
235
278
|
return r ? { ok: false, reason: r } : { ok: true };
|
|
236
279
|
}
|
|
237
|
-
|
|
280
|
+
if (obj.kind === REATTEST_KIND) {
|
|
281
|
+
// (c): a v4-only record kind — a v1/v2/v3 record never carries it (the version-skew guard).
|
|
282
|
+
if (!(obj.schema >= 4)) return { ok: false, reason: `${REATTEST_KIND} is a v4 record kind — a schema-${obj.schema} record never carries it` };
|
|
283
|
+
const r = validateReattest(obj);
|
|
284
|
+
return r ? { ok: false, reason: r } : { ok: true };
|
|
285
|
+
}
|
|
286
|
+
return { ok: false, reason: `kind must be "run", "red-probe", or "${REATTEST_KIND}" (got ${JSON.stringify(obj.kind)})` };
|
|
238
287
|
};
|
|
239
288
|
|
|
240
289
|
// readResults(path) → { records, malformed, malformedReasons, readError }. Absent file → empty (no run
|
|
@@ -280,6 +329,9 @@ export const filterSegmentResults = (records, loop, base) =>
|
|
|
280
329
|
// A v1 record IS a run (the kindless AD-046 shape); a v2 record is a run only under kind:"run".
|
|
281
330
|
export const isRunRecord = (r) => r.schema === 1 || r.kind === 'run';
|
|
282
331
|
export const isRedProbeRecord = (r) => r.schema >= 2 && r.kind === 'red-probe';
|
|
332
|
+
// (c) v4: a recorded custody re-attestation. Never read as "the latest run" (latestRunRecord uses
|
|
333
|
+
// isRunRecord); consumed by the custody chain as a valid anchor for exactly the file it names.
|
|
334
|
+
export const isReattestRecord = (r) => r.schema >= 4 && r.kind === REATTEST_KIND;
|
|
283
335
|
|
|
284
336
|
// latestRunRecord(loopRecords) → { record, index } | null over ONE loop's ordered records. Kind-aware
|
|
285
337
|
// (codex R2): a red-probe appended after a run must never be read as the loop's "latest run" — the
|
|
@@ -295,6 +347,40 @@ export const latestRunRecord = (loopRecords) => {
|
|
|
295
347
|
// above): the runner (--red mints only on 'red'), this checker (the gate passes only on 'green'),
|
|
296
348
|
// and the review-ledger telemetry (quarantine counts) all read the SAME algebra.
|
|
297
349
|
|
|
350
|
+
// foldSuiteCredit({ cwd, env, fingerprint }) → { credited, evidence, reason }. Read half of the (a)
|
|
351
|
+
// credit: may run-gates --record credit the unit-tests gate from the latest segment fold run's suite
|
|
352
|
+
// evidence instead of re-spawning? STRICT — the evidence must be fingerprint-bound (before === after
|
|
353
|
+
// === the current fingerprint, which also proves the tree unchanged) and exit 0. The cmd-identity check
|
|
354
|
+
// (suite.cmd === the gate cmd — the --only-subset defense) is the caller's. Any mismatch → credited:false.
|
|
355
|
+
// ENV residual (bounded, documented — maintainer-signed against Decision 7): the fold V8 suite runs under
|
|
356
|
+
// NODE_V8_COVERAGE, a plain gate spawn does not. Both now strip NODE_TEST_CONTEXT (run-gates.mjs
|
|
357
|
+
// spawnGateViaBash mirrors the fold suite's childTestEnv), so that divergence — the real vacuous-skip
|
|
358
|
+
// false-green — is closed. The ONE remaining delta is NODE_V8_COVERAGE: a test that FAILS under coverage
|
|
359
|
+
// exits nonzero and never credits (the exit-0 gate above), so only a test that PASSES *only* under
|
|
360
|
+
// coverage instrumentation (e.g. asserting process.env.NODE_V8_COVERAGE is set) could credit a green the
|
|
361
|
+
// plain gate would fail — an AD-047-class residual, unclosable without the double spawn (a) removes.
|
|
362
|
+
export const foldSuiteCredit = ({ cwd = process.cwd(), env = process.env, fingerprint } = {}) => {
|
|
363
|
+
const plans = plansInFlight(gitRoot(cwd) ?? cwd);
|
|
364
|
+
if (plans.length !== 1) return { credited: false, reason: 'no single in-flight plan (cannot resolve the loop)' };
|
|
365
|
+
const loop = plans[0].replace(/\.md$/, '');
|
|
366
|
+
const base = resolveBase(cwd);
|
|
367
|
+
const resultsPath = resolveResultsPath(cwd, env);
|
|
368
|
+
// Fail CLOSED on an unreadable / malformed fold ledger — the SAME posture decideCheck takes (a dropped
|
|
369
|
+
// line could hide a defect); a partially-trusted ledger never credits.
|
|
370
|
+
const read = resultsPath ? readResults(resultsPath) : { records: [], malformed: 0 };
|
|
371
|
+
if (read.readError) return { credited: false, reason: `the fold ledger is unreadable (${read.readError}) — fail closed, re-spawn` };
|
|
372
|
+
if (read.malformed > 0) return { credited: false, reason: `the fold ledger has ${read.malformed} malformed line(s) — a partially-read ledger never credits (fail closed), re-spawn` };
|
|
373
|
+
const sel = latestRunRecord(filterSegmentResults(read.records, loop, base));
|
|
374
|
+
if (sel == null) return { credited: false, reason: 'no fold run recorded for the current segment' };
|
|
375
|
+
const { suite } = sel.record;
|
|
376
|
+
if (!suite) return { credited: false, reason: 'the fold run carries no suite evidence (a pre-v4 record)' };
|
|
377
|
+
if (fingerprint == null || suite.fingerprintBefore !== fingerprint || suite.fingerprintAfter !== fingerprint) {
|
|
378
|
+
return { credited: false, reason: 'the fold suite evidence is not bound to the current tree (the tree moved) — re-spawn' };
|
|
379
|
+
}
|
|
380
|
+
if (suite.exit !== 0) return { credited: false, reason: `the fold suite exited ${suite.exit} (a nonzero/red suite never credits a green gate-run) — re-spawn` };
|
|
381
|
+
return { credited: true, evidence: suite };
|
|
382
|
+
};
|
|
383
|
+
|
|
298
384
|
// ── the check + report core ─────────────────────────────────────────────────────────────────────
|
|
299
385
|
|
|
300
386
|
// buildFoldState({ cwd, env, detect }) → everything both renders need. Pure I/O at the edges; every
|
|
@@ -424,6 +510,10 @@ export const decideCheck = (state) => {
|
|
|
424
510
|
const probes = new Map(latest.testIds.map((t) => [t.id, t]));
|
|
425
511
|
const boundSet = new Set(latest.boundTestIds);
|
|
426
512
|
const receipts = loopResults.map((r, i) => ({ r, i })).filter(({ r }) => isRedProbeRecord(r));
|
|
513
|
+
// Custody ANCHORS (c): a red-probe OR a recorded re-attestation on the file re-anchors custody at
|
|
514
|
+
// its hash — the honest replacement for a red-proof waiver on a green-only append. The OBSERVED-RED
|
|
515
|
+
// requirement below stays red-probe-only (a re-attest is not a red observation).
|
|
516
|
+
const custodyAnchors = loopResults.map((r, i) => ({ r, i })).filter(({ r }) => isRedProbeRecord(r) || isReattestRecord(r));
|
|
427
517
|
for (const id of latest.boundTestIds) {
|
|
428
518
|
const t = probes.get(id);
|
|
429
519
|
const verdict = probeVerdict(t);
|
|
@@ -439,13 +529,14 @@ export const decideCheck = (state) => {
|
|
|
439
529
|
if (own.length === 0) return { code: 1, reason: `no observed-red receipt for ${id} — a test never seen failing proves nothing about the fix. BEFORE folding a fix, run: node fold-completeness-run.mjs --red "${id}"` };
|
|
440
530
|
if (!own.some(({ i }) => i < sel.index)) return { code: 1, reason: `the observed-red receipt for ${id} was minted AFTER the loop's latest run — a post-hoc red proves nothing; run fold-completeness-run.mjs once more (a fresh run after the receipt)` };
|
|
441
531
|
const { file } = splitTestId(id);
|
|
442
|
-
// Custody eligibility (D5, codex R3): the anchor is the LATEST
|
|
443
|
-
// testId is in the bound set AND which precedes the latest run — an unbound throwaway
|
|
444
|
-
// or one minted post-run, never re-attests a file.
|
|
445
|
-
|
|
532
|
+
// Custody eligibility (D5, codex R3): the anchor is the LATEST red-probe OR re-attest on that FILE
|
|
533
|
+
// whose own testId is in the bound set AND which precedes the latest run — an unbound throwaway
|
|
534
|
+
// receipt, or one minted post-run, never re-attests a file. A green-only append after a red-probe
|
|
535
|
+
// re-anchors here via a recorded --reattest (item (c)), not a red-proof waiver.
|
|
536
|
+
const eligible = custodyAnchors.filter(({ r, i }) => i < sel.index && boundSet.has(r.testId) && splitTestId(r.testId).file === file);
|
|
446
537
|
const anchor = eligible[eligible.length - 1];
|
|
447
538
|
if (!anchor || t.fileHash == null || anchor.r.fileHash !== t.fileHash) {
|
|
448
|
-
return { code: 1, reason: `custody broken for ${id}: the test file ${file} no longer matches its last observed-red content —
|
|
539
|
+
return { code: 1, reason: `custody broken for ${id}: the test file ${file} no longer matches its last observed-red/re-attested content — after a green-only append, re-attest the file (node fold-completeness-run.mjs --reattest "<a bound testId in ${file}>"); after a real edit, re-observe red (--red "<the file's newest testId>"), or record a red-proof override if the red is genuinely unestablishable` };
|
|
449
540
|
}
|
|
450
541
|
}
|
|
451
542
|
if (latest.coverage.uncoveredChanged.length > 0) return { code: 1, reason: `uncovered changed line(s) — changed code no test executed: ${latest.coverage.uncoveredChanged.map(renderUncovered).join(', ')}` };
|
|
@@ -518,8 +609,9 @@ Usage:
|
|
|
518
609
|
Reads the result ledger the runner writes (<git dir>/${RESULTS_BASENAME}; AW_FOLD_RESULTS overrides),
|
|
519
610
|
resolves the effective ${ACTIVITY}.${SLOT} recipe, recomputes the canonical uncommitted-state
|
|
520
611
|
fingerprint, and decides whether the in-flight plan-execution SEGMENT's changed code is pinned by
|
|
521
|
-
tests — (loop, base = git rev-parse HEAD), schema v3 (
|
|
522
|
-
|
|
612
|
+
tests — (loop, base = git rev-parse HEAD), schema v3+ (v4 adds the suite-execution evidence + the
|
|
613
|
+
reattest custody anchor, BUGFREE-3/AD-049; AD-048 D7): bound testIds, receipts, custody, and tamper
|
|
614
|
+
all filter to the current segment; a committed phase's obligations close with its commit.
|
|
523
615
|
|
|
524
616
|
--status (default) → the human report: resolved recipe, plan-in-flight, the latest run summary
|
|
525
617
|
(per-testId D4 verdicts + rerun counts), the loop's red-probe receipts, verdict.
|
|
@@ -527,7 +619,8 @@ and tamper all filter to the current segment; a committed phase's obligations cl
|
|
|
527
619
|
exit 0 for solo / no plan in flight / a clean tree / not-a-git-tree / a CURRENT segment run whose
|
|
528
620
|
fingerprint AND segment bound-testId set both match, with — per bound testId — an N/N-green probe,
|
|
529
621
|
an observed-red receipt in this segment that PRECEDES the run, and content custody (run hash == the
|
|
530
|
-
latest custody-eligible
|
|
622
|
+
latest custody-eligible red-probe OR reattest hash on that file — a green-only append is re-anchored
|
|
623
|
+
by a recorded --reattest, not a red-proof waiver); plus 0 uncovered changed lines, 0 changed
|
|
531
624
|
unsupported source, every tampered
|
|
532
625
|
test-surface file covered by a recorded oracle-change override, and the reserved empty mutation
|
|
533
626
|
shape. A recorded red-proof override (review-ledger-write override) waives receipt + custody for
|
package/tools/grounding.mjs
CHANGED
|
@@ -30,6 +30,13 @@ import { resolve, join, relative, isAbsolute, dirname, basename } from 'node:pat
|
|
|
30
30
|
import { pathToFileURL } from 'node:url';
|
|
31
31
|
import { spawnSync } from 'node:child_process';
|
|
32
32
|
import { fail } from './orchestration-config.mjs';
|
|
33
|
+
// (e) --ledger-summary (BUGFREE-3 / AD-049): a computed, loop/base-SCOPED review-ledger digest for
|
|
34
|
+
// the facts payload. The review ledger is read-only here — grounding never writes it. ORIGINS +
|
|
35
|
+
// computeTelemetry give the counts; the segment filter gives the scope.
|
|
36
|
+
import { ORIGINS, computeTelemetry, filterSegmentRecords, readLedger, resolveLedgerPath, resolveBase } from './review-ledger.mjs';
|
|
37
|
+
import { plansInFlight } from './review-state.mjs';
|
|
38
|
+
|
|
39
|
+
const PLAN_EXECUTION = 'plan-execution';
|
|
33
40
|
|
|
34
41
|
// The agy single-argv byte contract (mirrors agy-review.sh — the wrapper is the enforcement home).
|
|
35
42
|
export const DEFAULT_MAX_PROMPT_BYTES = 120000;
|
|
@@ -104,6 +111,73 @@ export const trimToBudget = (payload, budget) => {
|
|
|
104
111
|
return { text, trimmedBytes: bytes - keep };
|
|
105
112
|
};
|
|
106
113
|
|
|
114
|
+
// ── (e) --ledger-summary: a loop/base-SCOPED review-ledger digest (read-only, computed) ─────────
|
|
115
|
+
// `computeTelemetry` aggregates ALL loops and is not per-round-facts-shaped, so this renders the
|
|
116
|
+
// in-flight SEGMENT only (plan-execution, loop, base) — the telemetry COUNTS filtered to that
|
|
117
|
+
// segment, plus a terse per-round/triage/override render. Deliberately compact + distinct from the
|
|
118
|
+
// review-ledger `--status` human view (that one is indented + segment-grouped): this is a byte-
|
|
119
|
+
// budgeted FACTS block that agy/codex review against, so unrelated loops are excluded by construction.
|
|
120
|
+
|
|
121
|
+
const baseTag = (base) => (base === null ? '(unborn branch)' : String(base).slice(0, 12));
|
|
122
|
+
const countsOf = (obj) => {
|
|
123
|
+
const keys = Object.keys(obj).sort();
|
|
124
|
+
return keys.length === 0 ? '(none)' : keys.map((k) => `${k}:${obj[k]}`).join(' ');
|
|
125
|
+
};
|
|
126
|
+
const summaryRoundLine = (r) =>
|
|
127
|
+
`round ${r.round} — ${r.backends.map((b) => `${b.backend} ${b.degraded ? `degraded(${b.reason})` : `${b.blockers}/${b.majors}/${b.minors} ${b.verdict}`}`).join(', ')}` +
|
|
128
|
+
(r.findings.length ? ` [${r.findings.map((f) => `${f.findingKey}(${f.severity})`).join(', ')}]` : '');
|
|
129
|
+
const summaryTriageLine = (t) => `triage @round ${t.round} — ${t.classifications.map((c) => `${c.findingKey}=${c.class}`).join(', ')}`;
|
|
130
|
+
const summaryOverrideLine = (o) =>
|
|
131
|
+
`override @round ${o.round} [${o.scope}] — ${o.scope === 'oracle-change' ? o.files.join(', ') : o.scope === 'size-cap' ? `sanctioned ${o.sanctionedLines} lines` : o.testId}: ${o.reason}`;
|
|
132
|
+
const summaryGateRunLine = (g) => `gate-run — status=${g.summary.status} ${g.summary.passed}/${g.summary.gates} green`;
|
|
133
|
+
const summaryRecordLine = (r) =>
|
|
134
|
+
r.kind === 'round' ? summaryRoundLine(r) : r.kind === 'triage' ? summaryTriageLine(r) : r.kind === 'override' ? summaryOverrideLine(r) : summaryGateRunLine(r);
|
|
135
|
+
|
|
136
|
+
// renderLedgerSummary(records, { loop, base }) → the scoped facts section, or '' when the segment
|
|
137
|
+
// holds no records (empty/absent → nothing to ground). Pure: the caller does the I/O.
|
|
138
|
+
export const renderLedgerSummary = (records, { loop, base }) => {
|
|
139
|
+
const segment = filterSegmentRecords(records, { activity: PLAN_EXECUTION, loop, base });
|
|
140
|
+
if (segment.length === 0) return '';
|
|
141
|
+
const t = computeTelemetry(segment, []).loops[0];
|
|
142
|
+
const lines = [
|
|
143
|
+
`## Review-ledger summary — loop ${loop} @ base ${baseTag(base)}`,
|
|
144
|
+
'',
|
|
145
|
+
`rounds ${t.rounds} · divergence-rounds ${t.divergenceRounds}`,
|
|
146
|
+
`finding origins — ${ORIGINS.map((k) => `${k}:${t.origins[k]}`).join(' ')}`,
|
|
147
|
+
`classifications — ${countsOf(t.classifications)}`,
|
|
148
|
+
`backend verdicts — ${Object.keys(t.backendVerdicts).sort().map((b) => `${b}{${countsOf(t.backendVerdicts[b])}}`).join(' · ') || '(none)'}`,
|
|
149
|
+
`overrides — ${countsOf(t.overrides)}`,
|
|
150
|
+
'',
|
|
151
|
+
...segment.map(summaryRecordLine),
|
|
152
|
+
];
|
|
153
|
+
return `${lines.join('\n')}\n`;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// resolveLedgerSummary({ cwd, env }) → the scoped section text for the SINGLE in-flight plan-execution
|
|
157
|
+
// segment. A loud STOP unless exactly one plan is in flight (the family's single-plan discipline —
|
|
158
|
+
// never guess which loop to ground). Reads the ledger read-only.
|
|
159
|
+
const gitTop = (cwd) => {
|
|
160
|
+
const r = gitLine(['rev-parse', '--show-toplevel'], cwd);
|
|
161
|
+
return r && r.status === 0 ? r.stdout.replace(/\r?\n$/, '') : cwd;
|
|
162
|
+
};
|
|
163
|
+
export const resolveLedgerSummary = ({ cwd, env }) => {
|
|
164
|
+
const root = gitTop(cwd);
|
|
165
|
+
const plans = plansInFlight(root);
|
|
166
|
+
if (plans.length !== 1) {
|
|
167
|
+
throw fail(1, `--ledger-summary needs exactly one in-flight plan (in flight: ${plans.length ? plans.join(', ') : 'none'}) — resolve to one active plan`);
|
|
168
|
+
}
|
|
169
|
+
const loop = plans[0].replace(/\.md$/, '');
|
|
170
|
+
const base = resolveBase(cwd);
|
|
171
|
+
const ledgerPath = resolveLedgerPath(cwd, env);
|
|
172
|
+
const { records, readError, malformed, malformedReasons } = ledgerPath ? readLedger(ledgerPath) : { records: [], malformed: 0 };
|
|
173
|
+
// Fail CLOSED like every sibling ledger reader (No-silent-failures): an unreadable OR malformed
|
|
174
|
+
// ledger must never silently render an empty/partial digest the reviewer then grounds against — an
|
|
175
|
+
// empty section would be indistinguishable from a legitimately-empty segment.
|
|
176
|
+
if (readError) throw fail(1, `--ledger-summary cannot read the ledger (${readError}) — failing closed; inspect ${ledgerPath}`);
|
|
177
|
+
if (malformed > 0) throw fail(1, `--ledger-summary: the ledger has ${malformed} malformed line(s) — failing closed (a dropped line could hide a round/finding): ${malformedReasons.join('; ')}`);
|
|
178
|
+
return renderLedgerSummary(records, { loop, base });
|
|
179
|
+
};
|
|
180
|
+
|
|
107
181
|
// ── the --out destination guard (gitignored / out-of-repo scratch ONLY) ────────────────
|
|
108
182
|
|
|
109
183
|
const gitLine = (args, cwd) => {
|
|
@@ -157,13 +231,17 @@ export const assertScratchDestination = (outPath, cwd) => {
|
|
|
157
231
|
const HELP = `grounding — grounded-review facts assembler for the agent-workflow family (AD-038).
|
|
158
232
|
|
|
159
233
|
Usage:
|
|
160
|
-
node grounding.mjs [--constraints] [--plan <path>] [--reserve-bytes <n>] [--out <path>]
|
|
234
|
+
node grounding.mjs [--constraints] [--plan <path>] [--ledger-summary] [--reserve-bytes <n>] [--out <path>]
|
|
161
235
|
|
|
162
236
|
--constraints slice the root AGENTS.md "Hard Constraints" section verbatim
|
|
163
237
|
(exactly one matching heading, else a loud STOP)
|
|
164
238
|
--plan <path> extract the plan's decision-bearing sections verbatim + whole:
|
|
165
239
|
"## Approach" + "## Verification" (REQUIRED — STOP if missing),
|
|
166
240
|
"## Decisions (locked)" when present; a duplicate heading is a STOP
|
|
241
|
+
--ledger-summary append a COMPUTED review-ledger digest for the SINGLE in-flight
|
|
242
|
+
plan-execution segment (rounds · origins · classifications · verdicts ·
|
|
243
|
+
overrides + a per-round render) — "computed, not remembered" facts; a
|
|
244
|
+
loud STOP unless exactly one plan is in flight
|
|
167
245
|
--reserve-bytes <n> the artifact share agy-review will add around these facts — the output
|
|
168
246
|
budget becomes AGY_MAX_PROMPT_BYTES − n (loud tail-trim on overflow)
|
|
169
247
|
--out <path> write instead of stdout — gitignored / out-of-repo scratch ONLY
|
|
@@ -181,9 +259,11 @@ const parseArgs = (argv) => {
|
|
|
181
259
|
let plan = null;
|
|
182
260
|
let out = null;
|
|
183
261
|
let reserve = 0;
|
|
262
|
+
let ledgerSummary = false;
|
|
184
263
|
for (let i = 0; i < argv.length; i += 1) {
|
|
185
264
|
const a = argv[i];
|
|
186
265
|
if (a === '--constraints') constraints = true;
|
|
266
|
+
else if (a === '--ledger-summary') ledgerSummary = true;
|
|
187
267
|
else if (a === '--plan') {
|
|
188
268
|
plan = argv[i + 1];
|
|
189
269
|
if (!plan || plan.startsWith('--')) throw fail(2, '--plan requires a <path>');
|
|
@@ -199,8 +279,8 @@ const parseArgs = (argv) => {
|
|
|
199
279
|
i += 1;
|
|
200
280
|
} else throw fail(2, `unknown argument: ${a}`);
|
|
201
281
|
}
|
|
202
|
-
if (!constraints && plan == null) throw fail(2, 'nothing to assemble — pass --constraints and/or --
|
|
203
|
-
return { constraints, plan, out, reserve };
|
|
282
|
+
if (!constraints && plan == null && !ledgerSummary) throw fail(2, 'nothing to assemble — pass --constraints, --plan <path>, and/or --ledger-summary');
|
|
283
|
+
return { constraints, plan, out, reserve, ledgerSummary };
|
|
204
284
|
};
|
|
205
285
|
|
|
206
286
|
const resolveBudget = (env, reserve) => {
|
|
@@ -220,7 +300,7 @@ export const main = (argv, ctx = {}) => {
|
|
|
220
300
|
const env = ctx.env ?? process.env;
|
|
221
301
|
try {
|
|
222
302
|
if (argv.includes('--help') || argv.includes('-h')) return { code: 0, stdout: HELP, stderr: '' };
|
|
223
|
-
const { constraints, plan, out, reserve } = parseArgs(argv);
|
|
303
|
+
const { constraints, plan, out, reserve, ledgerSummary } = parseArgs(argv);
|
|
224
304
|
const budget = resolveBudget(env, reserve);
|
|
225
305
|
|
|
226
306
|
const readOrStop = (path, label) => {
|
|
@@ -233,7 +313,14 @@ export const main = (argv, ctx = {}) => {
|
|
|
233
313
|
const constraintsText = constraints ? readOrStop('AGENTS.md', 'root AGENTS.md') : null;
|
|
234
314
|
const planText = plan != null ? readOrStop(plan, 'plan file') : null;
|
|
235
315
|
|
|
236
|
-
const
|
|
316
|
+
const parts = [];
|
|
317
|
+
const assembled = assembleGrounding({ constraintsText, planText, planLabel: plan ?? 'plan' });
|
|
318
|
+
if (assembled) parts.push(assembled);
|
|
319
|
+
if (ledgerSummary) {
|
|
320
|
+
const summary = resolveLedgerSummary({ cwd, env });
|
|
321
|
+
if (summary) parts.push(summary);
|
|
322
|
+
}
|
|
323
|
+
const payload = parts.join('\n');
|
|
237
324
|
const { text, trimmedBytes } = trimToBudget(payload, budget);
|
|
238
325
|
const stderr = trimmedBytes > 0
|
|
239
326
|
? `[grounding] TRIM: assembled ${Buffer.byteLength(payload, 'utf8')} bytes > budget ${budget} (AGY_MAX_PROMPT_BYTES minus --reserve-bytes ${reserve}) — dropped ${trimmedBytes} tail bytes; the cut is marked in-band.`
|
package/tools/lcov.mjs
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// lcov.mjs — a dependency-free LCOV parser for the fold-completeness runner's coverage.kind:"lcov"
|
|
3
|
+
// branch (BUGFREE-3, AD-049 — the language-independence contract). When a verification profile
|
|
4
|
+
// declares coverage.kind:"lcov", the consumer's OWN suite leaves an LCOV file at the declared path
|
|
5
|
+
// (the diff-cover / c8 --reporter=lcov pattern) and this module reads it into a per-file
|
|
6
|
+
// line→hits map, keyed by the SAME canonical absolute path the V8 coverage map uses — so the
|
|
7
|
+
// runner's ONE uncovered-changed loop consumes either source unchanged (computeUncoveredLines /
|
|
8
|
+
// effectiveCount stay the V8-only path, D10).
|
|
9
|
+
//
|
|
10
|
+
// FAIL-CLOSED posture (the whole reason the fold gate exists): a malformed record can never mark a
|
|
11
|
+
// line COVERED — a DA with a valid line but a non-integer HIT count reads the line UNCOVERED (hits 0,
|
|
12
|
+
// the gate fails), and a DA with no valid line number is skipped (unattributable — there is no line to
|
|
13
|
+
// flag), never a false green. An SF path that resolves OUTSIDE the repo — or does not resolve at all —
|
|
14
|
+
// is never trusted as coverage (a wrong file could otherwise read covered). (STATED residual, inherent
|
|
15
|
+
// to the LCOV data model: a CONFORMANT producer emits DA:N,0 for every uncovered executable line, so a
|
|
16
|
+
// changed executable line with NO DA entry reads non-executable — LCOV carries no separate
|
|
17
|
+
// executability signal; the V8 path, which reads an absent line as uncovered, differs by format.)
|
|
18
|
+
//
|
|
19
|
+
// Dependency-free, Node >= 18. No side effects on import.
|
|
20
|
+
|
|
21
|
+
import { join, resolve, isAbsolute, sep } from 'node:path';
|
|
22
|
+
import { realpathSync } from 'node:fs';
|
|
23
|
+
|
|
24
|
+
// Only these three LCOV records are consulted; every other record (FN/FNDA/BRDA/LF/LH/BRF/…) is
|
|
25
|
+
// ignored. LCOV groups per-file sections between an `SF:<path>` line and `end_of_record`.
|
|
26
|
+
const SF_RE = /^SF:(.*)$/;
|
|
27
|
+
const DA_RE = /^DA:(\d+),(.*)$/; // DA:<line>,<hits-field>… — the LINE must be an int; the hits field is
|
|
28
|
+
// parsed leniently below so a truncated/malformed hit count reads UNCOVERED (fail-closed), not omitted.
|
|
29
|
+
const END_RE = /^end_of_record\s*$/;
|
|
30
|
+
|
|
31
|
+
// parseLcov(text) → Map<sfString, Map<lineNumber, hits>>. Raw SF strings (NOT yet canonicalized).
|
|
32
|
+
// A line's hits is the MAX across duplicate DA entries within a section (some tracers emit per-test
|
|
33
|
+
// sections that also repeat lines). A DA whose line or hit count is not a base-10 integer is skipped
|
|
34
|
+
// (fail-closed: it can never mark a line covered). Lines with no DA entry never appear (a
|
|
35
|
+
// non-executable line — blank/comment — is simply absent, mirroring the V8 blank-line skip).
|
|
36
|
+
export const parseLcov = (text) => {
|
|
37
|
+
const out = new Map();
|
|
38
|
+
let current = null; // the Map<line,hits> for the SF section in progress
|
|
39
|
+
for (const rawLine of String(text).split('\n')) {
|
|
40
|
+
const line = rawLine.replace(/\r$/, '');
|
|
41
|
+
const sf = SF_RE.exec(line);
|
|
42
|
+
if (sf) {
|
|
43
|
+
const path = sf[1].trim();
|
|
44
|
+
if (path.length === 0) {
|
|
45
|
+
current = null;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
current = out.get(path);
|
|
49
|
+
if (!current) {
|
|
50
|
+
current = new Map();
|
|
51
|
+
out.set(path, current);
|
|
52
|
+
}
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (END_RE.test(line)) {
|
|
56
|
+
current = null;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const da = DA_RE.exec(line);
|
|
60
|
+
if (da && current) {
|
|
61
|
+
const n = Number.parseInt(da[1], 10);
|
|
62
|
+
if (!Number.isInteger(n) || n < 1) continue; // no valid line number → unattributable, skip
|
|
63
|
+
const hitsField = da[2].split(',')[0]; // the hit count (a trailing ,checksum is ignored)
|
|
64
|
+
// FAIL-CLOSED, STRICT: the hit count must be ALL digits — a truncated `DA:42,`, a
|
|
65
|
+
// `DA:2,xyz`, a partial `DA:5,1abc`, or a fractional `DA:6,2.5` all read UNCOVERED (hits 0), never
|
|
66
|
+
// parseInt-coerced to a positive "covered" value. A covered sibling still wins (max).
|
|
67
|
+
const effHits = /^\d+$/.test(hitsField) ? Number.parseInt(hitsField, 10) : 0;
|
|
68
|
+
current.set(n, Math.max(current.get(n) ?? 0, effHits));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const defaultCanon = (p) => {
|
|
75
|
+
try {
|
|
76
|
+
return realpathSync(p);
|
|
77
|
+
} catch {
|
|
78
|
+
return p;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Segment-safe containment ('/a' never contains '/ab'); correct at the filesystem root (mirrors
|
|
83
|
+
// fold-completeness-run.mjs containsPath). Defined locally so lcov.mjs never imports the runner (the
|
|
84
|
+
// runner imports THIS module).
|
|
85
|
+
const containsPath = (realRoot, realAbs) =>
|
|
86
|
+
realAbs === realRoot || realAbs.startsWith(realRoot.endsWith(sep) ? realRoot : realRoot + sep);
|
|
87
|
+
|
|
88
|
+
// lcovCoveredMap(text, rootTop, { canon }?) → Map<canonAbsKey, Map<line, hits>>. Each SF is resolved
|
|
89
|
+
// against rootTop (a relative / ./-prefixed / absolute-in-repo SF all normalize to the same key the
|
|
90
|
+
// V8 map uses), canon()-normalized, and KEPT only when it resolves strictly INSIDE the repo root; an
|
|
91
|
+
// outside-tree or unresolvable SF is DROPPED (never trusted as coverage). Duplicate SF sections are
|
|
92
|
+
// merged taking the max hits per line. canon is injectable for hermetic tests (default realpathSync).
|
|
93
|
+
export const lcovCoveredMap = (text, rootTop, deps = {}) => {
|
|
94
|
+
const canon = deps.canon ?? defaultCanon;
|
|
95
|
+
const raw = parseLcov(text);
|
|
96
|
+
const rootCanon = canon(rootTop);
|
|
97
|
+
const out = new Map();
|
|
98
|
+
for (const [sf, lineHits] of raw) {
|
|
99
|
+
const abs = isAbsolute(sf) ? sf : resolve(rootTop, sf);
|
|
100
|
+
const key = canon(abs);
|
|
101
|
+
if (!containsPath(rootCanon, key)) continue; // outside the repo (or unresolved elsewhere) → drop
|
|
102
|
+
let m = out.get(key);
|
|
103
|
+
if (!m) {
|
|
104
|
+
m = new Map();
|
|
105
|
+
out.set(key, m);
|
|
106
|
+
}
|
|
107
|
+
for (const [line, hits] of lineHits) m.set(line, Math.max(m.get(line) ?? 0, hits));
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// uncoveredChangedFromLcov(coveredMap, key, changedLines) → the sorted changed lines that the LCOV
|
|
113
|
+
// records as EXECUTABLE-but-unexecuted (a DA entry with 0 hits). A file ABSENT from the map (no SF)
|
|
114
|
+
// is signalled by a null return → the caller records a file-level RED (line:null), exactly like the
|
|
115
|
+
// V8 "absent from coverage" case. A changed line with NO DA entry in a PRESENT file is non-executable
|
|
116
|
+
// (blank/comment) and never flagged — mirroring the V8 blank-line skip.
|
|
117
|
+
export const uncoveredChangedFromLcov = (coveredMap, key, changedLines) => {
|
|
118
|
+
const lineHits = coveredMap.get(key);
|
|
119
|
+
if (!lineHits) return null; // file absent from coverage → file-level RED (caller decides)
|
|
120
|
+
const uncovered = [];
|
|
121
|
+
for (const n of changedLines) {
|
|
122
|
+
const hits = lineHits.get(n);
|
|
123
|
+
if (hits === 0) uncovered.push(n); // executable, 0 hits → uncovered
|
|
124
|
+
// hits > 0 → covered; hits === undefined → non-executable line, skip
|
|
125
|
+
}
|
|
126
|
+
return [...new Set(uncovered)].sort((a, b) => a - b);
|
|
127
|
+
};
|
|
@@ -37,7 +37,7 @@ import { dirname } from 'node:path';
|
|
|
37
37
|
import { pathToFileURL } from 'node:url';
|
|
38
38
|
import { spawnSync } from 'node:child_process';
|
|
39
39
|
import { writeContainedFileAtomic } from './atomic-write.mjs';
|
|
40
|
-
import { computeTreeFingerprint, plansInFlight, readReceipts, resolveReceiptsPath } from './review-state.mjs';
|
|
40
|
+
import { buildState, computeTreeFingerprint, plansInFlight, readReceipts, resolveReceiptsPath } from './review-state.mjs';
|
|
41
41
|
import {
|
|
42
42
|
REVIEW_CAP,
|
|
43
43
|
SCHEMA_VERSION,
|
|
@@ -101,6 +101,52 @@ const appendRecord = (ledgerPath, record, deps = {}) => {
|
|
|
101
101
|
return { writtenPath: ledgerPath, record };
|
|
102
102
|
};
|
|
103
103
|
|
|
104
|
+
// ── (g) record --from-receipts: draft the backends[] from the current-fingerprint receipts ────────
|
|
105
|
+
// (BUGFREE-3 / AD-049). The orchestrator hand-maintained each backend's { backend, verdict } beside
|
|
106
|
+
// the counts every round — drift-prone busywork. --from-receipts DRAFTS the backends[] instead: for
|
|
107
|
+
// each recipe-named backend it reads the review-state receipt status (verdict from the fresh grounded
|
|
108
|
+
// code receipt) and computes the counts from the orchestrator's OWN supplied findings; `origins` and
|
|
109
|
+
// `findings` stay explicit input. It NEVER invents a backend — a recipe-named backend with no fresh
|
|
110
|
+
// grounded receipt is a LOUD STOP (run its review, or supply it explicitly as a degraded backend).
|
|
111
|
+
// The assembled backends[] then rides the normal recordRound teeth (validateRound re-checks that
|
|
112
|
+
// findings-by-severity equals the drafted counts — true by construction).
|
|
113
|
+
export const draftBackendsFromReceipts = ({ state, findings = [], explicitBackends = [] }, deps = {}) => {
|
|
114
|
+
const stopFn = deps.stop ?? stop;
|
|
115
|
+
const explicitByName = new Map((Array.isArray(explicitBackends) ? explicitBackends : []).filter((b) => b && typeof b.backend === 'string').map((b) => [b.backend, b]));
|
|
116
|
+
const findingsArr = Array.isArray(findings) ? findings : [];
|
|
117
|
+
const countFor = (name) => {
|
|
118
|
+
const own = findingsArr.filter((f) => f && f.backend === name);
|
|
119
|
+
return {
|
|
120
|
+
blockers: own.filter((f) => f.severity === 'blocker').length,
|
|
121
|
+
majors: own.filter((f) => f.severity === 'major').length,
|
|
122
|
+
minors: own.filter((f) => f.severity === 'minor').length,
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
return state.requiredBackends.map((name) => {
|
|
126
|
+
const explicit = explicitByName.get(name);
|
|
127
|
+
if (explicit) {
|
|
128
|
+
// An explicit row is honored verbatim ONLY for a DEGRADED backend (a bridge the operator knows
|
|
129
|
+
// is down, which minted no receipt). A NON-degraded explicit row would BYPASS the
|
|
130
|
+
// receipt-derived verdict --from-receipts exists to compute (a stale hand-composed row silently
|
|
131
|
+
// winning) — a loud STOP, fail-closed (codex council R1). Drop --from-receipts to compose a
|
|
132
|
+
// non-degraded row by hand, or mark the backend degraded.
|
|
133
|
+
if (explicit.degraded !== true) {
|
|
134
|
+
throw stopFn(
|
|
135
|
+
`refusing --from-receipts: an explicit non-degraded backends[] row for ${name} would bypass the receipt-derived verdict this flag computes — mark it degraded (a bridge that is down), or drop --from-receipts and compose the round by hand`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return explicit;
|
|
139
|
+
}
|
|
140
|
+
const status = state.backends.find((b) => b.backend === name);
|
|
141
|
+
if (!status || status.state !== 'current') {
|
|
142
|
+
throw stopFn(
|
|
143
|
+
`refusing --from-receipts: no fresh grounded code receipt for ${name} (state: ${status?.state ?? 'missing'}) — run its review wrapper (codex-review code / agy-review code --facts @f) first, or supply it explicitly as a degraded backend in the payload's backends[]`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
return { backend: name, degraded: false, ...countFor(name), verdict: status.verdict };
|
|
147
|
+
});
|
|
148
|
+
};
|
|
149
|
+
|
|
104
150
|
// ── recordRound (the teeth + the integrity binding) ─────────────────────────────────────────────
|
|
105
151
|
|
|
106
152
|
// recordRound({ cwd, env, loop, activity, round, origins, backends, findings, timestamp }, deps) →
|
|
@@ -365,7 +411,7 @@ export const recordTriage = (params, deps = {}) => {
|
|
|
365
411
|
const HELP = `review-ledger-write — the review-round ledger WRITER (agent-workflow family, AD-045 + AD-047 + AD-048).
|
|
366
412
|
|
|
367
413
|
Usage:
|
|
368
|
-
node review-ledger-write.mjs record --json '<round-payload>'
|
|
414
|
+
node review-ledger-write.mjs record --json '<round-payload>' [--from-receipts] [--cwd <dir>]
|
|
369
415
|
node review-ledger-write.mjs classify --json '<triage-payload>' [--cwd <dir>]
|
|
370
416
|
node review-ledger-write.mjs override --json '<override-payload>' [--cwd <dir>]
|
|
371
417
|
|
|
@@ -373,7 +419,11 @@ Every verb operates on the current SEGMENT — (loop, base = git rev-parse HEAD)
|
|
|
373
419
|
caps, and teeth reset only when a gated commit moves base (schema 4; records carry base).
|
|
374
420
|
|
|
375
421
|
record appends one review round. The JSON payload carries { loop, round, origins, backends,
|
|
376
|
-
findings } (activity defaults to plan-execution; timestamp defaults to now).
|
|
422
|
+
findings } (activity defaults to plan-execution; timestamp defaults to now). With
|
|
423
|
+
--from-receipts the backends[] is DRAFTED from the current-fingerprint grounded code
|
|
424
|
+
receipts (verdict per backend) with counts computed from the supplied findings — origins /
|
|
425
|
+
findings stay explicit; a recipe-named backend with no receipt is a LOUD stop (run its
|
|
426
|
+
review, or supply it explicitly as a degraded backend). REFUSES while
|
|
377
427
|
triage is required; beyond the hard-max ceiling of ${HARD_MAX} rounds within one segment;
|
|
378
428
|
while a blocking finding of the segment's previous round VANISHED unclassified (D6 —
|
|
379
429
|
classify it first, "refuted" is the honest phantom lane); while the changed source surface
|
|
@@ -401,7 +451,7 @@ The read-only checker is a SEPARATE tool: node review-ledger.mjs --check / --sta
|
|
|
401
451
|
Exit codes: 0 written; 1 a typed STOP (teeth / malformed / missing receipt / fs error); 2 usage.`;
|
|
402
452
|
|
|
403
453
|
const parseArgs = (argv) => {
|
|
404
|
-
const opts = { cwd: undefined, json: undefined };
|
|
454
|
+
const opts = { cwd: undefined, json: undefined, fromReceipts: false };
|
|
405
455
|
for (let i = 0; i < argv.length; i += 1) {
|
|
406
456
|
const a = argv[i];
|
|
407
457
|
if (a === '--cwd') {
|
|
@@ -412,6 +462,8 @@ const parseArgs = (argv) => {
|
|
|
412
462
|
opts.json = argv[i + 1];
|
|
413
463
|
if (opts.json === undefined) throw usageFail('--json needs a JSON payload');
|
|
414
464
|
i += 1;
|
|
465
|
+
} else if (a === '--from-receipts') {
|
|
466
|
+
opts.fromReceipts = true;
|
|
415
467
|
} else {
|
|
416
468
|
throw usageFail(`unknown argument: ${a}`);
|
|
417
469
|
}
|
|
@@ -436,8 +488,15 @@ export const main = (argv, ctx = {}) => {
|
|
|
436
488
|
const sub = argv[0];
|
|
437
489
|
if (sub !== 'record' && sub !== 'classify' && sub !== 'override') throw usageFail(`unknown subcommand "${sub}" (expected: record | classify | override)`);
|
|
438
490
|
const opts = parseArgs(argv.slice(1));
|
|
491
|
+
if (opts.fromReceipts && sub !== 'record') throw usageFail('--from-receipts applies only to `record`');
|
|
439
492
|
const payload = parsePayload(opts.json);
|
|
440
493
|
const cwd = opts.cwd ?? cwd0;
|
|
494
|
+
if (opts.fromReceipts) {
|
|
495
|
+
// Draft backends[] from the current-fingerprint receipts + the supplied findings; origins /
|
|
496
|
+
// findings stay explicit. The drafted array then rides the normal recordRound teeth.
|
|
497
|
+
const state = buildState({ cwd, env });
|
|
498
|
+
payload.backends = draftBackendsFromReceipts({ state, findings: payload.findings, explicitBackends: payload.backends ?? [] });
|
|
499
|
+
}
|
|
441
500
|
const result =
|
|
442
501
|
sub === 'record'
|
|
443
502
|
? recordRound({ cwd, env, ...payload })
|
package/tools/review-ledger.mjs
CHANGED
|
@@ -104,13 +104,15 @@ const SEVERITIES = new Set(['blocker', 'major', 'minor']);
|
|
|
104
104
|
export const ORIGINS = ['first-draft', 'fold-induced', 'mechanics'];
|
|
105
105
|
const CLASSES = new Set(['fixable-bug', 'inherent-layer-residual', 'escalate']);
|
|
106
106
|
// v4 (BUGFREE-2 / D6): `refuted` — the honest lane for a phantom finding, refuted against code with
|
|
107
|
-
// a MANDATORY non-empty note citing the grounds; never silently dropped, never folded.
|
|
108
|
-
|
|
107
|
+
// a MANDATORY non-empty note citing the grounds; never silently dropped, never folded. Exported as
|
|
108
|
+
// the code-side vocabulary source the doc-parity lint (BUGFREE-3 / AD-049) checks the contract docs
|
|
109
|
+
// against — so the mode files can never drift from the schema's own class/scope lexicon.
|
|
110
|
+
export const V4_CLASSES = new Set([...CLASSES, 'refuted']);
|
|
109
111
|
const OVERRIDE_SCOPES = new Set(['oracle-change', 'red-proof']);
|
|
110
112
|
// v4 (BUGFREE-2 / D4): `size-cap` — the recorded waiver for a changed surface beyond the diff cap;
|
|
111
113
|
// exact payload carries the sanctioned magnitude, and it is SEGMENT-scoped (loop + base), unlike
|
|
112
114
|
// the two loop-scoped v3 scopes.
|
|
113
|
-
const V4_OVERRIDE_SCOPES = new Set([...OVERRIDE_SCOPES, 'size-cap']);
|
|
115
|
+
export const V4_OVERRIDE_SCOPES = new Set([...OVERRIDE_SCOPES, 'size-cap']);
|
|
114
116
|
|
|
115
117
|
// ── git-dir resolution (read-only queries; the ledger lives in the git dir, uncommittable) ──────
|
|
116
118
|
|