@pareto4/pp-core 1.12.1 → 1.13.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/.claude-plugin/plugin.json +1 -1
- package/agents/pp-code-reviewer.md +1 -1
- package/agents/pp-executor.md +2 -13
- package/agents/pp-planner.md +1 -1
- package/agents/pp-vault-reviewer.md +51 -0
- package/commands/pp-ns-review.md +2 -1
- package/commands/pp-vault-review.md +33 -0
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/pp-core/bin/lib/capability-registry.cjs +200 -10
- package/pp-core/bin/lib/check-command-router.cjs +1242 -37
- package/pp-core/bin/lib/clusters.cjs +1 -0
- package/pp-core/bin/lib/command-aliases.cjs +7 -0
- package/pp-core/bin/lib/compose-bindings.cjs +41 -27
- package/pp-core/bin/lib/decisions.cjs +104 -27
- package/pp-core/bin/lib/dispatch-plan.cjs +12 -1
- package/pp-core/bin/lib/frontmatter.cjs +37 -0
- package/pp-core/bin/lib/git-base-branch.cjs +136 -10
- package/pp-core/bin/lib/init-contract.cjs +180 -66
- package/pp-core/bin/lib/init-interactive.cjs +4 -20
- package/pp-core/bin/lib/loop-gates.cjs +213 -0
- package/pp-core/bin/lib/loop-resolver.cjs +107 -45
- package/pp-core/bin/lib/org-profile.cjs +218 -44
- package/pp-core/bin/lib/phase-command-router.cjs +7 -1
- package/pp-core/bin/lib/phase-manifest.cjs +527 -0
- package/pp-core/bin/lib/phase-tier.cjs +15 -2
- package/pp-core/bin/lib/phase.cjs +51 -1
- package/pp-core/bin/lib/vault-audit.cjs +2070 -0
- package/pp-core/bin/lib/vault-command-router.cjs +460 -1
- package/pp-core/bin/lib/vault-practices.cjs +1142 -104
- package/pp-core/bin/lib/vault-review.cjs +2359 -0
- package/pp-core/bin/lib/vault-trace.cjs +2831 -0
- package/pp-core/bin/lib/vault-update.cjs +266 -0
- package/pp-core/bin/lib/vault-write-back.cjs +19 -0
- package/pp-core/bin/lib/vault.cjs +277 -12
- package/pp-core/bin/pp-tools.cjs +30 -13
- package/pp-core/bin/shared/model-catalog.json +2 -1
- package/pp-core/references/executor-vault-trace.md +47 -0
- package/pp-core/references/loop-hook-dispatch.md +13 -2
- package/pp-core/references/planner-vault-practices.md +46 -1
- package/pp-core/references/planning-config.md +1 -1
- package/pp-core/templates/practices/README.md +140 -11
- package/pp-core/templates/project-charter.md +226 -0
- package/pp-core/workflows/add-tests.md +4 -0
- package/pp-core/workflows/audit-fix.md +2 -0
- package/pp-core/workflows/check-todos.md +1 -1
- package/pp-core/workflows/code-review-fix.md +2 -0
- package/pp-core/workflows/debug.md +4 -0
- package/pp-core/workflows/discovery-sprint/prefill.md +9 -0
- package/pp-core/workflows/discovery-sprint/prototype.md +2 -1
- package/pp-core/workflows/discovery-sprint/vault-fetch.md +37 -0
- package/pp-core/workflows/discovery-sprint.md +1 -1
- package/pp-core/workflows/discovery-to-build.md +1 -1
- package/pp-core/workflows/execute-phase/steps/serial-group-dispatch.md +1 -0
- package/pp-core/workflows/execute-phase/steps/vault-door-gate.md +19 -0
- package/pp-core/workflows/execute-phase/steps/vault-fetch.md +1 -1
- package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +10 -8
- package/pp-core/workflows/execute-phase.md +22 -5
- package/pp-core/workflows/execute-plan.md +4 -0
- package/pp-core/workflows/fast.md +4 -0
- package/pp-core/workflows/help/modes/full.md +3 -2
- package/pp-core/workflows/init.md +2 -2
- package/pp-core/workflows/new-project.md +1 -1
- package/pp-core/workflows/plan-phase/steps/phase-tier.md +9 -0
- package/pp-core/workflows/plan-phase.md +7 -4
- package/pp-core/workflows/quick.md +4 -0
- package/pp-core/workflows/ship.md +5 -1
- package/pp-core/workflows/undo.md +1 -1
- package/pp-core/workflows/validate-phase.md +4 -0
- package/pp-core/workflows/vault-review.md +65 -0
- package/pp-core/workflows/verify-work.md +3 -0
- package/scripts/gen-capability-registry.cjs +11 -0
- package/scripts/lint-test-file-count.allowlist.json +10 -1
- package/scripts/prompt-injection-scan.sh +2 -0
|
@@ -0,0 +1,2359 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* src/vault-review.cts — the DETERMINISTIC half of the agent practice review
|
|
7
|
+
* (`/pp-vault-review`, vault assurance R5).
|
|
8
|
+
*
|
|
9
|
+
* ## What this module is
|
|
10
|
+
*
|
|
11
|
+
* The agent review judges vault practices no `check:` block can settle. A
|
|
12
|
+
* judgement is not reproducible, so everything the agent must NOT invent lives
|
|
13
|
+
* here instead: the input set, both derived-N-A populations, the counts, the
|
|
14
|
+
* rate line, the git range the review was computed over, the artifact the agent
|
|
15
|
+
* fills in, and the lines that artifact must state. Every one of them is an
|
|
16
|
+
* exported, unit-tested function, so the agent's discretionary surface shrinks
|
|
17
|
+
* to the genuinely ambiguous remainder and nothing else.
|
|
18
|
+
*
|
|
19
|
+
* Purely a LEAF. It imports leaves (`./vault-audit.cjs`, `./vault-practices.cjs`,
|
|
20
|
+
* `./vault-trace.cjs`, `./practices-review-check.cjs`, `./git-base-branch.cjs`,
|
|
21
|
+
* `./shell-command-projection.cjs`) and no adapter. The two functions that touch
|
|
22
|
+
* the world — the oracle run and the range resolver — take INJECTED seams
|
|
23
|
+
* (`_audit`, `exec`), so a test drives them without a repo. The I/O composition
|
|
24
|
+
* (reading the trace, the vault cache and the verdict owner) is the CLI arm's,
|
|
25
|
+
* in `src/check-command-router.cts`.
|
|
26
|
+
*
|
|
27
|
+
* The sibling modules are imported at the top rather than required at call
|
|
28
|
+
* time: under `recommendedTypeChecked` a bare `require()` returns `any`, while
|
|
29
|
+
* `import x = require('./y.cjs')` resolves to the sibling's `.cts` source and is
|
|
30
|
+
* fully typed. None of them performs I/O at load.
|
|
31
|
+
*
|
|
32
|
+
* ## The verdict vocabulary is LOCKED
|
|
33
|
+
*
|
|
34
|
+
* `REVIEW_VERDICT` holds exactly PASS / FAIL / N-A / INCONCLUSIVE. It is a module
|
|
35
|
+
* constant and is never re-derived from configuration or from the vault — the
|
|
36
|
+
* rule `src/vault-trace.cts` and `src/vault-audit.cts` state for the same
|
|
37
|
+
* reason: a verdict vocabulary a client repository could move is not one.
|
|
38
|
+
*
|
|
39
|
+
* It shares no token with the mechanical audit's `AUDIT_VERDICT`. `NO_CHECK` and
|
|
40
|
+
* `NOT_MATCHED` are facts about a machine-checkable rule; `N-A` and
|
|
41
|
+
* `INCONCLUSIVE` are facts about a judgement.
|
|
42
|
+
*
|
|
43
|
+
* A DELIBERATE DIVERGENCE from code review's practices findings ("likely
|
|
44
|
+
* violates, never certainty", capped at Warning): this review asserts a FLAT
|
|
45
|
+
* `FAIL`. It can, because it gates nothing — a FAIL here moves no score and
|
|
46
|
+
* blocks no ship. Uncertainty has its own honest home, `INCONCLUSIVE`.
|
|
47
|
+
*
|
|
48
|
+
* ## Arm A widens the judged boundary, and says so in every row
|
|
49
|
+
*
|
|
50
|
+
* `deriveNaAUniverse` (Arm A) reports on practices that were NEVER traced — the
|
|
51
|
+
* vault practice universe recorded at the pinned ref minus the ledger. That is
|
|
52
|
+
* wider than "traced practices with no usable `check:` block", and every Arm A
|
|
53
|
+
* row's reason names the universe as its source, so a reader meets the widening
|
|
54
|
+
* at the row that caused it. It is needed because `buildTraceStatement` fills
|
|
55
|
+
* the ledger from `matchPractices(...).matched ++ .global` only: every ledger
|
|
56
|
+
* entry already matched, so "never matched" can only come from the universe.
|
|
57
|
+
* Arm B re-derives applicability at review time from the phase's real diff.
|
|
58
|
+
*
|
|
59
|
+
* ## `check:`-presence is read LIVE, with an advisory — no trace-schema change
|
|
60
|
+
*
|
|
61
|
+
* The trace records no `check:`-presence field, so "traced ∩ NO_CHECK" needs the
|
|
62
|
+
* vault cache at review time. This module reads it through the shipped
|
|
63
|
+
* evaluator (`evaluatePractices` + `isNoCheckVerdict`); when the trace's pin and
|
|
64
|
+
* the cache's `resolvedSha` disagree it emits one advisory shaped like the trace
|
|
65
|
+
* gate's `PIN_DIVERGED` note — it reports, never blocks. A `checkPresent` field
|
|
66
|
+
* on the trace would be a schema change under `docs/vault-trace-format.md`'s
|
|
67
|
+
* freeze discipline for a consumer that does not need it, and this review is not
|
|
68
|
+
* a gate, so environment-dependence is acceptable here.
|
|
69
|
+
*
|
|
70
|
+
* ## The oracle is HARVESTED, never re-derived
|
|
71
|
+
*
|
|
72
|
+
* `deriveCandidateSet` learns `check:`-presence by running the shipped
|
|
73
|
+
* `evaluatePractices` and filtering with `isNoCheckVerdict`. It never scans for a
|
|
74
|
+
* `check:` key, because the audit tests `hasInvalidCheck` FIRST: an INVALID check
|
|
75
|
+
* is the mechanical audit's `FAIL`, not a `NO_CHECK`. The oracle runs over an
|
|
76
|
+
* EMPTY file set on purpose: `NO_CHECK` is decided before any file is consulted,
|
|
77
|
+
* so the answer is file- and machine-independent.
|
|
78
|
+
*
|
|
79
|
+
* ## Findings this module is built around
|
|
80
|
+
*
|
|
81
|
+
* - `matchPractices` returns `practicesProcessed: 0` for a never-fetched vault
|
|
82
|
+
* and an empty one alike, so Arm B consumes a `discoverPracticeFiles` result
|
|
83
|
+
* (whose `vaultPresent` and `readFailed` stay DISTINCT) plus the verdict
|
|
84
|
+
* owner's view of the cache (`computeVaultVerdict`), never the match count.
|
|
85
|
+
* - A parser-rejected practice is reported as a NAMED exclusion, and a degraded
|
|
86
|
+
* one as a note — never a silent narrowing.
|
|
87
|
+
* - The review range SHARES the audit's phase-scoped resolution first
|
|
88
|
+
* (`resolvePhaseRange`: the manifest's `startCommit`, its recorded commits,
|
|
89
|
+
* then SUMMARY trace digests). Only when that falls back does this module run
|
|
90
|
+
* its own `(NN-` commit-token scan over `base..HEAD`, and only then the whole
|
|
91
|
+
* base-branch span. `REVIEW_RANGE_SOURCE` mirrors the phase-scoped values
|
|
92
|
+
* rather than referencing `AUDIT_RANGE_SOURCE`, which belongs to the blocking
|
|
93
|
+
* gate.
|
|
94
|
+
* - An invalid `check:` is the mechanical audit's FAIL: it is excluded from the
|
|
95
|
+
* candidate set and reported as a named exclusion (the practices README states
|
|
96
|
+
* the audit blocks on it). Every judgement here binds to the audit's verdicts,
|
|
97
|
+
* never to prose.
|
|
98
|
+
*
|
|
99
|
+
* ## Nothing measured must never render as clean
|
|
100
|
+
*
|
|
101
|
+
* Every degenerate path returns a record that SAYS what did not happen —
|
|
102
|
+
* `oracleSource: 'unavailable'` with a note, a `notDerived` record per arm, and
|
|
103
|
+
* `reviewRefusesCleanRender`, the single predicate that forbids a clean review
|
|
104
|
+
* when no file was considered or an input was not measured.
|
|
105
|
+
*
|
|
106
|
+
* ## Trust boundary
|
|
107
|
+
*
|
|
108
|
+
* Practice ids, `applies-to` globs and practice prose are CLIENT-AUTHORED DATA.
|
|
109
|
+
* They are bounded with the shipped `boundedToken` before they reach any string
|
|
110
|
+
* this module returns, and applicability is decided by the shipped `globMatch`
|
|
111
|
+
* alone. No client-authored byte is ever compiled into a regular expression and
|
|
112
|
+
* no second glob grammar is written here (the declarative-check rule,
|
|
113
|
+
* docs/practices-format.md). Reason texts name practice ids and mechanisms,
|
|
114
|
+
* never absolute filesystem paths.
|
|
115
|
+
*/
|
|
116
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
117
|
+
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
118
|
+
const git_base_branch_cjs_1 = require("./git-base-branch.cjs");
|
|
119
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
120
|
+
const auditMod = require("./vault-audit.cjs");
|
|
121
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
122
|
+
const practicesMod = require("./vault-practices.cjs");
|
|
123
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
124
|
+
const traceMod = require("./vault-trace.cjs");
|
|
125
|
+
// The artifact validator's fence markers and its parse cap are HARVESTED from
|
|
126
|
+
// the shipped review checker that already exports them (plan 27-28's module),
|
|
127
|
+
// never re-spelled here. A second spelling of a fence marker is a second
|
|
128
|
+
// definition of where untrusted content ends, and this repository has already
|
|
129
|
+
// paid for a decision taken by matching text rather than by reading a value.
|
|
130
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
131
|
+
const reviewCheckMod = require("./practices-review-check.cjs");
|
|
132
|
+
const { AUDIT_VERDICT, AUDIT_FAIL_REASON, GIT_EMPTY_TREE } = auditMod;
|
|
133
|
+
const { boundedToken } = practicesMod;
|
|
134
|
+
const { TRACE_PROBLEM_KIND } = traceMod;
|
|
135
|
+
const { FENCE_START_MARKER, FENCE_END_MARKER, MAX_PARSED_UNITS } = reviewCheckMod;
|
|
136
|
+
// ─── The frozen vocabulary ───────────────────────────────────────────────────
|
|
137
|
+
/**
|
|
138
|
+
* D-06-10's four verdicts, verbatim, and LOCKED.
|
|
139
|
+
*
|
|
140
|
+
* `INCONCLUSIVE` is a distinct fourth verdict and never folds into `N-A`:
|
|
141
|
+
* "not applicable here" and "I could not determine this" are completely
|
|
142
|
+
* different facts, and collapsing them hides the verifier's blind spots inside
|
|
143
|
+
* a count that looks like coverage. That is the same split D-05-03 already made
|
|
144
|
+
* one tier down between `NO_CHECK` and `NOT_MATCHED`.
|
|
145
|
+
*/
|
|
146
|
+
const REVIEW_VERDICT = Object.freeze({
|
|
147
|
+
PASS: 'PASS',
|
|
148
|
+
FAIL: 'FAIL',
|
|
149
|
+
NA: 'N-A',
|
|
150
|
+
INCONCLUSIVE: 'INCONCLUSIVE',
|
|
151
|
+
});
|
|
152
|
+
/**
|
|
153
|
+
* The TWO derived-N-A arms, as two distinct machine labels (F-2, resolved by
|
|
154
|
+
* the operator 2026-09-03). They reach the rendered artifact as two distinct
|
|
155
|
+
* derived reasons and MUST NOT be blurred into one "derived" flag: one says the
|
|
156
|
+
* practice was never in this phase's conversation at all, the other says it was
|
|
157
|
+
* and no changed file fell under it. Those are different findings — the first
|
|
158
|
+
* about the vault's reach, the second about this phase's diff — and a reader who
|
|
159
|
+
* cannot tell them apart cannot act on either.
|
|
160
|
+
*/
|
|
161
|
+
const NA_DERIVED_REASON = Object.freeze({
|
|
162
|
+
/** Arm A — in the vault practice universe at the pinned ref, absent from the ledger. */
|
|
163
|
+
UNIVERSE_MINUS_LEDGER: 'universe-minus-ledger',
|
|
164
|
+
/** Arm B — a scoped practice whose `applies-to` globs matched no file in the review diff. */
|
|
165
|
+
NO_FILE_IN_DIFF: 'no-file-in-diff',
|
|
166
|
+
});
|
|
167
|
+
/**
|
|
168
|
+
* Machine labels for a `notDerived` ground. Values, not prose: `matchBudgetNotes`
|
|
169
|
+
* is this codebase's standing proof that a decision is never taken by matching
|
|
170
|
+
* text, and a consumer distinguishing "never fetched" from "unreadable" must not
|
|
171
|
+
* have to read a sentence to do it.
|
|
172
|
+
*/
|
|
173
|
+
const NOT_DERIVED_GROUND = Object.freeze({
|
|
174
|
+
UNIVERSE_ABSENT: 'universe-absent',
|
|
175
|
+
// 06-12 Task 5 follow-up. A universe the trace DID record, withheld because
|
|
176
|
+
// the trace is degraded, is a different finding from one the trace never
|
|
177
|
+
// recorded — and the caller is the only party that can tell them apart, so it
|
|
178
|
+
// says which. Kept out of NOT_MEASURED_GROUNDS for exactly the reason
|
|
179
|
+
// UNIVERSE_ABSENT is: every degraded-trace review takes the reconstruct arm,
|
|
180
|
+
// and refusing on it would refuse the primary path.
|
|
181
|
+
UNIVERSE_WITHHELD: 'universe-withheld-degraded',
|
|
182
|
+
VAULT_STATE_UNKNOWN: 'vault-state-unknown',
|
|
183
|
+
VAULT_NEVER_FETCHED: 'vault-never-fetched',
|
|
184
|
+
VAULT_UNREADABLE: 'vault-unreadable',
|
|
185
|
+
/**
|
|
186
|
+
* The cache the vault verdict owner reads records a fetch MISS, or holds no
|
|
187
|
+
* file: a vault directory may exist, but no client vault was loaded into it.
|
|
188
|
+
* Without this ground a miss rendered as an empty, clean review.
|
|
189
|
+
*/
|
|
190
|
+
VAULT_FETCH_MISS: 'vault-fetch-miss',
|
|
191
|
+
NO_FILES_CONSIDERED: 'no-files-considered',
|
|
192
|
+
MATCHER_UNAVAILABLE: 'matcher-unavailable',
|
|
193
|
+
});
|
|
194
|
+
/** The ground recorded when no NO_CHECK oracle reached the derivation at all. */
|
|
195
|
+
const ORACLE_UNAVAILABLE_GROUND = 'no-check-oracle-unavailable';
|
|
196
|
+
/**
|
|
197
|
+
* The grounds that mean the candidate derivation was NOT MEASURED (D3, plan
|
|
198
|
+
* 06-11) — the half of the distinction `reviewRefusesCleanRender` consumes.
|
|
199
|
+
*
|
|
200
|
+
* EMPTY BECAUSE NOTHING WAS MEASURED and EMPTY BECAUSE THE DERIVATION RAN AND
|
|
201
|
+
* NOTHING QUALIFIED are two different findings, and only one of them is a
|
|
202
|
+
* rubber stamp. Every ground below names an input the derivation NEEDED and did
|
|
203
|
+
* not get, so an empty result computed under one of them says nothing about the
|
|
204
|
+
* phase and must not render as a clean review. A ground absent from this set
|
|
205
|
+
* describes a measurement that happened, whose empty result IS a finding and
|
|
206
|
+
* renders with its stated N-A.
|
|
207
|
+
*
|
|
208
|
+
* `UNIVERSE_ABSENT` is deliberately NOT here, and the omission is the whole
|
|
209
|
+
* difference between fixing D3 and shouting it. That ground says the TRACE
|
|
210
|
+
* records no practice universe — which is true of every reconstruct review by
|
|
211
|
+
* definition, because a phase with no trace has no universe key. Refusing on it
|
|
212
|
+
* would refuse the entire reconstruct arm, which is the primary path (every
|
|
213
|
+
* phase of a project that never traced takes it), not a degraded one. A review that refuses
|
|
214
|
+
* everything judges exactly as much as one that judges nothing; it is only
|
|
215
|
+
* louder about it.
|
|
216
|
+
*/
|
|
217
|
+
const UNMEASURED_GROUND = new Set([
|
|
218
|
+
NOT_DERIVED_GROUND.VAULT_STATE_UNKNOWN,
|
|
219
|
+
NOT_DERIVED_GROUND.VAULT_NEVER_FETCHED,
|
|
220
|
+
NOT_DERIVED_GROUND.VAULT_UNREADABLE,
|
|
221
|
+
NOT_DERIVED_GROUND.VAULT_FETCH_MISS,
|
|
222
|
+
NOT_DERIVED_GROUND.NO_FILES_CONSIDERED,
|
|
223
|
+
NOT_DERIVED_GROUND.MATCHER_UNAVAILABLE,
|
|
224
|
+
ORACLE_UNAVAILABLE_GROUND,
|
|
225
|
+
]);
|
|
226
|
+
/**
|
|
227
|
+
* Where THIS tier's range came from — recorded beside the range itself.
|
|
228
|
+
*
|
|
229
|
+
* Deliberately NOT an extension of `AUDIT_RANGE_SOURCE` in
|
|
230
|
+
* `src/vault-audit.cts`: that vocabulary belongs to the BLOCKING tier, and one
|
|
231
|
+
* shared vocabulary would couple the two sides of the wall D-06-01 exists to
|
|
232
|
+
* keep apart — and would shift the producer-audit citations for no gain.
|
|
233
|
+
*
|
|
234
|
+
* * UAT-6-F1 added three members whose VALUES mirror the
|
|
235
|
+
* phase-scoped sources of the shared resolution. They are this object's own
|
|
236
|
+
* strings, reached from the audit's through `REVIEW_SOURCE_FOR_AUDIT_SOURCE`,
|
|
237
|
+
* never a reference to the audit's object.
|
|
238
|
+
*/
|
|
239
|
+
const REVIEW_RANGE_SOURCE = Object.freeze({
|
|
240
|
+
PHASE_START_COMMIT: 'phase-start-commit',
|
|
241
|
+
PHASE_FIRST_COMMIT: 'phase-first-commit',
|
|
242
|
+
TRACE_DIGEST: 'trace-digest',
|
|
243
|
+
PHASE_COMMIT_SCAN: 'phase-commit-scan',
|
|
244
|
+
BASE_BRANCH: 'base-branch',
|
|
245
|
+
});
|
|
246
|
+
/**
|
|
247
|
+
* The audit's phase-scoped range sources, keyed by the audit's OWN member
|
|
248
|
+
* values (read from the module, never re-spelled), mapped to this tier's.
|
|
249
|
+
* `base-branch` is deliberately UNMAPPED: a shared resolution that fell back
|
|
250
|
+
* hands over to this tier's own token scan rather than being adopted. Not
|
|
251
|
+
* exported — the export surface is a pinned contract.
|
|
252
|
+
*/
|
|
253
|
+
const REVIEW_SOURCE_FOR_AUDIT_SOURCE = Object.freeze({
|
|
254
|
+
[auditMod.AUDIT_RANGE_SOURCE.PHASE_START_COMMIT]: REVIEW_RANGE_SOURCE.PHASE_START_COMMIT,
|
|
255
|
+
[auditMod.AUDIT_RANGE_SOURCE.PHASE_FIRST_COMMIT]: REVIEW_RANGE_SOURCE.PHASE_FIRST_COMMIT,
|
|
256
|
+
[auditMod.AUDIT_RANGE_SOURCE.TRACE_DIGEST]: REVIEW_RANGE_SOURCE.TRACE_DIGEST,
|
|
257
|
+
});
|
|
258
|
+
/**
|
|
259
|
+
* How to get a phase whose range nobody could resolve back on a resolvable
|
|
260
|
+
* footing (08-05, D-08-17). Exported so the agent tier, a test and the artifact
|
|
261
|
+
* all quote THE SAME sentence: a remedy copied into a test rots the day the
|
|
262
|
+
* shipped one is reworded, and the pin that guards it stops guarding.
|
|
263
|
+
*
|
|
264
|
+
* Both remedies, deliberately. The first prevents the recurrence, the second
|
|
265
|
+
* rescues the phase that already happened — an operator handed only the first
|
|
266
|
+
* still has no way to review the phase in front of them.
|
|
267
|
+
*/
|
|
268
|
+
const REVIEW_RANGE_UNRESOLVABLE_REMEDY = 'Anchor the phase by running it through /pp-execute-phase (execute:pre records the start commit ' +
|
|
269
|
+
'via phase manifest start), or audit it by hand over a range you name: ' +
|
|
270
|
+
'pp-tools vault audit <phase-dir> --range <from>..<to>.';
|
|
271
|
+
/**
|
|
272
|
+
* The opening of the note `resolveReviewRange` pushes when the SHARED
|
|
273
|
+
* phase-scoped resolution did not answer. One constant, two writers and one
|
|
274
|
+
* reader: the `range_unresolvable` statement finds that note by this prefix to
|
|
275
|
+
* quote the ground it recorded, and a prefix spelled twice is a prefix that
|
|
276
|
+
* stops matching the day one copy is reworded.
|
|
277
|
+
*/
|
|
278
|
+
const SHARED_RESOLUTION_NOTE_PREFIX = 'the shared phase-scoped resolution';
|
|
279
|
+
/** The id charset a practice id must satisfy before it is echoed into a reason. */
|
|
280
|
+
const REVIEW_ID_SHAPE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
281
|
+
/** A phase token: `6`, `06` or `06.1`. Anything else never reaches `git log`. */
|
|
282
|
+
const PHASE_TOKEN_SHAPE = /^\d{1,3}(?:\.\d{1,3})?$/;
|
|
283
|
+
/** A resolved commit object name, as `git log --format=%H` emits it. */
|
|
284
|
+
const COMMIT_SHA_SHAPE = /^[0-9a-f]{7,64}$/;
|
|
285
|
+
// ─── Small shared helpers ────────────────────────────────────────────────────
|
|
286
|
+
function asStringList(v) {
|
|
287
|
+
if (!Array.isArray(v))
|
|
288
|
+
return [];
|
|
289
|
+
const out = [];
|
|
290
|
+
for (const entry of v) {
|
|
291
|
+
if (typeof entry === 'string' && entry !== '')
|
|
292
|
+
out.push(entry);
|
|
293
|
+
}
|
|
294
|
+
return out;
|
|
295
|
+
}
|
|
296
|
+
/** Order-stable dedup. The ledger's own order is the reported order. */
|
|
297
|
+
function uniqueInOrder(ids) {
|
|
298
|
+
const seen = new Set();
|
|
299
|
+
const out = [];
|
|
300
|
+
for (const id of ids) {
|
|
301
|
+
if (seen.has(id))
|
|
302
|
+
continue;
|
|
303
|
+
seen.add(id);
|
|
304
|
+
out.push(id);
|
|
305
|
+
}
|
|
306
|
+
return out;
|
|
307
|
+
}
|
|
308
|
+
/** Bound a client-authored value before it reaches any returned string. */
|
|
309
|
+
function safe(v) {
|
|
310
|
+
return boundedToken(v);
|
|
311
|
+
}
|
|
312
|
+
/** A practice id fit to quote back. Out-of-charset ids are bounded, never trusted. */
|
|
313
|
+
function quotableId(id) {
|
|
314
|
+
return REVIEW_ID_SHAPE.test(id) ? id : safe(id);
|
|
315
|
+
}
|
|
316
|
+
/** Is this token one of the two verdicts that carry a judgement (D-06-10)? */
|
|
317
|
+
function isJudgedVerdict(verdict) {
|
|
318
|
+
return verdict === REVIEW_VERDICT.PASS || verdict === REVIEW_VERDICT.FAIL;
|
|
319
|
+
}
|
|
320
|
+
/** Is this token one of the two NON-verdicts, which D-06-11 requires a reason of? */
|
|
321
|
+
function isNonVerdict(verdict) {
|
|
322
|
+
return verdict === REVIEW_VERDICT.NA || verdict === REVIEW_VERDICT.INCONCLUSIVE;
|
|
323
|
+
}
|
|
324
|
+
// ─── The input set (traced ∩ NO_CHECK) ───────────────────────────────────────
|
|
325
|
+
/**
|
|
326
|
+
* Derive the candidate set the agent tier may judge: the ids of THIS ARM's
|
|
327
|
+
* practice universe, intersected with the ids the SHIPPED evaluator calls
|
|
328
|
+
* `NO_CHECK`. Order-stable, deduplicated.
|
|
329
|
+
*
|
|
330
|
+
* ## The two universes (D3, plan 06-11)
|
|
331
|
+
*
|
|
332
|
+
* - TRACED (`reconstruct` absent or false) — the universe is the phase's trace
|
|
333
|
+
* ledger. Unchanged, byte for byte, from the shipped behaviour: a traced
|
|
334
|
+
* phase is judged against what it recorded.
|
|
335
|
+
* - RECONSTRUCT (`reconstruct: true`) — there is no ledger, so the universe is
|
|
336
|
+
* the VAULT'S OWN practice list, in the order discovery returned it. This is
|
|
337
|
+
* the half of D3 that made an untraced phase derive nothing at all: iterating
|
|
338
|
+
* an empty ledger produced an empty candidate set, an empty remainder and no
|
|
339
|
+
* fenced prose, no matter how large the diff or the vault, while the arm's
|
|
340
|
+
* own statement said the set had been rebuilt from both.
|
|
341
|
+
*
|
|
342
|
+
* APPLICABILITY IS NOT DECIDED HERE, and deliberately so. This function answers
|
|
343
|
+
* "which practices could the agent tier own at all" (the mechanical tier owns
|
|
344
|
+
* every check-bearing one); crossing that universe with the review's real diff
|
|
345
|
+
* is `deriveNaANoFile`'s job and reaches the SHIPPED `globMatch` through the
|
|
346
|
+
* same `_practices` seam. No second matcher is written for the reconstruct arm,
|
|
347
|
+
* because a second matcher is a second answer to one question (the
|
|
348
|
+
* declarative-check rule). The practices that fall outside the range therefore leave
|
|
349
|
+
* this set as STATED derived N-A rows rather than as silent absences.
|
|
350
|
+
*
|
|
351
|
+
* Three oracle paths, and the third one SAYS so:
|
|
352
|
+
* - `practices` supplied → `oracleSource: 'evaluated'`. The shipped
|
|
353
|
+
* `evaluatePractices` runs over an empty file set (the ladder answers
|
|
354
|
+
* `NO_CHECK` before consulting a file, so no diff is needed and none is
|
|
355
|
+
* used) and `isNoCheckVerdict` selects. Every excluded id carries the
|
|
356
|
+
* shipped verdict and fail-reason that excluded it.
|
|
357
|
+
* - `noCheckIds` supplied → `oracleSource: 'provided'`. The pure path: a
|
|
358
|
+
* caller that already holds the set.
|
|
359
|
+
* - neither → `oracleSource: 'unavailable'`, an EMPTY candidate list and a
|
|
360
|
+
* note stating that nothing was measured. An empty set with no note is
|
|
361
|
+
* indistinguishable from "nothing qualifies", which is the silence this
|
|
362
|
+
* workstream exists to end.
|
|
363
|
+
*/
|
|
364
|
+
function deriveCandidateSet(opts) {
|
|
365
|
+
const audit = opts._audit ?? auditMod;
|
|
366
|
+
const ledgerIds = uniqueInOrder(asStringList(opts.ledgerIds));
|
|
367
|
+
const reconstruct = opts.reconstruct === true;
|
|
368
|
+
const notes = [];
|
|
369
|
+
const exclusions = [];
|
|
370
|
+
// ── the oracle ──
|
|
371
|
+
let oracleSource = 'unavailable';
|
|
372
|
+
const noCheck = new Set();
|
|
373
|
+
const verdictById = new Map();
|
|
374
|
+
// Every id the oracle SAW, in the order it saw it. On the reconstruct arm this
|
|
375
|
+
// is the vault's practice list and becomes the universe; on the traced arm it
|
|
376
|
+
// is collected and unused, so the two arms share one traversal and cannot
|
|
377
|
+
// disagree about which ids the oracle covered.
|
|
378
|
+
const oracleIds = [];
|
|
379
|
+
if (Array.isArray(opts.practices)) {
|
|
380
|
+
oracleSource = 'evaluated';
|
|
381
|
+
const inputs = [];
|
|
382
|
+
for (const p of opts.practices) {
|
|
383
|
+
inputs.push({
|
|
384
|
+
id: typeof p?.id === 'string' ? p.id : '',
|
|
385
|
+
check: (p?.check ?? undefined),
|
|
386
|
+
checkInvalid: typeof p?.checkInvalid === 'string' ? p.checkInvalid : undefined,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
const evaluated = audit.evaluatePractices({
|
|
390
|
+
practices: inputs,
|
|
391
|
+
// EMPTY on purpose — see the module header. The ladder returns NO_CHECK
|
|
392
|
+
// before any file is consulted, so the oracle is both file-independent
|
|
393
|
+
// and machine-independent.
|
|
394
|
+
files: [],
|
|
395
|
+
});
|
|
396
|
+
for (const entry of evaluated.practices) {
|
|
397
|
+
verdictById.set(entry.id, { verdict: entry.verdict, reason: entry.reason, detail: entry.detail });
|
|
398
|
+
oracleIds.push(entry.id);
|
|
399
|
+
if (audit.isNoCheckVerdict(entry.verdict))
|
|
400
|
+
noCheck.add(entry.id);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
else if (opts.noCheckIds !== undefined && Array.isArray(opts.noCheckIds)) {
|
|
404
|
+
oracleSource = 'provided';
|
|
405
|
+
for (const id of asStringList(opts.noCheckIds)) {
|
|
406
|
+
noCheck.add(id);
|
|
407
|
+
oracleIds.push(id);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
notes.push('no NO_CHECK oracle was supplied: neither a live practice list nor a precomputed id set reached ' +
|
|
412
|
+
'this derivation, so the candidate set is EMPTY because nothing was measured — not because no ' +
|
|
413
|
+
'practice qualifies.');
|
|
414
|
+
}
|
|
415
|
+
// ── this arm's universe ∩ NO_CHECK, with every exclusion NAMED ──
|
|
416
|
+
//
|
|
417
|
+
// D3: the reconstruct arm's universe is the vault's practice list, because it
|
|
418
|
+
// has no ledger to intersect and iterating one produced an empty set that then
|
|
419
|
+
// rendered as a clean review. The traced arm is untouched.
|
|
420
|
+
const universeIds = reconstruct ? uniqueInOrder(oracleIds) : ledgerIds;
|
|
421
|
+
if (reconstruct) {
|
|
422
|
+
notes.push(
|
|
423
|
+
// NOT "no trace ledger exists": since 06-12 this arm is also taken by a
|
|
424
|
+
// phase whose trace parses but records a DEGRADED vault, and that phase
|
|
425
|
+
// does have a ledger — one this review declines to trust. Claiming it does
|
|
426
|
+
// not exist would be false on exactly the input G1 was about.
|
|
427
|
+
`no trusted trace ledger is available for this phase, so the candidate universe is the ${universeIds.length} practice ` +
|
|
428
|
+
"id(s) the vault's own practice list supplied. Which of them the phase's diff actually falls under is " +
|
|
429
|
+
'measured separately, by the shipped applicability matcher, and every one that does not is reported as ' +
|
|
430
|
+
'a derived N-A rather than dropped.');
|
|
431
|
+
}
|
|
432
|
+
const candidates = [];
|
|
433
|
+
for (const id of universeIds) {
|
|
434
|
+
if (noCheck.has(id)) {
|
|
435
|
+
candidates.push(id);
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
const known = verdictById.get(id);
|
|
439
|
+
if (known && known.reason === AUDIT_FAIL_REASON.CHECK_INVALID) {
|
|
440
|
+
exclusions.push({
|
|
441
|
+
id,
|
|
442
|
+
mechanicalVerdict: known.verdict,
|
|
443
|
+
reason: known.reason,
|
|
444
|
+
detail: `the \`check:\` block on "${quotableId(id)}" is not usable (${safe(known.detail)}). The shipped ` +
|
|
445
|
+
'ladder tests an invalid check FIRST and returns FAIL, so this practice is the mechanical ' +
|
|
446
|
+
"tier's and never reaches the agent tier.",
|
|
447
|
+
});
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
if (known) {
|
|
451
|
+
exclusions.push({
|
|
452
|
+
id,
|
|
453
|
+
mechanicalVerdict: known.verdict,
|
|
454
|
+
reason: known.reason,
|
|
455
|
+
detail: `"${quotableId(id)}" carries a usable \`check:\` block, so the shipped mechanical tier owns it ` +
|
|
456
|
+
'and the agent tier never judges it. The verdict shown was computed over an EMPTY file set — ' +
|
|
457
|
+
'the ladder decides NO_CHECK before any file is consulted, so no diff was needed and none was used.',
|
|
458
|
+
});
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
exclusions.push({
|
|
462
|
+
id,
|
|
463
|
+
mechanicalVerdict: null,
|
|
464
|
+
reason: null,
|
|
465
|
+
detail: `"${quotableId(id)}" is recorded in ${reconstruct ? "this vault's practice list" : "this phase's trace ledger"}` +
|
|
466
|
+
' but is not in the NO_CHECK set the oracle produced, so it is not a candidate for agent judgement.',
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
// ── WR-02: a parser-rejected practice never leaves the set in silence ──
|
|
470
|
+
for (const entry of opts.skipped ?? []) {
|
|
471
|
+
const where = typeof entry?.path === 'string' ? entry.path : '';
|
|
472
|
+
exclusions.push({
|
|
473
|
+
id: safe(node_path_1.default.basename(where) || where),
|
|
474
|
+
mechanicalVerdict: AUDIT_VERDICT.FAIL,
|
|
475
|
+
reason: AUDIT_FAIL_REASON.PRACTICE_UNLOADABLE,
|
|
476
|
+
detail: 'a practice file was REJECTED by the parser and therefore never entered the practice set: ' +
|
|
477
|
+
`${safe(entry?.warning)}. Its check-bearing-ness is unknown, so it can be neither a candidate ` +
|
|
478
|
+
'nor a silent absence.',
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
// ── degraded practices are RETAINED (their prose still applies) but named ──
|
|
482
|
+
for (const entry of opts.degraded ?? []) {
|
|
483
|
+
notes.push(`practice "${quotableId(typeof entry?.id === 'string' ? entry.id : '')}" was retained but DEGRADED: ` +
|
|
484
|
+
`${safe(asStringList(entry?.warnings).join('; '))}. Its prose still applies and it stays in the population.`);
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
candidates,
|
|
488
|
+
exclusions,
|
|
489
|
+
advisories: derivePinAdvisories(opts.tracePinSha, opts.cacheSha),
|
|
490
|
+
notes,
|
|
491
|
+
oracleSource,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* The pin advisory (recorded decision above; no trace-schema change).
|
|
496
|
+
*
|
|
497
|
+
* Mirrors the wording discipline of the trace gate's `PIN_DIVERGED` note
|
|
498
|
+
* (`cmdVaultTrace`) and harvests its KIND from
|
|
499
|
+
* `TRACE_PROBLEM_KIND` so the two cannot drift. It reports; it never blocks;
|
|
500
|
+
* it states that no reader action is required. An ABSENT cache emits NOTHING —
|
|
501
|
+
* an absent cache is not a divergence, and a check that fired on absence would
|
|
502
|
+
* penalise every machine that never fetched.
|
|
503
|
+
*/
|
|
504
|
+
function derivePinAdvisories(tracePinSha, cacheSha) {
|
|
505
|
+
const pinned = typeof tracePinSha === 'string' ? tracePinSha.trim() : '';
|
|
506
|
+
const local = typeof cacheSha === 'string' ? cacheSha.trim() : '';
|
|
507
|
+
if (pinned === '' || local === '' || pinned === local)
|
|
508
|
+
return [];
|
|
509
|
+
return [
|
|
510
|
+
{
|
|
511
|
+
kind: TRACE_PROBLEM_KIND.PIN_DIVERGED,
|
|
512
|
+
detail: `this phase's trace pins the vault at "${safe(pinned)}" while the local cache records ` +
|
|
513
|
+
`"${safe(local)}", and the \`check:\`-presence oracle above was read from the LOCAL cache. This ` +
|
|
514
|
+
'is a NOTE, not a defect: whether the vault has moved since the trace was written is a different ' +
|
|
515
|
+
'question from whether this review is valid, and this tier gates nothing. No action is required ' +
|
|
516
|
+
'of anyone reading this.',
|
|
517
|
+
},
|
|
518
|
+
];
|
|
519
|
+
}
|
|
520
|
+
// ─── Counts and the rate line ────────────────────────────────────────────────
|
|
521
|
+
/**
|
|
522
|
+
* Count the four verdicts, in the shape the audit's `countVerdicts`
|
|
523
|
+
* established one tier down.
|
|
524
|
+
*
|
|
525
|
+
* `judged` is `total - na`. Recorded because it is a choice: an `INCONCLUSIVE`
|
|
526
|
+
* practice WAS judged — the agent looked at it and could not determine an
|
|
527
|
+
* answer — while an `N-A` practice was not judged at all, either because the
|
|
528
|
+
* derivation removed it or because the agent found it inapplicable. So
|
|
529
|
+
* `INCONCLUSIVE` counts inside `judged` and is ALSO reported on its own, which
|
|
530
|
+
* is what makes a hollow review visible: a rate line reading `12 of 16 judged ·
|
|
531
|
+
* 4 N-A · 11 INCONCLUSIVE` says something a count of PASSes never could.
|
|
532
|
+
*
|
|
533
|
+
* `total` counts only rows with a RECOGNISED verdict, so `judged + na === total`
|
|
534
|
+
* holds unconditionally. Rows outside the vocabulary are counted as
|
|
535
|
+
* `unrecognised` and never absorbed into a real verdict — an unrecognised row
|
|
536
|
+
* silently folded into N-A would be an invented non-verdict.
|
|
537
|
+
*
|
|
538
|
+
* D-06-11 is enforced at the type level by `NonVerdictRow`. It is ALSO checked
|
|
539
|
+
* here, because rows arrive as parsed YAML from an artifact no compiler saw. A
|
|
540
|
+
* bare non-verdict is REPORTED in `malformed` and still counted: dropping it
|
|
541
|
+
* would shrink the denominator, which is the one number this tier must never
|
|
542
|
+
* quietly improve.
|
|
543
|
+
*/
|
|
544
|
+
function countReviewVerdicts(rows) {
|
|
545
|
+
const counts = {
|
|
546
|
+
pass: 0,
|
|
547
|
+
fail: 0,
|
|
548
|
+
na: 0,
|
|
549
|
+
inconclusive: 0,
|
|
550
|
+
judged: 0,
|
|
551
|
+
total: 0,
|
|
552
|
+
unrecognised: 0,
|
|
553
|
+
malformed: [],
|
|
554
|
+
};
|
|
555
|
+
const list = Array.isArray(rows) ? rows : [];
|
|
556
|
+
for (const raw of list) {
|
|
557
|
+
if (raw === null || typeof raw !== 'object') {
|
|
558
|
+
counts.unrecognised += 1;
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
const row = raw;
|
|
562
|
+
const verdict = typeof row.verdict === 'string' ? row.verdict : '';
|
|
563
|
+
const id = typeof row.id === 'string' ? row.id : '';
|
|
564
|
+
if (!isJudgedVerdict(verdict) && !isNonVerdict(verdict)) {
|
|
565
|
+
counts.unrecognised += 1;
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
if (verdict === REVIEW_VERDICT.PASS)
|
|
569
|
+
counts.pass += 1;
|
|
570
|
+
else if (verdict === REVIEW_VERDICT.FAIL)
|
|
571
|
+
counts.fail += 1;
|
|
572
|
+
else if (verdict === REVIEW_VERDICT.NA)
|
|
573
|
+
counts.na += 1;
|
|
574
|
+
else
|
|
575
|
+
counts.inconclusive += 1;
|
|
576
|
+
counts.total += 1;
|
|
577
|
+
if (isNonVerdict(verdict)) {
|
|
578
|
+
const reason = typeof row.reason === 'string' ? row.reason.trim() : '';
|
|
579
|
+
if (reason === '') {
|
|
580
|
+
counts.malformed.push({
|
|
581
|
+
id: quotableId(id),
|
|
582
|
+
verdict,
|
|
583
|
+
problem: 'A non-verdict must carry a stated reason. A bare N-A or INCONCLUSIVE is ' +
|
|
584
|
+
'indistinguishable from the agent giving up.',
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
counts.judged = counts.total - counts.na;
|
|
590
|
+
return counts;
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* D-06-12's non-verdict rate line, computed from the four verdict counts and
|
|
594
|
+
* NOTHING else — a supplied `judged` or `total` is deliberately ignored, so a
|
|
595
|
+
* caller cannot narrate a denominator the rows do not support.
|
|
596
|
+
*
|
|
597
|
+
* D-06-13 constrains the shape: counts only. No ratio, no percentage, no letter
|
|
598
|
+
* grade, no single score. A single number invites being quoted as "87 %
|
|
599
|
+
* compliant" — a measured-sounding claim about an LLM judgement, which is
|
|
600
|
+
* precisely the over-claim this phase exists to prevent.
|
|
601
|
+
*/
|
|
602
|
+
function renderRateLine(counts) {
|
|
603
|
+
const n = (v) => (typeof v === 'number' && Number.isFinite(v) && v > 0 ? Math.floor(v) : 0);
|
|
604
|
+
const pass = n(counts?.pass);
|
|
605
|
+
const fail = n(counts?.fail);
|
|
606
|
+
const na = n(counts?.na);
|
|
607
|
+
const inconclusive = n(counts?.inconclusive);
|
|
608
|
+
const total = pass + fail + na + inconclusive;
|
|
609
|
+
const judged = total - na;
|
|
610
|
+
return `${judged} of ${total} practices judged · ${na} N-A · ${inconclusive} INCONCLUSIVE`;
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* The MECHANICAL tier's coverage over THIS review's own resolved file set
|
|
614
|
+
* (MECH-02, D-10-03), or `null` when there was nothing honest to compute it
|
|
615
|
+
* from.
|
|
616
|
+
*
|
|
617
|
+
* HARVESTED, NEVER REWRITTEN. Three shipped pieces and no fourth: the shipped
|
|
618
|
+
* `evaluatePractices` through the same audit seam `deriveCandidateSet` reaches,
|
|
619
|
+
* then the shipped `countVerdicts`, then the ONE renderer `src/vault-audit.cts`
|
|
620
|
+
* exports. `src/check-command-router.cts` states the rule for its own
|
|
621
|
+
* pass-through and it holds identically here — a second computation is a second
|
|
622
|
+
* place the report can disagree with the gate that blocks on it.
|
|
623
|
+
*
|
|
624
|
+
* The counter and the renderer are deliberately NOT taken off the seam. The
|
|
625
|
+
* seam exists so a test can substitute the EVALUATOR; letting a caller
|
|
626
|
+
* substitute the arithmetic or the wording would hand back exactly the
|
|
627
|
+
* narrated denominator `renderRateLine` refuses to accept.
|
|
628
|
+
*
|
|
629
|
+
* The reader is the shipped BOUNDED one, rooted at the review's own `cwd`, so a
|
|
630
|
+
* `require:`/`forbid:` assertion is measured rather than collapsed into
|
|
631
|
+
* `file_unreadable` — and so the containment properties that reader's contract
|
|
632
|
+
* proves are the ones in force here too. No reader, no read: a run with no cwd
|
|
633
|
+
* evaluates with the fail-closed default rather than inventing one.
|
|
634
|
+
*/
|
|
635
|
+
function deriveMechanicalCoverageLine(opts) {
|
|
636
|
+
const audit = opts?._audit ?? auditMod;
|
|
637
|
+
if (typeof audit?.evaluatePractices !== 'function')
|
|
638
|
+
return null;
|
|
639
|
+
const inputs = [];
|
|
640
|
+
for (const p of opts?.practices ?? []) {
|
|
641
|
+
if (!p || typeof p.id !== 'string' || p.id === '')
|
|
642
|
+
continue;
|
|
643
|
+
inputs.push({
|
|
644
|
+
id: p.id,
|
|
645
|
+
check: (p.check ?? undefined),
|
|
646
|
+
checkInvalid: typeof p.checkInvalid === 'string' ? p.checkInvalid : undefined,
|
|
647
|
+
// MECH-01 (D-10-12): the practice's own applies-to bounds the comparison
|
|
648
|
+
// here for the same reason it does in the gate. An ABSENT list is the
|
|
649
|
+
// same case as an EMPTY one — applies everywhere — which is what the
|
|
650
|
+
// evaluator's own branch says, so nothing is re-decided here.
|
|
651
|
+
appliesTo: [...asStringList(p.appliesTo)],
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
const root = typeof opts?.cwd === 'string' && opts.cwd !== '' ? opts.cwd : null;
|
|
655
|
+
const readFile = root === null ? null : auditMod.createBoundedFileReader(root);
|
|
656
|
+
const evaluated = audit.evaluatePractices({
|
|
657
|
+
practices: inputs,
|
|
658
|
+
files: [...asStringList(opts?.files)],
|
|
659
|
+
...(readFile === null ? {} : { readFile }),
|
|
660
|
+
});
|
|
661
|
+
return auditMod.renderMechanicalCoverageLine(auditMod.countVerdicts(evaluated.practices));
|
|
662
|
+
}
|
|
663
|
+
// ─── Arm A — the vault universe minus the trace ledger ───────────────────────
|
|
664
|
+
/**
|
|
665
|
+
* Arm A: every practice in the vault universe recorded at the pinned ref that
|
|
666
|
+
* the trace ledger never mentions. Those practices were never matched at plan
|
|
667
|
+
* time, so no code in this phase was written against them — a DERIVED N-A, not
|
|
668
|
+
* a judgement (D-06-09).
|
|
669
|
+
*
|
|
670
|
+
* BOUNDARY WIDENING, surfaced in every row (and in the module header): these
|
|
671
|
+
* ids were never traced, so reporting them widens the judged boundary beyond
|
|
672
|
+
* the traced input set (see the module header). The reason
|
|
673
|
+
* text names the vault universe as its source so a reader of the ARTIFACT meets
|
|
674
|
+
* the widening at the row that caused it, never only in a plan nobody reopens.
|
|
675
|
+
*
|
|
676
|
+
* ABSENT and EMPTY are different facts and this function keeps them apart —
|
|
677
|
+
* `docs/vault-trace-format.md` says so explicitly. `practiceUniverse` is
|
|
678
|
+
* OMITTED (or null) whenever the pin is null, because a degraded run has no
|
|
679
|
+
* universe to record: that derives NOTHING and returns a loud not-derived
|
|
680
|
+
* record. A literal empty array means the manifest was read and held no
|
|
681
|
+
* practices: that derives zero rows with NO gap record, because nothing is
|
|
682
|
+
* missing.
|
|
683
|
+
*/
|
|
684
|
+
function deriveNaAUniverse(opts) {
|
|
685
|
+
const notes = [];
|
|
686
|
+
const universe = opts?.universe;
|
|
687
|
+
if (universe === undefined || universe === null || !Array.isArray(universe)) {
|
|
688
|
+
return {
|
|
689
|
+
derived: false,
|
|
690
|
+
rows: [],
|
|
691
|
+
notDerived: {
|
|
692
|
+
arm: NA_DERIVED_REASON.UNIVERSE_MINUS_LEDGER,
|
|
693
|
+
...(opts?.universeWithheld === true
|
|
694
|
+
? {
|
|
695
|
+
ground: NOT_DERIVED_GROUND.UNIVERSE_WITHHELD,
|
|
696
|
+
statement: 'Arm A derived NOTHING: this trace DOES record a practice universe, and it was ' +
|
|
697
|
+
'deliberately withheld because the trace is DEGRADED — it records that the vault could ' +
|
|
698
|
+
'not be loaded when the work was planned, so its universe proves nothing about which ' +
|
|
699
|
+
'practices governed that work and may not be subtracted from. This is a ' +
|
|
700
|
+
// Same wording constraint as the sibling branch below: the
|
|
701
|
+
// prompt-injection scanner's regex carries no leading word
|
|
702
|
+
// boundary, so "is a different fact from …" is written in place
|
|
703
|
+
// of the natural phrasing.
|
|
704
|
+
'WITHHELD universe, which is a different fact from a trace that records no universe at ' +
|
|
705
|
+
'all, and a different fact from a vault that was read and held no practices — no ' +
|
|
706
|
+
'derived N-A may be reported from it.',
|
|
707
|
+
}
|
|
708
|
+
: {
|
|
709
|
+
ground: NOT_DERIVED_GROUND.UNIVERSE_ABSENT,
|
|
710
|
+
statement: 'Arm A derived NOTHING: this trace records no practice universe at all. The universe key is ' +
|
|
711
|
+
'omitted whenever the vault pin is null, so a degraded or never-pinned run has no universe to ' +
|
|
712
|
+
// WORDING CONSTRAINT, recorded so a future edit does not undo it:
|
|
713
|
+
// `tests/prompt-injection-scan.security.test.cjs` scans this module's
|
|
714
|
+
// emitted `.cjs` for a role-manipulation pattern whose regex carries
|
|
715
|
+
// no leading word boundary, so the last three letters of "fact"
|
|
716
|
+
// followed by " as a <noun>" match it. Every sentence here therefore
|
|
717
|
+
// says "is a different fact from …" rather than the natural phrasing.
|
|
718
|
+
'subtract the ledger from. This is an ABSENT universe, which is a different fact from a vault ' +
|
|
719
|
+
'that was read and held no practices — no derived N-A may be reported from it.',
|
|
720
|
+
}),
|
|
721
|
+
},
|
|
722
|
+
notes,
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
const ledger = new Set(asStringList(opts.ledgerIds));
|
|
726
|
+
const rows = [];
|
|
727
|
+
for (const id of uniqueInOrder(asStringList(universe))) {
|
|
728
|
+
if (ledger.has(id))
|
|
729
|
+
continue;
|
|
730
|
+
rows.push({
|
|
731
|
+
id,
|
|
732
|
+
verdict: REVIEW_VERDICT.NA,
|
|
733
|
+
naDerivedReason: NA_DERIVED_REASON.UNIVERSE_MINUS_LEDGER,
|
|
734
|
+
reason: `derived, not judged: "${quotableId(id)}" is in the VAULT PRACTICE UNIVERSE recorded at the ` +
|
|
735
|
+
"pinned ref and absent from this phase's trace ledger, so it was never matched to any file at " +
|
|
736
|
+
'plan time and no code here was written against it. Reported under a judged boundary WIDENED ' +
|
|
737
|
+
'beyond the traced set: the trace ledger is not the whole vault, and this practice was never traced.',
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
if (rows.length === 0) {
|
|
741
|
+
notes.push(`Arm A derived zero rows: the trace ledger accounts for every one of the ${universe.length} ` +
|
|
742
|
+
'practice id(s) in the recorded universe. The universe was READ — this is an empty result, not a gap.');
|
|
743
|
+
}
|
|
744
|
+
return { derived: true, rows, notDerived: null, notes };
|
|
745
|
+
}
|
|
746
|
+
// ─── Arm B — review-time applicability against the real diff ────────────────
|
|
747
|
+
/**
|
|
748
|
+
* Arm B: a candidate whose `applies-to` globs match no file in the review
|
|
749
|
+
* range. Re-derived at review time against the phase's REAL diff (D-06-15's
|
|
750
|
+
* wording), never against a plan's `files_modified` — a plan states intent and
|
|
751
|
+
* this tier reports what happened. The artifact states which range produced the
|
|
752
|
+
* file list, and when that range is refused or empty this arm is SKIPPED with a
|
|
753
|
+
* loud not-derived record rather than fabricating N-A from an unmeasured diff:
|
|
754
|
+
* "no file matched" and "no file was looked at" are different facts, and only
|
|
755
|
+
* the first is a finding.
|
|
756
|
+
*
|
|
757
|
+
* The vault-state guard is CR-01's workaround and the reason this function
|
|
758
|
+
* takes a `discoverPracticeFiles` result rather than calling `matchPractices`:
|
|
759
|
+
* `matchPractices` returns `practicesProcessed: 0` for a never-fetched vault
|
|
760
|
+
* and for an empty one alike, so it cannot be the vault-state oracle. Discovery
|
|
761
|
+
* keeps `vaultPresent` and `readFailed` distinct, and the two degraded states
|
|
762
|
+
* get two DIFFERENT loud statements and two different grounds — never fetched
|
|
763
|
+
* and unreadable are different repairs, and collapsing them would tell an
|
|
764
|
+
* operator to do the wrong one.
|
|
765
|
+
*
|
|
766
|
+
* A GLOBAL-scope practice never receives an Arm B N-A, by construction: a
|
|
767
|
+
* global practice applies to the whole change set by definition, so "no file
|
|
768
|
+
* fell under it" is not a statement that can be true of one.
|
|
769
|
+
*
|
|
770
|
+
* Applicability is decided by the SHIPPED `globMatch` alone (the declarative-check
|
|
771
|
+
* rule: no second grammar, no client-authored byte compiled into a RegExp).
|
|
772
|
+
*/
|
|
773
|
+
function deriveNaANoFile(opts) {
|
|
774
|
+
const arm = NA_DERIVED_REASON.NO_FILE_IN_DIFF;
|
|
775
|
+
const notes = [];
|
|
776
|
+
const refuse = (ground, statement) => ({
|
|
777
|
+
derived: false,
|
|
778
|
+
rows: [],
|
|
779
|
+
notDerived: { arm, ground, statement },
|
|
780
|
+
notes,
|
|
781
|
+
});
|
|
782
|
+
const discovery = opts?.discovery;
|
|
783
|
+
if (discovery === undefined || discovery === null || typeof discovery !== 'object') {
|
|
784
|
+
return refuse(NOT_DERIVED_GROUND.VAULT_STATE_UNKNOWN, 'Arm B derived NOTHING: no practice-discovery result was supplied, so whether the client vault is ' +
|
|
785
|
+
'present and readable is UNKNOWN. Applicability is not derived from an unknown vault state, ' +
|
|
786
|
+
'because a practice set that may be empty only because nothing was read cannot produce an ' +
|
|
787
|
+
'honest N-A.');
|
|
788
|
+
}
|
|
789
|
+
if (discovery.vaultPresent === false) {
|
|
790
|
+
return refuse(NOT_DERIVED_GROUND.VAULT_NEVER_FETCHED, 'Arm B derived NOTHING: no fetched client vault cache exists on this machine — the vault was ' +
|
|
791
|
+
'NEVER FETCHED. This is not an empty vault, and no applicability was derived from it. Repair by ' +
|
|
792
|
+
'fetching the vault, then re-run the review.');
|
|
793
|
+
}
|
|
794
|
+
if (discovery.fetchMiss === true) {
|
|
795
|
+
return refuse(NOT_DERIVED_GROUND.VAULT_FETCH_MISS, 'Arm B derived NOTHING: the vault cache records a FETCH MISS (or holds no file), so no client vault ' +
|
|
796
|
+
'was loaded and no applicability was derived from it. An empty practice set read from a miss is ' +
|
|
797
|
+
'not an empty vault. Repair by fetching the vault, then re-run the review.');
|
|
798
|
+
}
|
|
799
|
+
if (discovery.readFailed === true) {
|
|
800
|
+
return refuse(NOT_DERIVED_GROUND.VAULT_UNREADABLE, 'Arm B derived NOTHING: the client vault is PRESENT but a read of it REFUSED — the vault is ' +
|
|
801
|
+
'unreadable. This is not an empty vault and not a missing one, and no applicability was derived ' +
|
|
802
|
+
'from it. Repair by making the cached vault readable, then re-run the review.');
|
|
803
|
+
}
|
|
804
|
+
const diffFiles = asStringList(opts.diffFiles).map((f) => f.replace(/\\/g, '/'));
|
|
805
|
+
if (diffFiles.length === 0) {
|
|
806
|
+
return refuse(NOT_DERIVED_GROUND.NO_FILES_CONSIDERED, 'Arm B derived NOTHING: the review range yielded zero files, so no applicability was measured. ' +
|
|
807
|
+
'"No file matched this practice" and "no file was looked at" are different facts and only the ' +
|
|
808
|
+
'first is a finding — an N-A fabricated from an unmeasured diff would be the second wearing the ' +
|
|
809
|
+
"first's label.");
|
|
810
|
+
}
|
|
811
|
+
const matcher = opts._practices ?? practicesMod;
|
|
812
|
+
if (typeof matcher.globMatch !== 'function') {
|
|
813
|
+
return refuse(NOT_DERIVED_GROUND.MATCHER_UNAVAILABLE, 'Arm B derived NOTHING: the shipped glob matcher could not be reached, and no second matcher is ' +
|
|
814
|
+
'written here (the declarative-check rule). No applicability was measured.');
|
|
815
|
+
}
|
|
816
|
+
const byId = new Map();
|
|
817
|
+
for (const p of opts.practices ?? []) {
|
|
818
|
+
if (p && typeof p.id === 'string')
|
|
819
|
+
byId.set(p.id, p);
|
|
820
|
+
}
|
|
821
|
+
const rows = [];
|
|
822
|
+
for (const id of uniqueInOrder(asStringList(opts.candidates))) {
|
|
823
|
+
const practice = byId.get(id);
|
|
824
|
+
if (!practice) {
|
|
825
|
+
notes.push(`Arm B skipped "${quotableId(id)}": it is a candidate but no practice record carrying its ` +
|
|
826
|
+
'`applies-to` globs was supplied, so its applicability was NOT measured and no N-A is claimed.');
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
// A global practice applies to everything; "no file fell under it" cannot
|
|
830
|
+
// be true of one, so it is excluded from this arm by construction.
|
|
831
|
+
if (practice.scope !== 'scoped')
|
|
832
|
+
continue;
|
|
833
|
+
const globs = asStringList(practice.appliesTo);
|
|
834
|
+
if (globs.length === 0) {
|
|
835
|
+
notes.push(`Arm B skipped "${quotableId(id)}": it is scoped but carries no \`applies-to\` glob, so there is ` +
|
|
836
|
+
'nothing to measure applicability against and no N-A is claimed.');
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
let matchedAny = false;
|
|
840
|
+
for (const file of diffFiles) {
|
|
841
|
+
for (const glob of globs) {
|
|
842
|
+
if (matcher.globMatch(file, glob)) {
|
|
843
|
+
matchedAny = true;
|
|
844
|
+
break;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
if (matchedAny)
|
|
848
|
+
break;
|
|
849
|
+
}
|
|
850
|
+
if (matchedAny)
|
|
851
|
+
continue;
|
|
852
|
+
rows.push({
|
|
853
|
+
id,
|
|
854
|
+
verdict: REVIEW_VERDICT.NA,
|
|
855
|
+
naDerivedReason: NA_DERIVED_REASON.NO_FILE_IN_DIFF,
|
|
856
|
+
reason: `derived, not judged: "${quotableId(id)}" is a scoped practice and none of its \`applies-to\` ` +
|
|
857
|
+
`glob(s) matched any of the ${diffFiles.length} file(s) in this review's range, so no changed ` +
|
|
858
|
+
'file in this phase falls under it.',
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
return { derived: true, rows, notDerived: null, notes };
|
|
862
|
+
}
|
|
863
|
+
// ─── The partition that feeds the agent ─────────────────────────────────────
|
|
864
|
+
/**
|
|
865
|
+
* Split the reported population into the two derived-N-A sets and the
|
|
866
|
+
* REMAINDER — the only ids that reach the agent (D-06-09).
|
|
867
|
+
*
|
|
868
|
+
* The three sets are DISJOINT and their union is exactly `population`. Arm A's
|
|
869
|
+
* ids come from the universe minus the ledger and Arm B's from the candidates
|
|
870
|
+
* (a subset of the ledger), so they are structurally disjoint already; the
|
|
871
|
+
* ordering below makes it true even when a caller hands in overlapping rows,
|
|
872
|
+
* with Arm A winning, because "never traced at all" is the stronger statement
|
|
873
|
+
* and the weaker one would say less about the same id.
|
|
874
|
+
*/
|
|
875
|
+
function partitionForJudgement(opts) {
|
|
876
|
+
const rowsOf = (v) => {
|
|
877
|
+
if (v === undefined || v === null)
|
|
878
|
+
return [];
|
|
879
|
+
if (Array.isArray(v))
|
|
880
|
+
return [...v];
|
|
881
|
+
const arm = v;
|
|
882
|
+
return Array.isArray(arm.rows) ? [...arm.rows] : [];
|
|
883
|
+
};
|
|
884
|
+
const armA = [];
|
|
885
|
+
const armAIds = new Set();
|
|
886
|
+
for (const row of rowsOf(opts?.naUniverse)) {
|
|
887
|
+
if (typeof row?.id !== 'string' || armAIds.has(row.id))
|
|
888
|
+
continue;
|
|
889
|
+
armAIds.add(row.id);
|
|
890
|
+
armA.push(row);
|
|
891
|
+
}
|
|
892
|
+
const armB = [];
|
|
893
|
+
const armBIds = new Set();
|
|
894
|
+
for (const row of rowsOf(opts?.naNoFile)) {
|
|
895
|
+
if (typeof row?.id !== 'string' || armAIds.has(row.id) || armBIds.has(row.id))
|
|
896
|
+
continue;
|
|
897
|
+
armBIds.add(row.id);
|
|
898
|
+
armB.push(row);
|
|
899
|
+
}
|
|
900
|
+
const candidates = uniqueInOrder(asStringList(opts?.candidates));
|
|
901
|
+
const population = uniqueInOrder([...armA.map((r) => r.id), ...candidates]);
|
|
902
|
+
const remainder = population.filter((id) => !armAIds.has(id) && !armBIds.has(id));
|
|
903
|
+
return { naUniverse: armA, naNoFile: armB, remainder, population };
|
|
904
|
+
}
|
|
905
|
+
// ─── The review range, and the refusal that guards it ───────────────────────
|
|
906
|
+
/** Normalise a phase token to the zero-padded form the commit convention uses. */
|
|
907
|
+
function normalisePhaseToken(v) {
|
|
908
|
+
const raw = typeof v === 'number' && Number.isFinite(v) ? String(v) : typeof v === 'string' ? v.trim() : '';
|
|
909
|
+
if (!PHASE_TOKEN_SHAPE.test(raw))
|
|
910
|
+
return null;
|
|
911
|
+
const parts = raw.split('.');
|
|
912
|
+
const padded = parts[0].padStart(2, '0');
|
|
913
|
+
return parts.length === 1 ? padded : `${padded}.${parts[1]}`;
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Resolve the range this review is computed over, and RECORD where it came from
|
|
917
|
+
* (F-3 / WR-04).
|
|
918
|
+
*
|
|
919
|
+
* First — the SHARED phase-scoped resolution (UAT-6-F1). When a
|
|
920
|
+
* `phaseDir` is given, the mechanical tier's `resolvePhaseRange` is asked
|
|
921
|
+
* before anything else, and its ladder is the one this tier adopts:
|
|
922
|
+
* `phase-start-commit` (the manifest anchor written at execute:pre),
|
|
923
|
+
* `phase-first-commit` (the commits recorded at verify:post), then
|
|
924
|
+
* `trace-digest` (SUMMARY `gitCommit` digests, for a phase built before that
|
|
925
|
+
* writer). Its notes travel with the adopted range. When it answers
|
|
926
|
+
* `base-branch` — its fourth rung — the fallback is NOT adopted: one note names
|
|
927
|
+
* its ground and this tier continues into its own scan. The vocabulary stays
|
|
928
|
+
* separate for the reason `REVIEW_RANGE_SOURCE` states.
|
|
929
|
+
*
|
|
930
|
+
* Then — `phase-commit-scan`: the EARLIEST commit on `base..HEAD` whose
|
|
931
|
+
* message carries this repository's `type(NN-MM):` convention token for the
|
|
932
|
+
* phase, anchored on that commit's parent. This is WR-04's own suggested fix,
|
|
933
|
+
* and it covers a phase branch that never recorded a manifest. The scan stays
|
|
934
|
+
* over `base..HEAD` and is never widened to whole history: this repository
|
|
935
|
+
* carries two workstreams whose `(05-` tokens would both match. A review that
|
|
936
|
+
* attributes another phase's file to this one is the misleading verdict this
|
|
937
|
+
* tier exists to avoid.
|
|
938
|
+
*
|
|
939
|
+
* When the phase's first commit is the repository root there is no parent to
|
|
940
|
+
* anchor on, so the anchor becomes git's empty-tree object — HARVESTED from the
|
|
941
|
+
* mechanical tier's `GIT_EMPTY_TREE` rather than spelled again here. That is
|
|
942
|
+
* not a fallback: a phase-scoped range was resolved, so the source stays
|
|
943
|
+
* `phase-commit-scan`.
|
|
944
|
+
*
|
|
945
|
+
* Fallback — `base-branch`: `resolveBaseBranchResult(cwd)...HEAD`. The 5-tier
|
|
946
|
+
* ladder is never re-derived (#1146 records what the naive `origin/HEAD` +
|
|
947
|
+
* `:-main` form cost) and every git call goes through the timeout-bounded
|
|
948
|
+
* `execGit` seam, never `execFileSync`. The RESULT form is used rather than the
|
|
949
|
+
* bare string so a REFUSED `git.base_branch` becomes a stated note instead of a
|
|
950
|
+
* silent change of range (T-6-49).
|
|
951
|
+
*
|
|
952
|
+
* TWO deliberate details in the scan:
|
|
953
|
+
* - `-F` (fixed-string). A commit subject is repository data; reading the
|
|
954
|
+
* needle as a pattern would let the convention token's own parenthesis be
|
|
955
|
+
* a group, and a phase token that failed `PHASE_TOKEN_SHAPE` never reaches
|
|
956
|
+
* git at all.
|
|
957
|
+
* - `base..HEAD`, not `base...HEAD`. For `git log` the three-dot form is the
|
|
958
|
+
* SYMMETRIC difference and would admit commits that live only on the base,
|
|
959
|
+
* which are by definition not this phase's.
|
|
960
|
+
*
|
|
961
|
+
* Deletions are excluded from the file list (`--diff-filter=d`), the same
|
|
962
|
+
* scoping the mechanical tier states its own reasons for: a deleted violation
|
|
963
|
+
* is the fix, and a deleted file has no content left to satisfy anything.
|
|
964
|
+
*
|
|
965
|
+
* Every degenerate path pushes a NOTE. A range that resolved nothing and a diff
|
|
966
|
+
* that could not be read must never look like a phase that changed nothing.
|
|
967
|
+
*/
|
|
968
|
+
function resolveReviewRange(opts) {
|
|
969
|
+
const exec = typeof opts?.exec === 'function' ? opts.exec : shell_command_projection_cjs_1.execGit;
|
|
970
|
+
const cwd = typeof opts?.cwd === 'string' && opts.cwd !== '' ? opts.cwd : process.cwd();
|
|
971
|
+
const notes = [];
|
|
972
|
+
let base = 'main';
|
|
973
|
+
try {
|
|
974
|
+
// The RESULT form, not the bare string: `git.base_branch` is the one value
|
|
975
|
+
// this function puts on a git command line that comes from a file a cloned
|
|
976
|
+
// repository can carry, and the ladder now refuses a shape git would read as
|
|
977
|
+
// an option rather than a revision (T-6-49). A refusal that fell back to
|
|
978
|
+
// "main" in silence would be exactly the failure mode this whole module
|
|
979
|
+
// exists to end, so it is stated here — this function already owns the
|
|
980
|
+
// channel, and every other degenerate path below already uses it.
|
|
981
|
+
const resolved = (0, git_base_branch_cjs_1.resolveBaseBranchResult)(cwd, { execGit: exec });
|
|
982
|
+
base = resolved.branch;
|
|
983
|
+
if (resolved.configRejected !== null) {
|
|
984
|
+
notes.push(`${resolved.configRejected.reason} The range below is computed over "${safe(base)}" instead.`);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
catch {
|
|
988
|
+
base = 'main';
|
|
989
|
+
notes.push('the base-branch ladder threw, so the range fell back to the literal default "main".');
|
|
990
|
+
}
|
|
991
|
+
let range = `${base}...HEAD`;
|
|
992
|
+
let rangeSource = REVIEW_RANGE_SOURCE.BASE_BRANCH;
|
|
993
|
+
// The shared phase-scoped resolution, ahead of the token scan (UAT-6-F1).
|
|
994
|
+
let sharedResolved = false;
|
|
995
|
+
if (typeof opts?.phaseDir === 'string' && opts.phaseDir !== '') {
|
|
996
|
+
try {
|
|
997
|
+
const shared = auditMod.resolvePhaseRange({ root: cwd, phaseDir: node_path_1.default.resolve(cwd, opts.phaseDir), exec });
|
|
998
|
+
const mapped = Object.prototype.hasOwnProperty.call(REVIEW_SOURCE_FOR_AUDIT_SOURCE, shared.rangeSource)
|
|
999
|
+
? REVIEW_SOURCE_FOR_AUDIT_SOURCE[shared.rangeSource]
|
|
1000
|
+
: undefined;
|
|
1001
|
+
if (mapped !== undefined) {
|
|
1002
|
+
range = shared.range;
|
|
1003
|
+
rangeSource = mapped;
|
|
1004
|
+
for (const note of shared.notes ?? [])
|
|
1005
|
+
notes.push(note);
|
|
1006
|
+
sharedResolved = true;
|
|
1007
|
+
}
|
|
1008
|
+
else {
|
|
1009
|
+
notes.push(`${SHARED_RESOLUTION_NOTE_PREFIX} fell back (${safe(shared.fallbackReason ?? 'no ground recorded')}): ` +
|
|
1010
|
+
'no usable phase manifest entry and no verifiable SUMMARY trace digest, so the phase-commit scan ran next.');
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
catch {
|
|
1014
|
+
notes.push(`${SHARED_RESOLUTION_NOTE_PREFIX} threw, so the phase-commit scan ran next.`);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
const token = normalisePhaseToken(opts?.phaseToken);
|
|
1018
|
+
if (sharedResolved) {
|
|
1019
|
+
// The phase-scoped range above is adopted; the token scan is not needed.
|
|
1020
|
+
}
|
|
1021
|
+
else if (token === null) {
|
|
1022
|
+
notes.push('no usable phase token was supplied, so the phase-commit scan never ran and the range is the whole ' +
|
|
1023
|
+
'base-branch span — which, on a repository that uses no phase branching, covers every phase since ' +
|
|
1024
|
+
'the base rather than this one. Files below may belong to another phase.');
|
|
1025
|
+
}
|
|
1026
|
+
else {
|
|
1027
|
+
const needle = `(${token}-`;
|
|
1028
|
+
let anchor = null;
|
|
1029
|
+
try {
|
|
1030
|
+
const log = exec(['log', '--format=%H', '--reverse', '-F', '--grep', needle, `${base}..HEAD`], { cwd });
|
|
1031
|
+
if (log.exitCode === 0 && !log.error) {
|
|
1032
|
+
const first = String(log.stdout || '')
|
|
1033
|
+
.split('\n')
|
|
1034
|
+
.map((line) => line.trim())
|
|
1035
|
+
.find((line) => COMMIT_SHA_SHAPE.test(line));
|
|
1036
|
+
if (first !== undefined) {
|
|
1037
|
+
const parent = exec(['rev-parse', '--verify', `${first}^`], { cwd });
|
|
1038
|
+
anchor = parent.exitCode === 0 && !parent.error ? `${first}^` : GIT_EMPTY_TREE;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
catch {
|
|
1043
|
+
anchor = null;
|
|
1044
|
+
}
|
|
1045
|
+
if (anchor === null) {
|
|
1046
|
+
notes.push(`no commit on ${base}..HEAD carries the "${needle}" convention token, so the phase-commit scan ` +
|
|
1047
|
+
'resolved nothing and the range fell back to the whole base-branch span. Files below may belong ' +
|
|
1048
|
+
'to another phase.');
|
|
1049
|
+
}
|
|
1050
|
+
else {
|
|
1051
|
+
range = `${anchor}..HEAD`;
|
|
1052
|
+
rangeSource = REVIEW_RANGE_SOURCE.PHASE_COMMIT_SCAN;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
let files = [];
|
|
1056
|
+
try {
|
|
1057
|
+
const diff = exec(['diff', '--name-only', '--diff-filter=d', range], { cwd });
|
|
1058
|
+
if (diff.exitCode === 0 && !diff.error) {
|
|
1059
|
+
files = String(diff.stdout || '')
|
|
1060
|
+
.split('\n')
|
|
1061
|
+
.map((line) => line.trim())
|
|
1062
|
+
.filter((line) => line !== '');
|
|
1063
|
+
}
|
|
1064
|
+
else {
|
|
1065
|
+
notes.push(`the diff over ${range} could not be READ, so zero files were considered — this is an unreadable ` +
|
|
1066
|
+
'diff, not a phase that changed nothing.');
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
catch {
|
|
1070
|
+
notes.push(`the diff over ${range} threw, so zero files were considered — this is an unreadable diff, not a ` +
|
|
1071
|
+
'phase that changed nothing.');
|
|
1072
|
+
}
|
|
1073
|
+
// ── WHAT the range established, read from the audit leaf's ONE predicate
|
|
1074
|
+
// (08-05, D-08-17). `phase-commit-scan` counts as phase-scoped because the
|
|
1075
|
+
// review's OWN scan anchored on this phase's commits; only the base-branch
|
|
1076
|
+
// span establishes nothing about the phase, which is exactly the direction
|
|
1077
|
+
// WR-04's predicate states and 08-04 preserved (D-08-18).
|
|
1078
|
+
//
|
|
1079
|
+
// The values are read from `AUDIT_RANGE_OUTCOME`, never re-spelled here — the
|
|
1080
|
+
// `REVIEW_SOURCE_FOR_AUDIT_SOURCE` rule, for the same reason: a token this
|
|
1081
|
+
// tier spells for itself is a token that can drift from the one the blocking
|
|
1082
|
+
// tier serializes. FAIL TOWARD THE CAUTIOUS CELL if the predicate throws: zero
|
|
1083
|
+
// files is `range_unresolvable`, anything else `fallback_audited`. Neither
|
|
1084
|
+
// fallback can claim `phase_scoped`, because a call we could not complete is
|
|
1085
|
+
// not a witness that the range belongs to this phase.
|
|
1086
|
+
let outcome;
|
|
1087
|
+
try {
|
|
1088
|
+
outcome = auditMod.classifyRangeOutcome({
|
|
1089
|
+
phaseScoped: rangeSource !== REVIEW_RANGE_SOURCE.BASE_BRANCH,
|
|
1090
|
+
filesConsidered: files.length,
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
catch {
|
|
1094
|
+
outcome =
|
|
1095
|
+
files.length === 0
|
|
1096
|
+
? auditMod.AUDIT_RANGE_OUTCOME.RANGE_UNRESOLVABLE
|
|
1097
|
+
: auditMod.AUDIT_RANGE_OUTCOME.FALLBACK_AUDITED;
|
|
1098
|
+
}
|
|
1099
|
+
return { range, rangeSource, base, files, filesConsidered: files.length, outcome, notes };
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* The grounds, in order, on which THIS input records that the candidate
|
|
1103
|
+
* derivation was not measured. Empty when everything it records was measured —
|
|
1104
|
+
* including when it records nothing at all about measurement.
|
|
1105
|
+
*
|
|
1106
|
+
* Extracted rather than inlined into the predicate because two consumers need
|
|
1107
|
+
* one answer: the predicate decides, and the payload's refusal statement NAMES
|
|
1108
|
+
* what was missing. Recomputing the second from the first's prose, or asking the
|
|
1109
|
+
* question twice, would be the 04-18 shape again.
|
|
1110
|
+
*/
|
|
1111
|
+
function unmeasuredGrounds(input) {
|
|
1112
|
+
if (input === null || typeof input !== 'object')
|
|
1113
|
+
return [];
|
|
1114
|
+
const source = input;
|
|
1115
|
+
const grounds = [];
|
|
1116
|
+
if (source.inputSet !== null && typeof source.inputSet === 'object' && source.inputSet.oracleSource === 'unavailable') {
|
|
1117
|
+
grounds.push(ORACLE_UNAVAILABLE_GROUND);
|
|
1118
|
+
}
|
|
1119
|
+
for (const arm of [source.naUniverse, source.naNoFile]) {
|
|
1120
|
+
if (arm === null || arm === undefined || typeof arm !== 'object')
|
|
1121
|
+
continue;
|
|
1122
|
+
const record = arm.notDerived;
|
|
1123
|
+
if (record === null || record === undefined || typeof record !== 'object')
|
|
1124
|
+
continue;
|
|
1125
|
+
const ground = record.ground;
|
|
1126
|
+
if (typeof ground === 'string' && UNMEASURED_GROUND.has(ground) && !grounds.includes(ground)) {
|
|
1127
|
+
grounds.push(ground);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
return grounds;
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* The ONE predicate that forbids a clean review (F-3, D-06-15; widened by D3).
|
|
1134
|
+
*
|
|
1135
|
+
* True on either of two independent grounds:
|
|
1136
|
+
*
|
|
1137
|
+
* 1. THE REVIEW CONSIDERED NO FILES — whatever the range source, and regardless
|
|
1138
|
+
* of how many rows a caller has assembled. A review that looked at nothing
|
|
1139
|
+
* and rendered clean is the rubber stamp this phase exists to prevent, and
|
|
1140
|
+
* `secure-phase` already states the rule in its own words: empty-by-no-
|
|
1141
|
+
* planning must not rubber-stamp a clean artifact.
|
|
1142
|
+
* 2. THE CANDIDATE DERIVATION WAS NOT MEASURED — the input POSITIVELY records
|
|
1143
|
+
* an unavailable NO_CHECK oracle, or an arm that did not derive on a ground
|
|
1144
|
+
* in `UNMEASURED_GROUND`. This is D3's second half: a phase with a hundred
|
|
1145
|
+
* changed files and an unreachable vault passed condition 1 easily and
|
|
1146
|
+
* rendered clean having judged nothing.
|
|
1147
|
+
*
|
|
1148
|
+
* The two silences are treated DIFFERENTLY, on purpose. A missing, non-numeric,
|
|
1149
|
+
* non-integer or negative file count is not a witness that files were
|
|
1150
|
+
* considered, so it refuses: the range's own contract is to state that number.
|
|
1151
|
+
* An input that says nothing about measurement is a caller holding only a range
|
|
1152
|
+
* — 06-04's CLI arm and the range tests do exactly that — and inventing a
|
|
1153
|
+
* refusal from its silence would refuse every one of them. Condition 2 fires on
|
|
1154
|
+
* a RECORD, never on an absence.
|
|
1155
|
+
*
|
|
1156
|
+
* Exported so the CLI arm (06-04) and the workflow (06-07) consult THIS
|
|
1157
|
+
* predicate rather than a private restatement: two predicates for one claim are
|
|
1158
|
+
* two claims, and this repository has already paid for that once (04-18).
|
|
1159
|
+
*/
|
|
1160
|
+
function reviewRefusesCleanRender(input) {
|
|
1161
|
+
const value = typeof input === 'number'
|
|
1162
|
+
? input
|
|
1163
|
+
: input !== null && typeof input === 'object'
|
|
1164
|
+
? input.filesConsidered
|
|
1165
|
+
: undefined;
|
|
1166
|
+
if (!(typeof value === 'number' && Number.isInteger(value) && value > 0))
|
|
1167
|
+
return true;
|
|
1168
|
+
return unmeasuredGrounds(input).length > 0;
|
|
1169
|
+
}
|
|
1170
|
+
// ─── The artifact validator (parse, then judge ONE unit) ─────────────────────
|
|
1171
|
+
/**
|
|
1172
|
+
* ## Why this half exists, and why it is shaped like this
|
|
1173
|
+
*
|
|
1174
|
+
* Everything above computes what the agent must NOT invent. This half judges
|
|
1175
|
+
* what the agent DID write. The design is not a new one: it is
|
|
1176
|
+
* `src/practices-review-check.cts`'s, copied deliberately, because that module's
|
|
1177
|
+
* header records a defect class that survived FOUR verification rounds while the
|
|
1178
|
+
* detector lived as inline shell glue built on state shared across findings. Its
|
|
1179
|
+
* answer — parse into nodes FIRST, then judge exactly ONE unit as a pure
|
|
1180
|
+
* function of that unit alone, with the single-parameter signature standing as
|
|
1181
|
+
* the isolation guarantee — is the one this module adopts verbatim.
|
|
1182
|
+
*
|
|
1183
|
+
* So `judgeReviewRow` takes one parameter and reads nothing else. A malformed
|
|
1184
|
+
* row can change its OWN verdict and its own problems, and no sibling's, in
|
|
1185
|
+
* either direction. Everything that is genuinely cross-row — the rate line's
|
|
1186
|
+
* arithmetic, the counts line's arithmetic, the fence balance, the forbidden
|
|
1187
|
+
* score-token scan — is computed by `validateReviewArtifact` from the judged
|
|
1188
|
+
* rows, AFTER every row has been judged in isolation.
|
|
1189
|
+
*
|
|
1190
|
+
* ## Trust boundary
|
|
1191
|
+
*
|
|
1192
|
+
* The text handed here is UNTRUSTED on two counts at once: an LLM wrote the
|
|
1193
|
+
* section, and the fenced excerpts inside it are client-authored vault prose.
|
|
1194
|
+
* Nothing here performs I/O, nothing throws, and every value echoed into a
|
|
1195
|
+
* problem detail passes `boundedToken` first.
|
|
1196
|
+
*/
|
|
1197
|
+
/**
|
|
1198
|
+
* The problem vocabulary. Frozen and exported for the same reason the three
|
|
1199
|
+
* vocabularies above are: a consumer must branch on a VALUE, and a test must
|
|
1200
|
+
* harvest the token it asserts on rather than copy it.
|
|
1201
|
+
*
|
|
1202
|
+
* The four `FORBIDDEN_*` members are the D-06-13 score FORMS. They live in this
|
|
1203
|
+
* one vocabulary rather than a second one because a token the scan finds becomes
|
|
1204
|
+
* a problem in this list unchanged — one label, one home, no translation step
|
|
1205
|
+
* where the two could drift.
|
|
1206
|
+
*/
|
|
1207
|
+
const REVIEW_PROBLEM_KIND = Object.freeze({
|
|
1208
|
+
/** No `## Practice Verdicts (agent-judged)` heading anywhere in the supplied text. */
|
|
1209
|
+
SECTION_ABSENT: 'section-absent',
|
|
1210
|
+
/** The heading is present and the section holds no verdict row at all. */
|
|
1211
|
+
NO_ROWS: 'no-rows',
|
|
1212
|
+
ROW_MISSING_ID: 'row-missing-id',
|
|
1213
|
+
ROW_UNKNOWN_VERDICT: 'row-unknown-verdict',
|
|
1214
|
+
/** D-06-11: a non-verdict with no stated reason. */
|
|
1215
|
+
ROW_MISSING_REASON: 'row-missing-reason',
|
|
1216
|
+
/** D-06-09: a row marked derived that does not say WHICH derivation produced it. */
|
|
1217
|
+
ROW_DERIVED_LABEL_MISSING: 'row-derived-label-missing',
|
|
1218
|
+
/** D-06-09: a derived row reclassified into a judged verdict. */
|
|
1219
|
+
ROW_DERIVED_VERDICT_CONFLICT: 'row-derived-verdict-conflict',
|
|
1220
|
+
RATE_LINE_ABSENT: 'rate-line-absent',
|
|
1221
|
+
/** D-06-12: the rate line's numbers are not the ones the rows support. */
|
|
1222
|
+
RATE_LINE_ARITHMETIC: 'rate-line-arithmetic',
|
|
1223
|
+
COUNTS_LINE_ABSENT: 'counts-line-absent',
|
|
1224
|
+
/** D-06-13: the counts line's numbers are not the ones the rows support. */
|
|
1225
|
+
COUNTS_LINE_ARITHMETIC: 'counts-line-arithmetic',
|
|
1226
|
+
// ── MECH-02 / D-10-20: the mechanical coverage line ────────────────────────
|
|
1227
|
+
// An instructed-only line is an unenforced one: an artifact that dropped it
|
|
1228
|
+
// validated clean. These four kinds close it.
|
|
1229
|
+
/** The payload carried a coverage line and the artifact does not. */
|
|
1230
|
+
COVERAGE_LINE_ABSENT: 'coverage-line-absent',
|
|
1231
|
+
/** A coverage line is present but is not the engine's sentence, verbatim. */
|
|
1232
|
+
COVERAGE_LINE_MISMATCH: 'coverage-line-mismatch',
|
|
1233
|
+
/**
|
|
1234
|
+
* The line sits inside the verdicts section, which the agent contract
|
|
1235
|
+
* explicitly forbids: "a second bare shaped line makes the section
|
|
1236
|
+
* contradict itself". It belongs under `## Range`.
|
|
1237
|
+
*/
|
|
1238
|
+
COVERAGE_LINE_MISPLACED: 'coverage-line-misplaced',
|
|
1239
|
+
/**
|
|
1240
|
+
* The payload carried NO coverage line and the artifact invented one. Its
|
|
1241
|
+
* absence means the inputs were never measured, so any figure here states a
|
|
1242
|
+
* measurement nobody took.
|
|
1243
|
+
*/
|
|
1244
|
+
COVERAGE_LINE_UNEXPECTED: 'coverage-line-unexpected',
|
|
1245
|
+
/** The external-content fence markers do not pair up. */
|
|
1246
|
+
FENCE_UNBALANCED: 'fence-unbalanced',
|
|
1247
|
+
/** The parse cap was reached; rows beyond it were NOT judged. */
|
|
1248
|
+
ROWS_TRUNCATED: 'rows-truncated',
|
|
1249
|
+
// ── The population, checked against the payload by `vault review finalize` ──
|
|
1250
|
+
/** A practice of the review's population has no row: dropped rather than judged. */
|
|
1251
|
+
ROW_POPULATION_MISSING: 'row-population-missing',
|
|
1252
|
+
/** A row names a practice outside the population the payload derived. */
|
|
1253
|
+
ROW_POPULATION_UNEXPECTED: 'row-population-unexpected',
|
|
1254
|
+
/** One practice carries more than one row. */
|
|
1255
|
+
ROW_DUPLICATE: 'row-duplicate',
|
|
1256
|
+
// The four D-06-13 score forms.
|
|
1257
|
+
FORBIDDEN_PERCENTAGE: 'forbidden-percentage',
|
|
1258
|
+
FORBIDDEN_RATIO: 'forbidden-ratio',
|
|
1259
|
+
FORBIDDEN_SCORE_ASSIGNMENT: 'forbidden-score-assignment',
|
|
1260
|
+
FORBIDDEN_LETTER_GRADE: 'forbidden-letter-grade',
|
|
1261
|
+
});
|
|
1262
|
+
/** The heading D-06-02 locks, verbatim. Never re-derived from configuration. */
|
|
1263
|
+
const REVIEW_SECTION_HEADING = '## Practice Verdicts (agent-judged)';
|
|
1264
|
+
/** The D-06-12 rate line, as a shape. The renderer above is the only writer. */
|
|
1265
|
+
const RATE_LINE_SHAPE = /^(\d+) of (\d+) practices judged · (\d+) N-A · (\d+) INCONCLUSIVE$/;
|
|
1266
|
+
/**
|
|
1267
|
+
* The D-10-03 mechanical coverage line, as a shape. The audit's
|
|
1268
|
+
* `renderMechanicalCoverageLine` is the only writer; this recognises its output so a
|
|
1269
|
+
* MISPLACED or UNEXPECTED line can be named even when no expectation was supplied.
|
|
1270
|
+
*/
|
|
1271
|
+
const COVERAGE_LINE_SHAPE = /^(\d+) of (\d+) practices mechanically checked · (\d+) PASS · (\d+) FAIL · (\d+) NOT_MATCHED · (\d+) NO_CHECK$/;
|
|
1272
|
+
/** The D-06-13 counts line, as a shape. */
|
|
1273
|
+
const COUNTS_LINE_SHAPE = /^(\d+) PASS · (\d+) FAIL · (\d+) INCONCLUSIVE · (\d+) N-A \(of (\d+) traced\)$/;
|
|
1274
|
+
/** A markdown table separator cell: `---`, `:---`, `---:` or `:---:`. */
|
|
1275
|
+
const TABLE_SEPARATOR_CELL = /^:?-{3,}:?$/;
|
|
1276
|
+
/** A `Source` cell that declares the row DERIVED rather than agent-judged. */
|
|
1277
|
+
const DERIVED_SOURCE_SHAPE = /^derived\b/i;
|
|
1278
|
+
/** The distinguishing label inside a derived `Source` cell: `derived (…)`. */
|
|
1279
|
+
const DERIVED_LABEL_SHAPE = /\(([^)]*)\)/;
|
|
1280
|
+
/**
|
|
1281
|
+
* The four forbidden score FORMS, targeted by SHAPE rather than by word.
|
|
1282
|
+
*
|
|
1283
|
+
* That distinction is the whole design. A word-targeted scan would flag the
|
|
1284
|
+
* disclaimer sentence a review is supposed to carry — the one that states no
|
|
1285
|
+
* single score, ratio, percentage or letter grade was computed — and a rule that
|
|
1286
|
+
* forbids saying what the rule forbids is a rule nobody can comply with. Each
|
|
1287
|
+
* pattern below therefore requires the numeric or graded FORM itself, never the
|
|
1288
|
+
* name of it.
|
|
1289
|
+
*
|
|
1290
|
+
* The two sanctioned lines need no exemption entry and deliberately do not get
|
|
1291
|
+
* one: neither carries a percent sign, a slash-separated pair, a score
|
|
1292
|
+
* assignment or a letter grade, so they pass on their own shape. An exemption
|
|
1293
|
+
* list would additionally let a real violation hide inside a line that merely
|
|
1294
|
+
* looked sanctioned.
|
|
1295
|
+
*/
|
|
1296
|
+
const FORBIDDEN_SCORE_SHAPES = Object.freeze([
|
|
1297
|
+
{ kind: REVIEW_PROBLEM_KIND.FORBIDDEN_PERCENTAGE, re: /\d\s*%|\b\d+(?:\.\d+)?\s*(?:percent|pct)\b/gi },
|
|
1298
|
+
{ kind: REVIEW_PROBLEM_KIND.FORBIDDEN_RATIO, re: /\b\d+\s*\/\s*\d+\b/g },
|
|
1299
|
+
{
|
|
1300
|
+
kind: REVIEW_PROBLEM_KIND.FORBIDDEN_SCORE_ASSIGNMENT,
|
|
1301
|
+
re: /\b(?:score|rating|compliance|conformance|health)\b[^\S\n]*[:=][^\S\n]*\S/gi,
|
|
1302
|
+
},
|
|
1303
|
+
{
|
|
1304
|
+
kind: REVIEW_PROBLEM_KIND.FORBIDDEN_LETTER_GRADE,
|
|
1305
|
+
re: /\bgrades?\b[^\S\n]*[:=]?[^\S\n]*[A-F][+-]?(?![A-Za-z0-9])/gi,
|
|
1306
|
+
},
|
|
1307
|
+
]);
|
|
1308
|
+
/** Compose one problem. Every echoed value is bounded before it lands here. */
|
|
1309
|
+
function reviewProblem(kind, id, line, detail) {
|
|
1310
|
+
return { kind, id, line, detail };
|
|
1311
|
+
}
|
|
1312
|
+
/**
|
|
1313
|
+
* The D-06-13 counts line, computed from the four verdict counts and nothing
|
|
1314
|
+
* else — the same discipline `renderRateLine` states for its own line. Internal:
|
|
1315
|
+
* the ONE writer of this shape is this module, and `validateReviewArtifact`
|
|
1316
|
+
* returns the string it computed, so a consumer harvests it rather than
|
|
1317
|
+
* re-spelling it.
|
|
1318
|
+
*/
|
|
1319
|
+
function renderCountsLine(counts) {
|
|
1320
|
+
return (`${counts.pass} PASS · ${counts.fail} FAIL · ${counts.inconclusive} INCONCLUSIVE · ` +
|
|
1321
|
+
`${counts.na} N-A (of ${counts.total} traced)`);
|
|
1322
|
+
}
|
|
1323
|
+
/** Split a markdown table line into trimmed cells, dropping the outer pipes. */
|
|
1324
|
+
function tableCells(line) {
|
|
1325
|
+
const trimmed = line.trim();
|
|
1326
|
+
const inner = trimmed.slice(1, trimmed.endsWith('|') ? -1 : undefined);
|
|
1327
|
+
return inner.split('|').map((c) => c.trim());
|
|
1328
|
+
}
|
|
1329
|
+
function isTableLine(line) {
|
|
1330
|
+
return line.trim().startsWith('|');
|
|
1331
|
+
}
|
|
1332
|
+
function isSeparatorLine(line) {
|
|
1333
|
+
if (!isTableLine(line))
|
|
1334
|
+
return false;
|
|
1335
|
+
const cells = tableCells(line);
|
|
1336
|
+
return cells.length > 0 && cells.every((c) => TABLE_SEPARATOR_CELL.test(c));
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Locate the verdict section's own lines inside the supplied text.
|
|
1340
|
+
*
|
|
1341
|
+
* BOTH CONSUMERS, ONE PARSER. The `## Practice Verdicts (agent-judged)` section
|
|
1342
|
+
* inside a VERIFICATION.md and a whole VAULT-REVIEW.md body reach this function
|
|
1343
|
+
* the same way: the heading is found, and the section runs to the next
|
|
1344
|
+
* top-level `##` heading or to the end of the text. A body that carries the
|
|
1345
|
+
* heading nowhere reports `present: false` — which is a finding, never a silent
|
|
1346
|
+
* empty result.
|
|
1347
|
+
*/
|
|
1348
|
+
function locateReviewSection(lines) {
|
|
1349
|
+
// A line between a closed pair of fence markers is client prose, never
|
|
1350
|
+
// structure: a heading-shaped client line can neither open the section nor
|
|
1351
|
+
// end it (the same trust boundary `parseReviewRows` draws for rows).
|
|
1352
|
+
const fenced = fencedLineMap(lines);
|
|
1353
|
+
const start = lines.findIndex((l, i) => !fenced[i] && l.trim() === REVIEW_SECTION_HEADING);
|
|
1354
|
+
if (start === -1)
|
|
1355
|
+
return { present: false, start: 0, end: lines.length };
|
|
1356
|
+
let end = lines.length;
|
|
1357
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
1358
|
+
const line = lines[i];
|
|
1359
|
+
if (fenced[i])
|
|
1360
|
+
continue;
|
|
1361
|
+
if (line.startsWith('## ') || line.trim() === '---') {
|
|
1362
|
+
end = i;
|
|
1363
|
+
break;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
return { present: true, start: start + 1, end };
|
|
1367
|
+
}
|
|
1368
|
+
/**
|
|
1369
|
+
* WHICH LINES ARE CLIENT-AUTHORED. One computation, for every pass that needs
|
|
1370
|
+
* the answer.
|
|
1371
|
+
*
|
|
1372
|
+
* The fence MARKERS are harvested from the shipped check module rather than
|
|
1373
|
+
* re-spelled here, for the reason stated at the imports: a second spelling of a
|
|
1374
|
+
* marker is a second definition of where untrusted content ends. A second
|
|
1375
|
+
* COMPUTATION of which lines that marker encloses is the same failure with the
|
|
1376
|
+
* same consequence, and it is what shipped — `scanForbiddenScoreTokens` had this
|
|
1377
|
+
* pass inline and `parseReviewRows` had none, so a client-authored markdown
|
|
1378
|
+
* table inside a closed fence was excluded from the score scan and counted as an
|
|
1379
|
+
* agent verdict by the row parser at the same time (T-6-18). The two passes
|
|
1380
|
+
* disagreeing about the trust boundary let the audited party set the audit's own
|
|
1381
|
+
* number.
|
|
1382
|
+
*
|
|
1383
|
+
* Both semantics are the ones the score scan already had, and both are kept
|
|
1384
|
+
* deliberately:
|
|
1385
|
+
*
|
|
1386
|
+
* - Only a CLOSED pair fences anything. An opener with no closer marks
|
|
1387
|
+
* nothing, so the unterminated remainder is still scanned and still parsed.
|
|
1388
|
+
* Failing towards reading is the direction that cannot hide a violation, and
|
|
1389
|
+
* the unbalanced pair is separately reported as `FENCE_UNBALANCED`.
|
|
1390
|
+
* - The markers' own lines are inside the region. They are structure, never
|
|
1391
|
+
* content, and neither pass has anything to say about them.
|
|
1392
|
+
*
|
|
1393
|
+
* Total over any input; `lines` is never mutated.
|
|
1394
|
+
*/
|
|
1395
|
+
function fencedLineMap(lines) {
|
|
1396
|
+
const fenced = new Array(lines.length).fill(false);
|
|
1397
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
1398
|
+
if (!lines[i].includes(FENCE_START_MARKER))
|
|
1399
|
+
continue;
|
|
1400
|
+
let close = -1;
|
|
1401
|
+
for (let j = i + 1; j < lines.length; j += 1) {
|
|
1402
|
+
if (lines[j].includes(FENCE_END_MARKER)) {
|
|
1403
|
+
close = j;
|
|
1404
|
+
break;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
if (close === -1)
|
|
1408
|
+
break;
|
|
1409
|
+
for (let k = i; k <= close; k += 1)
|
|
1410
|
+
fenced[k] = true;
|
|
1411
|
+
i = close;
|
|
1412
|
+
}
|
|
1413
|
+
return fenced;
|
|
1414
|
+
}
|
|
1415
|
+
/**
|
|
1416
|
+
* Parse the section's verdict rows into units. NOTHING is judged here, and no
|
|
1417
|
+
* unit is ever dropped or truncated out of the returned array below the cap:
|
|
1418
|
+
* a row this parser discarded would be a row whose defects nobody could report.
|
|
1419
|
+
*
|
|
1420
|
+
* FENCED LINES ARE NOT ROWS. The exclusion is `fencedLineMap`'s, shared with the
|
|
1421
|
+
* score scan rather than restated, and it is the trust boundary this module's
|
|
1422
|
+
* header draws: a pipe table between the external-content markers is prose from
|
|
1423
|
+
* the client's vault that the agent was instructed to reproduce verbatim, and it
|
|
1424
|
+
* is not a verdict the agent reached. Counting it is tampering in one direction
|
|
1425
|
+
* — the judged population inflates and the arithmetic cross-check then validates
|
|
1426
|
+
* the inflated figures — and denial of service in the other, since any table in
|
|
1427
|
+
* any practice document turns a cell reading `Type` into a `row-unknown-verdict`
|
|
1428
|
+
* charged to the agent.
|
|
1429
|
+
*
|
|
1430
|
+
* A fenced line is not a discarded row and is never counted against
|
|
1431
|
+
* `MAX_PARSED_UNITS`: it was never a unit to begin with.
|
|
1432
|
+
*
|
|
1433
|
+
* The header row is recognised by a ONE-LINE LOOKAHEAD — a table line
|
|
1434
|
+
* immediately followed by a separator line is the header. That lookahead lives
|
|
1435
|
+
* in the parser and never in the judge, so the isolation invariant below is
|
|
1436
|
+
* untouched by it. Recognising the header by its literal column text would
|
|
1437
|
+
* instead make a row whose verdict cell happened to read `Verdict` silently
|
|
1438
|
+
* vanish.
|
|
1439
|
+
*/
|
|
1440
|
+
function parseReviewRows(lines, start, end) {
|
|
1441
|
+
const units = [];
|
|
1442
|
+
let truncated = 0;
|
|
1443
|
+
// The same window the score scan is handed (`lines.slice(section.start,
|
|
1444
|
+
// section.end)`), so the two passes cannot be looking at different text.
|
|
1445
|
+
const fenced = fencedLineMap(lines.slice(start, end));
|
|
1446
|
+
for (let i = start; i < end; i += 1) {
|
|
1447
|
+
const line = lines[i];
|
|
1448
|
+
if (fenced[i - start])
|
|
1449
|
+
continue;
|
|
1450
|
+
if (!isTableLine(line))
|
|
1451
|
+
continue;
|
|
1452
|
+
if (isSeparatorLine(line))
|
|
1453
|
+
continue;
|
|
1454
|
+
if (i + 1 < end && isSeparatorLine(lines[i + 1]))
|
|
1455
|
+
continue;
|
|
1456
|
+
if (units.length >= MAX_PARSED_UNITS) {
|
|
1457
|
+
truncated += 1;
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1460
|
+
units.push({ index: units.length, line: i + 1, raw: line, cells: tableCells(line) });
|
|
1461
|
+
}
|
|
1462
|
+
return { units, truncated };
|
|
1463
|
+
}
|
|
1464
|
+
/**
|
|
1465
|
+
* Judge exactly ONE row, as a pure function of that row alone.
|
|
1466
|
+
*
|
|
1467
|
+
* ITS ARITY IS THE CONTRACT. One parameter means this function cannot consult a
|
|
1468
|
+
* sibling, so a malformed row can only ever change its own verdict and its own
|
|
1469
|
+
* problems. That is the invariant `src/practices-review-check.cts` pins as
|
|
1470
|
+
* `judgeUnit.length === 1`, for a defect class that four verification rounds
|
|
1471
|
+
* failed to close while any state was shared across units; the arity is pinned
|
|
1472
|
+
* here by a test for the same reason.
|
|
1473
|
+
*
|
|
1474
|
+
* Four row-level rules, and no more — everything cross-row belongs to the caller:
|
|
1475
|
+
* - a practice id is present (an anonymous verdict is not attributable);
|
|
1476
|
+
* - the verdict is one of the four locked tokens (D-06-10);
|
|
1477
|
+
* - a non-verdict carries a stated, non-empty reason (D-06-11);
|
|
1478
|
+
* - a derived row says WHICH derivation produced it and does not carry a
|
|
1479
|
+
* judged verdict (D-06-09 — the two derived populations are different
|
|
1480
|
+
* findings, and a derived N-A reclassified into PASS/FAIL is the agent
|
|
1481
|
+
* overwriting a fact it was handed).
|
|
1482
|
+
*/
|
|
1483
|
+
function judgeReviewRow(unit) {
|
|
1484
|
+
const u = (unit ?? {});
|
|
1485
|
+
const cells = Array.isArray(u.cells) ? u.cells : [];
|
|
1486
|
+
const cell = (n) => (typeof cells[n] === 'string' ? cells[n].trim() : '');
|
|
1487
|
+
const line = typeof u.line === 'number' && Number.isFinite(u.line) ? u.line : null;
|
|
1488
|
+
const idCell = cell(0).replace(/`/g, '').trim();
|
|
1489
|
+
const verdict = cell(1);
|
|
1490
|
+
const reason = cell(2);
|
|
1491
|
+
const source = cell(3);
|
|
1492
|
+
const id = idCell === '' ? '' : quotableId(idCell);
|
|
1493
|
+
const problems = [];
|
|
1494
|
+
if (idCell === '') {
|
|
1495
|
+
problems.push(reviewProblem(REVIEW_PROBLEM_KIND.ROW_MISSING_ID, null, line, 'this row carries no practice id, so its verdict is attributable to nothing. A verdict ' +
|
|
1496
|
+
'nobody can trace back to a practice is not a finding.'));
|
|
1497
|
+
}
|
|
1498
|
+
const recognised = isJudgedVerdict(verdict) || isNonVerdict(verdict);
|
|
1499
|
+
if (!recognised) {
|
|
1500
|
+
problems.push(reviewProblem(REVIEW_PROBLEM_KIND.ROW_UNKNOWN_VERDICT, id === '' ? null : id, line, `"${safe(verdict)}" is not one of the four locked verdicts. A coined fifth verdict ` +
|
|
1501
|
+
'is counted as unrecognised and is never absorbed into a real one.'));
|
|
1502
|
+
}
|
|
1503
|
+
else if (isNonVerdict(verdict) && reason === '') {
|
|
1504
|
+
problems.push(reviewProblem(REVIEW_PROBLEM_KIND.ROW_MISSING_REASON, id === '' ? null : id, line, 'A non-verdict must carry a stated reason. A bare N-A or INCONCLUSIVE is ' +
|
|
1505
|
+
'indistinguishable from the agent giving up.'));
|
|
1506
|
+
}
|
|
1507
|
+
const derived = DERIVED_SOURCE_SHAPE.test(source);
|
|
1508
|
+
let derivedLabel = null;
|
|
1509
|
+
if (derived) {
|
|
1510
|
+
const matched = DERIVED_LABEL_SHAPE.exec(source);
|
|
1511
|
+
const label = matched && typeof matched[1] === 'string' ? matched[1].trim() : '';
|
|
1512
|
+
derivedLabel = label === '' ? null : safe(label);
|
|
1513
|
+
if (derivedLabel === null) {
|
|
1514
|
+
problems.push(reviewProblem(REVIEW_PROBLEM_KIND.ROW_DERIVED_LABEL_MISSING, id === '' ? null : id, line, 'this row is marked derived but does not say WHICH derivation produced it. The two ' +
|
|
1515
|
+
`derived populations ("${NA_DERIVED_REASON.UNIVERSE_MINUS_LEDGER}" and ` +
|
|
1516
|
+
`"${NA_DERIVED_REASON.NO_FILE_IN_DIFF}") are different findings about different things, ` +
|
|
1517
|
+
'and a reader who cannot tell them apart cannot act on either.'));
|
|
1518
|
+
}
|
|
1519
|
+
if (isJudgedVerdict(verdict)) {
|
|
1520
|
+
problems.push(reviewProblem(REVIEW_PROBLEM_KIND.ROW_DERIVED_VERDICT_CONFLICT, id === '' ? null : id, line, `a derived row carries "${safe(verdict)}", a judged verdict. Derived rows are handed to ` +
|
|
1521
|
+
'the agent to reproduce, not to reclassify: a derived N-A promoted to PASS or FAIL is a ' +
|
|
1522
|
+
'computed fact overwritten by a judgement.'));
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
return { id, verdict, reason, source, derived, derivedLabel, problems };
|
|
1526
|
+
}
|
|
1527
|
+
/**
|
|
1528
|
+
* Find every D-06-13 score form in the supplied text, by SHAPE.
|
|
1529
|
+
*
|
|
1530
|
+
* FENCED REGIONS ARE EXCLUDED, and that is a decision rather than an oversight.
|
|
1531
|
+
* D-06-13 governs what the AGENT writes; text between the external-content fence
|
|
1532
|
+
* markers is client-authored vault prose the agent was instructed to reproduce
|
|
1533
|
+
* verbatim, and a client practice that mentions a ratio of its own is not the
|
|
1534
|
+
* agent scoring a phase. A fence that never closes does NOT swallow the rest of
|
|
1535
|
+
* the document: the unterminated remainder is scanned, because failing towards
|
|
1536
|
+
* scanning is the direction that cannot hide a violation, and the unbalanced
|
|
1537
|
+
* pair is separately reported by `validateReviewArtifact`.
|
|
1538
|
+
*
|
|
1539
|
+
* The exclusion itself is `fencedLineMap`'s and is shared with `parseReviewRows`
|
|
1540
|
+
* rather than computed twice. It used to live inline here, and the row parser
|
|
1541
|
+
* had no equivalent — two passes over one document disagreeing about where
|
|
1542
|
+
* untrusted content ends, which is T-6-18.
|
|
1543
|
+
*
|
|
1544
|
+
* Total over any input. A non-string reports no findings, which is honest: there
|
|
1545
|
+
* was no text to scan, and the caller's own absent-input problem says so.
|
|
1546
|
+
*/
|
|
1547
|
+
function scanForbiddenScoreTokens(text) {
|
|
1548
|
+
if (typeof text !== 'string' || text === '')
|
|
1549
|
+
return [];
|
|
1550
|
+
const lines = text.split(/\r?\n/);
|
|
1551
|
+
// Computed before any scanning so an unterminated opener cannot silence
|
|
1552
|
+
// everything after it.
|
|
1553
|
+
const fenced = fencedLineMap(lines);
|
|
1554
|
+
const findings = [];
|
|
1555
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
1556
|
+
if (fenced[i])
|
|
1557
|
+
continue;
|
|
1558
|
+
for (const shape of FORBIDDEN_SCORE_SHAPES) {
|
|
1559
|
+
for (const m of lines[i].matchAll(shape.re)) {
|
|
1560
|
+
findings.push({ kind: shape.kind, line: i + 1, match: safe(m[0]) });
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
return findings;
|
|
1565
|
+
}
|
|
1566
|
+
function validateReviewArtifact(text, opts) {
|
|
1567
|
+
const problems = [];
|
|
1568
|
+
const src = typeof text === 'string' ? text : '';
|
|
1569
|
+
const lines = src.split(/\r?\n/);
|
|
1570
|
+
const section = locateReviewSection(lines);
|
|
1571
|
+
if (typeof text !== 'string') {
|
|
1572
|
+
problems.push(reviewProblem(REVIEW_PROBLEM_KIND.SECTION_ABSENT, null, null, 'no artifact text was supplied, so nothing was validated. This is an absent input, which is ' +
|
|
1573
|
+
'a different finding from an artifact that was read and held no defect.'));
|
|
1574
|
+
}
|
|
1575
|
+
else if (!section.present) {
|
|
1576
|
+
problems.push(reviewProblem(REVIEW_PROBLEM_KIND.SECTION_ABSENT, null, null, `the artifact carries no "${REVIEW_SECTION_HEADING}" heading, so no verdict row was found ` +
|
|
1577
|
+
'and nothing was judged. The heading is locked; a section under any other heading is ' +
|
|
1578
|
+
'not this section.'));
|
|
1579
|
+
}
|
|
1580
|
+
const { units, truncated } = section.present
|
|
1581
|
+
? parseReviewRows(lines, section.start, section.end)
|
|
1582
|
+
: { units: [], truncated: 0 };
|
|
1583
|
+
if (truncated > 0) {
|
|
1584
|
+
problems.push(reviewProblem(REVIEW_PROBLEM_KIND.ROWS_TRUNCATED, null, null, `${truncated} row(s) beyond the parse cap of ${MAX_PARSED_UNITS} were NOT judged. A cap ` +
|
|
1585
|
+
'reached in silence would report a partial judgement as a whole one.'));
|
|
1586
|
+
}
|
|
1587
|
+
const rows = [];
|
|
1588
|
+
for (const unit of units) {
|
|
1589
|
+
const judged = judgeReviewRow(unit);
|
|
1590
|
+
rows.push(judged);
|
|
1591
|
+
for (const p of judged.problems)
|
|
1592
|
+
problems.push(p);
|
|
1593
|
+
}
|
|
1594
|
+
if (section.present && units.length === 0) {
|
|
1595
|
+
problems.push(reviewProblem(REVIEW_PROBLEM_KIND.NO_ROWS, null, null, 'the section heading is present and carries no verdict row. An empty section is not a clean ' +
|
|
1596
|
+
'review: a gap is reported loudly, never as an empty register.'));
|
|
1597
|
+
}
|
|
1598
|
+
const counts = countReviewVerdicts(rows);
|
|
1599
|
+
const expectedRateLine = renderRateLine(counts);
|
|
1600
|
+
const expectedCountsLine = renderCountsLine(counts);
|
|
1601
|
+
const body = section.present ? lines.slice(section.start, section.end) : [];
|
|
1602
|
+
const foundRateLine = body.map((l) => l.trim()).find((l) => RATE_LINE_SHAPE.test(l)) ?? null;
|
|
1603
|
+
const foundCountsLine = body.map((l) => l.trim()).find((l) => COUNTS_LINE_SHAPE.test(l)) ?? null;
|
|
1604
|
+
if (section.present) {
|
|
1605
|
+
if (foundRateLine === null) {
|
|
1606
|
+
problems.push(reviewProblem(REVIEW_PROBLEM_KIND.RATE_LINE_ABSENT, null, null, `The section carries no non-verdict rate line. Expected the computed line ` +
|
|
1607
|
+
`"${expectedRateLine}" — the denominator is the point, and a list of green rows without ` +
|
|
1608
|
+
'it cannot tell a reader that a review is hollow.'));
|
|
1609
|
+
}
|
|
1610
|
+
else if (foundRateLine !== expectedRateLine) {
|
|
1611
|
+
problems.push(reviewProblem(REVIEW_PROBLEM_KIND.RATE_LINE_ARITHMETIC, null, null, `The rate line reads "${safe(foundRateLine)}" while the rows support ` +
|
|
1612
|
+
`"${expectedRateLine}". The arithmetic is computed from the rows and never narrated.`));
|
|
1613
|
+
}
|
|
1614
|
+
if (foundCountsLine === null) {
|
|
1615
|
+
problems.push(reviewProblem(REVIEW_PROBLEM_KIND.COUNTS_LINE_ABSENT, null, null, `The section carries no counts line. Expected "${expectedCountsLine}".`));
|
|
1616
|
+
}
|
|
1617
|
+
else if (foundCountsLine !== expectedCountsLine) {
|
|
1618
|
+
problems.push(reviewProblem(REVIEW_PROBLEM_KIND.COUNTS_LINE_ARITHMETIC, null, null, `The counts line reads "${safe(foundCountsLine)}" while the rows support ` +
|
|
1619
|
+
`"${expectedCountsLine}".`));
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
// ── MECH-02 / D-10-20: the mechanical coverage line ────────────────────────
|
|
1623
|
+
// Scanned over the WHOLE document, not `body`: the agent contract puts this
|
|
1624
|
+
// line under `## Range` and forbids it inside the verdicts section, so a
|
|
1625
|
+
// scan scoped to that section could never see it. That scoping is exactly
|
|
1626
|
+
// why phase 10 shipped the line unenforced.
|
|
1627
|
+
if (opts && Object.prototype.hasOwnProperty.call(opts, 'expectedCoverageLine')) {
|
|
1628
|
+
const expected = opts.expectedCoverageLine;
|
|
1629
|
+
const inSection = (i) => section.present && i >= section.start && i < section.end;
|
|
1630
|
+
let foundIdx = -1;
|
|
1631
|
+
const fencedLines = fencedLineMap(lines);
|
|
1632
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
1633
|
+
if (fencedLines[i])
|
|
1634
|
+
continue;
|
|
1635
|
+
if (COVERAGE_LINE_SHAPE.test(lines[i].trim())) {
|
|
1636
|
+
foundIdx = i;
|
|
1637
|
+
break;
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
const found = foundIdx >= 0 ? lines[foundIdx].trim() : null;
|
|
1641
|
+
if (typeof expected === 'string' && expected.length > 0) {
|
|
1642
|
+
if (found === null) {
|
|
1643
|
+
problems.push(reviewProblem(REVIEW_PROBLEM_KIND.COVERAGE_LINE_ABSENT, null, null, `The payload carried the mechanical coverage line "${expected}" and the ` +
|
|
1644
|
+
'artifact reproduces it nowhere. That line is how a reader learns how much of the ' +
|
|
1645
|
+
'vault was mechanically checked; without it a page of judged rows cannot say ' +
|
|
1646
|
+
'whether the mechanical tier ran at all.'));
|
|
1647
|
+
}
|
|
1648
|
+
else if (found !== expected) {
|
|
1649
|
+
problems.push(reviewProblem(REVIEW_PROBLEM_KIND.COVERAGE_LINE_MISMATCH, null, null, `The coverage line reads "${safe(found)}" where the engine computed ` +
|
|
1650
|
+
`"${expected}". It is the engine's sentence, reproduced verbatim and never restated.`));
|
|
1651
|
+
}
|
|
1652
|
+
else if (inSection(foundIdx)) {
|
|
1653
|
+
problems.push(reviewProblem(REVIEW_PROBLEM_KIND.COVERAGE_LINE_MISPLACED, null, null, 'The coverage line sits inside the verdicts section, where a second bare ' +
|
|
1654
|
+
'shaped line makes the section contradict itself — its figures count a different ' +
|
|
1655
|
+
'population from the rate line above them. It belongs under "## Range".'));
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
else if (expected === null && found !== null) {
|
|
1659
|
+
problems.push(reviewProblem(REVIEW_PROBLEM_KIND.COVERAGE_LINE_UNEXPECTED, null, null, `The artifact carries "${safe(found)}" while the payload carried no coverage ` +
|
|
1660
|
+
'line at all. Its absence means the inputs were never measured, so this figure ' +
|
|
1661
|
+
'states a measurement nobody took.'));
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
let fenceStarts = 0;
|
|
1665
|
+
let fenceEnds = 0;
|
|
1666
|
+
let depth = 0;
|
|
1667
|
+
let endBeforeStart = false;
|
|
1668
|
+
for (const line of body) {
|
|
1669
|
+
if (line.includes(FENCE_START_MARKER)) {
|
|
1670
|
+
fenceStarts += 1;
|
|
1671
|
+
depth += 1;
|
|
1672
|
+
}
|
|
1673
|
+
if (line.includes(FENCE_END_MARKER)) {
|
|
1674
|
+
fenceEnds += 1;
|
|
1675
|
+
if (depth === 0)
|
|
1676
|
+
endBeforeStart = true;
|
|
1677
|
+
else
|
|
1678
|
+
depth -= 1;
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
if (section.present && (fenceStarts !== fenceEnds || endBeforeStart)) {
|
|
1682
|
+
problems.push(reviewProblem(REVIEW_PROBLEM_KIND.FENCE_UNBALANCED, null, null, `the external-content fence markers do not pair up: ${fenceStarts} opener(s) and ` +
|
|
1683
|
+
`${fenceEnds} closer(s)` +
|
|
1684
|
+
(endBeforeStart ? ', with a closer appearing before any opener' : '') +
|
|
1685
|
+
'. An unbalanced pair means client-authored prose is sitting outside the boundary that ' +
|
|
1686
|
+
'marks it as reference data, which is the escape the fencing exists to prevent.'));
|
|
1687
|
+
}
|
|
1688
|
+
for (const finding of scanForbiddenScoreTokens(body.join('\n'))) {
|
|
1689
|
+
problems.push(reviewProblem(finding.kind, null, section.present ? section.start + finding.line : finding.line, `No single-number roll-up may appear anywhere in this section; "${finding.match}" is one. ` +
|
|
1690
|
+
'Counts by verdict stay sortable and trackable across phases without inviting a ' +
|
|
1691
|
+
'measured-sounding claim about a judgement.'));
|
|
1692
|
+
}
|
|
1693
|
+
return {
|
|
1694
|
+
sectionPresent: section.present,
|
|
1695
|
+
units,
|
|
1696
|
+
rows,
|
|
1697
|
+
counts,
|
|
1698
|
+
foundRateLine,
|
|
1699
|
+
expectedRateLine,
|
|
1700
|
+
foundCountsLine,
|
|
1701
|
+
expectedCountsLine,
|
|
1702
|
+
fenceStarts,
|
|
1703
|
+
fenceEnds,
|
|
1704
|
+
problems,
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
/** The only `predicate.vault.state` value that means the vault was reached. */
|
|
1708
|
+
const TRACE_STATE_LOADED = 'loaded';
|
|
1709
|
+
/**
|
|
1710
|
+
* Normalise the caller's per-block list.
|
|
1711
|
+
*
|
|
1712
|
+
* FAIL-CLOSED at the edge: an entry that is not an object still becomes a
|
|
1713
|
+
* record — one whose `state` is null and therefore is not `loaded`. Dropping it
|
|
1714
|
+
* instead would let a junk entry beside a loaded one read as a loaded phase,
|
|
1715
|
+
* which is the multi-block fail-open T-6-47 names.
|
|
1716
|
+
*/
|
|
1717
|
+
function asTraceBlocks(v) {
|
|
1718
|
+
if (!Array.isArray(v))
|
|
1719
|
+
return [];
|
|
1720
|
+
const out = [];
|
|
1721
|
+
for (const entry of v) {
|
|
1722
|
+
const b = (entry !== null && typeof entry === 'object' ? entry : {});
|
|
1723
|
+
const source = typeof b.source === 'string' && b.source !== '' ? b.source : null;
|
|
1724
|
+
out.push({
|
|
1725
|
+
source,
|
|
1726
|
+
state: typeof b.state === 'string' && b.state !== '' ? b.state : null,
|
|
1727
|
+
reason: typeof b.reason === 'string' && b.reason !== '' ? b.reason : null,
|
|
1728
|
+
override: asOverrideRecord(b.override, source),
|
|
1729
|
+
});
|
|
1730
|
+
}
|
|
1731
|
+
return out;
|
|
1732
|
+
}
|
|
1733
|
+
/**
|
|
1734
|
+
* Normalise ONE block's override record, or answer null (08-05, D-08-11).
|
|
1735
|
+
*
|
|
1736
|
+
* Null unless the value is a plain object: an array, a string or a number is
|
|
1737
|
+
* not a record, and treating one as though it were would put a fabricated
|
|
1738
|
+
* bypass in front of a reader. A record whose fields are ALL missing still
|
|
1739
|
+
* survives as an entry — the artifact recorded an override, and "recorded, and
|
|
1740
|
+
* said nothing about it" is a different finding from "no override", which is
|
|
1741
|
+
* exactly the distinction D-06-14 exists to keep visible.
|
|
1742
|
+
*
|
|
1743
|
+
* EVERY field passes `safe()` here rather than at the point of use, including
|
|
1744
|
+
* `source`, because all six reach both `statements[]` and an agent prompt. That
|
|
1745
|
+
* is the discipline `describeDegradedTrace` already applies to the same
|
|
1746
|
+
* artifact's other fields (T-6-44 / T-6-46, and T-08-26 for this one): a trace
|
|
1747
|
+
* artifact is agent- or hand-editable DATA.
|
|
1748
|
+
*/
|
|
1749
|
+
function asOverrideRecord(v, source) {
|
|
1750
|
+
if (v === null || typeof v !== 'object' || Array.isArray(v))
|
|
1751
|
+
return null;
|
|
1752
|
+
const o = v;
|
|
1753
|
+
const field = (x) => (typeof x === 'string' && x !== '' ? safe(x) : null);
|
|
1754
|
+
return {
|
|
1755
|
+
source: source === null ? null : safe(source),
|
|
1756
|
+
gate: field(o.gate),
|
|
1757
|
+
reason: field(o.reason),
|
|
1758
|
+
suppressedReason: field(o.suppressedReason),
|
|
1759
|
+
who: field(o.who),
|
|
1760
|
+
when: field(o.when),
|
|
1761
|
+
};
|
|
1762
|
+
}
|
|
1763
|
+
/**
|
|
1764
|
+
* Name the FIRST block that is not `loaded`, and say what it recorded.
|
|
1765
|
+
*
|
|
1766
|
+
* The value is a labelled fact for the reviewer, never a routing signal: the
|
|
1767
|
+
* routing is `traceLoaded`, computed from the same blocks. Every fragment that
|
|
1768
|
+
* originates in a trace artifact — the source path and the recorded reason
|
|
1769
|
+
* alike — passes through `safe()` before it reaches the returned string, exactly
|
|
1770
|
+
* as `sources` and `pinSha` already do (T-6-44, T-6-46). A trace artifact is
|
|
1771
|
+
* agent- or hand-editable data, and this string crosses into an agent prompt.
|
|
1772
|
+
*/
|
|
1773
|
+
function describeDegradedTrace(blocks) {
|
|
1774
|
+
const offending = blocks.find((b) => b.state !== TRACE_STATE_LOADED);
|
|
1775
|
+
if (offending === undefined) {
|
|
1776
|
+
return ('no per-block provenance was carried with this trace, so nothing here establishes that the vault ' +
|
|
1777
|
+
'was reached when this phase was planned');
|
|
1778
|
+
}
|
|
1779
|
+
const source = offending.source === null ? 'an unnamed trace artifact' : safe(offending.source);
|
|
1780
|
+
const state = offending.state;
|
|
1781
|
+
const reason = offending.reason === null ? null : safe(offending.reason);
|
|
1782
|
+
if (state === null) {
|
|
1783
|
+
return `${source}: records no \`predicate.vault.state\` at all, so it cannot record that the vault loaded`;
|
|
1784
|
+
}
|
|
1785
|
+
if (state === 'degraded') {
|
|
1786
|
+
return `${source}: ${reason ?? 'no reason recorded'}`;
|
|
1787
|
+
}
|
|
1788
|
+
return (`${source}: records state "${safe(state)}", which is not a state this tier recognises` +
|
|
1789
|
+
(reason === null ? '' : `; its recorded reason is ${reason}`));
|
|
1790
|
+
}
|
|
1791
|
+
function buildReviewPayload(opts) {
|
|
1792
|
+
const statements = [];
|
|
1793
|
+
const notes = [];
|
|
1794
|
+
const trace = opts?.trace ?? null;
|
|
1795
|
+
const tracePresent = trace?.present === true;
|
|
1796
|
+
// ── The provenance, decided ONCE and here (06-12, G1). `present` answers "did
|
|
1797
|
+
// a block parse"; `traceLoaded` answers the different question the workflow's
|
|
1798
|
+
// §2 actually routes on: "was the vault reached when this phase was planned".
|
|
1799
|
+
// FAIL-CLOSED, deliberately (T-6-43, T-6-47): loaded requires strict equality
|
|
1800
|
+
// with `loaded` on EVERY parsed block, so one degraded block among loaded ones
|
|
1801
|
+
// is not a loaded phase, an unrecognised value is not a loaded one, and a
|
|
1802
|
+
// `present` trace carrying no per-block provenance at all is not one either —
|
|
1803
|
+
// an absence of a record is not a record of success.
|
|
1804
|
+
const traceBlocks = asTraceBlocks(trace?.blocks);
|
|
1805
|
+
const traceLoaded = tracePresent && traceBlocks.length > 0 && traceBlocks.every((b) => b.state === TRACE_STATE_LOADED);
|
|
1806
|
+
const traceState = !tracePresent
|
|
1807
|
+
? null
|
|
1808
|
+
: traceLoaded
|
|
1809
|
+
? TRACE_STATE_LOADED
|
|
1810
|
+
: 'degraded';
|
|
1811
|
+
const traceReason = traceState === 'degraded' ? describeDegradedTrace(traceBlocks) : null;
|
|
1812
|
+
const ledgerIds = asStringList(trace?.ledgerIds);
|
|
1813
|
+
const phaseDir = typeof opts?.phaseDir === 'string' && opts.phaseDir !== '' ? safe(opts.phaseDir) : null;
|
|
1814
|
+
// ── The range, first: every derivation below is scoped by it.
|
|
1815
|
+
const range = resolveReviewRange({
|
|
1816
|
+
cwd: opts.cwd,
|
|
1817
|
+
phaseToken: opts?.phaseToken ?? null,
|
|
1818
|
+
// UAT-6-F1: the check router already supplies `phaseDir: phase.rel`, so
|
|
1819
|
+
// forwarding it is all the shared phase-scoped resolution needs.
|
|
1820
|
+
phaseDir: typeof opts?.phaseDir === 'string' && opts.phaseDir !== '' ? opts.phaseDir : null,
|
|
1821
|
+
...(typeof opts?.exec === 'function' ? { exec: opts.exec } : {}),
|
|
1822
|
+
});
|
|
1823
|
+
statements.push(`This review is computed over the git range "${range.range}" (rangeSource: ${range.rangeSource}, ` +
|
|
1824
|
+
`base: ${safe(range.base)}), which considered ${range.filesConsidered} changed file(s). Every ` +
|
|
1825
|
+
'applicability judgement below is scoped to exactly that set.');
|
|
1826
|
+
for (const note of range.notes)
|
|
1827
|
+
statements.push(`Range note: ${note}`);
|
|
1828
|
+
// ── The vault verdict owner's answer, stated when it is not `loaded`: the
|
|
1829
|
+
// practices below were read from the cache as it stands, and a reader must
|
|
1830
|
+
// know whether the load gate considers that cache the client's loaded vault.
|
|
1831
|
+
const ownerVerdict = opts?.vaultVerdict ?? null;
|
|
1832
|
+
const vault = ownerVerdict !== null && typeof ownerVerdict === 'object' && typeof ownerVerdict.verdict === 'string'
|
|
1833
|
+
? { verdict: safe(ownerVerdict.verdict), reason: typeof ownerVerdict.reason === 'string' ? safe(ownerVerdict.reason) : '' }
|
|
1834
|
+
: null;
|
|
1835
|
+
if (vault !== null && vault.verdict !== 'loaded') {
|
|
1836
|
+
statements.push(`The vault verdict owner reports this project's vault as ${vault.verdict}` +
|
|
1837
|
+
(vault.reason === '' ? '' : ` (${vault.reason})`) +
|
|
1838
|
+
': the practices reviewed below were read from the local cache as it stands, which the vault load ' +
|
|
1839
|
+
"gate does not treat as the client's loaded vault.");
|
|
1840
|
+
}
|
|
1841
|
+
// ── D-06-15: an absent trace does not refuse; it says so and RECONSTRUCTS.
|
|
1842
|
+
if (!tracePresent) {
|
|
1843
|
+
statements.push('No practice trace was found for this phase' +
|
|
1844
|
+
(phaseDir === null ? '' : ` in ${phaseDir}`) +
|
|
1845
|
+
'. This review therefore took the RECONSTRUCT arm: the applicable practice set is rebuilt ' +
|
|
1846
|
+
"from this phase's real diff and the vault's own practice list rather than from a ledger. " +
|
|
1847
|
+
'Any completed phase is eligible for review and a missing trace is itself a finding, ' +
|
|
1848
|
+
'so it is recorded here rather than used as a reason to refuse.');
|
|
1849
|
+
}
|
|
1850
|
+
// ── G1: a trace that PARSES but records a failed vault load. This is not the
|
|
1851
|
+
// absent case and must never be reported as one — a found-and-failed trace is
|
|
1852
|
+
// a finding, and calling it an absence loses it entirely.
|
|
1853
|
+
if (tracePresent && !traceLoaded) {
|
|
1854
|
+
statements.push(
|
|
1855
|
+
// The source list is NOT repeated here: `traceReason` already opens with
|
|
1856
|
+
// the artifact path of the block that failed, which is the one a reader
|
|
1857
|
+
// needs. Naming the whole list beside it printed the same path three times
|
|
1858
|
+
// in one sentence.
|
|
1859
|
+
'A practice trace WAS found for this phase' +
|
|
1860
|
+
(phaseDir === null ? '' : ` in ${phaseDir}`) +
|
|
1861
|
+
`, and it records a FAILURE — ${traceReason ?? 'no reason recorded'}. A degraded block is a valid ` +
|
|
1862
|
+
'record of a failure — it says the vault could not be loaded when the work was planned — so it ' +
|
|
1863
|
+
'proves nothing about which practices governed that work and may not stand in for a loaded trace. ' +
|
|
1864
|
+
'This review therefore took the RECONSTRUCT arm: the applicable practice set is rebuilt ' +
|
|
1865
|
+
"from this phase's real diff and the vault's own practice list, and this trace's own ledger and " +
|
|
1866
|
+
'practice universe are deliberately not consulted.');
|
|
1867
|
+
}
|
|
1868
|
+
// ── D-08-09 / D-08-11: every RECORDED gate override, stated. `check
|
|
1869
|
+
// vault.trace` reports the same records under `checked_with_override`, so a
|
|
1870
|
+
// review that stayed silent about one would be the second surface disagreeing
|
|
1871
|
+
// with the first about whether a phase was overridden — and the silent one is
|
|
1872
|
+
// the one a human reads. Collected from EVERY parsed block, not keyed on
|
|
1873
|
+
// `traceLoaded`: an override is recordable whenever a gate blocked, which is
|
|
1874
|
+
// independent of whether the vault itself loaded, and a degraded block's
|
|
1875
|
+
// override is exactly the one a reader must not lose.
|
|
1876
|
+
const overrides = traceBlocks
|
|
1877
|
+
.map((b) => b.override)
|
|
1878
|
+
.filter((o) => o !== null);
|
|
1879
|
+
for (const o of overrides) {
|
|
1880
|
+
const said = (v) => v ?? '(not recorded)';
|
|
1881
|
+
statements.push(`A vault-gate OVERRIDE is recorded in ${said(o.source)}: gate ${said(o.gate)} was overridden by ` +
|
|
1882
|
+
`${said(o.who)} at ${said(o.when)} with the stated reason "${said(o.reason)}", suppressing the ` +
|
|
1883
|
+
`verdict ${said(o.suppressedReason)}. The work this phase records was therefore planned or ` +
|
|
1884
|
+
'executed under a deliberate, attributed bypass of that gate; this review reports the fact ' +
|
|
1885
|
+
'and does not judge it.');
|
|
1886
|
+
}
|
|
1887
|
+
// ── The input set: this arm's universe ∩ NO_CHECK, harvested through the
|
|
1888
|
+
// shipped ladder. On the reconstruct arm the universe is the vault's own
|
|
1889
|
+
// practice list (D3): the ledger is empty there by definition, and deriving
|
|
1890
|
+
// from it is what let an untraced phase render a clean review having judged
|
|
1891
|
+
// nothing. Crossing that universe with this range's real diff is Arm B's job
|
|
1892
|
+
// below, through the shipped matcher — not a second one written here.
|
|
1893
|
+
//
|
|
1894
|
+
// KEYED ON `traceLoaded`, NOT ON `tracePresent` (06-12, G1). A degraded block
|
|
1895
|
+
// carries a ledger and a practice universe of its own, and both are worthless:
|
|
1896
|
+
// docs/vault-trace-format.md §"What a degraded record proves" says an empty
|
|
1897
|
+
// ledger and a complete one are indistinguishable through it. Feeding them to
|
|
1898
|
+
// the derivation while the statement above announces a reconstruction from the
|
|
1899
|
+
// vault's practice list would make that statement false — which is the whole
|
|
1900
|
+
// reason this is more than a label change.
|
|
1901
|
+
const inputSet = deriveCandidateSet({
|
|
1902
|
+
ledgerIds: traceLoaded ? ledgerIds : [],
|
|
1903
|
+
reconstruct: !traceLoaded,
|
|
1904
|
+
practices: [...(opts?.practices ?? [])],
|
|
1905
|
+
skipped: opts?.skipped ?? [],
|
|
1906
|
+
degraded: opts?.degraded ?? [],
|
|
1907
|
+
tracePinSha: trace?.pinSha ?? null,
|
|
1908
|
+
cacheSha: opts?.cacheSha ?? null,
|
|
1909
|
+
...(opts?._audit ? { _audit: opts._audit } : {}),
|
|
1910
|
+
});
|
|
1911
|
+
for (const note of inputSet.notes)
|
|
1912
|
+
statements.push(`Input set: ${note}`);
|
|
1913
|
+
// ── Both derived-N-A arms, kept as two labels that never blur.
|
|
1914
|
+
// Arm A consumes the BLOCK's own recorded universe, so it is keyed on
|
|
1915
|
+
// `traceLoaded` for the same reason the derivation above is: a degraded
|
|
1916
|
+
// block's universe is not a universe anyone may report against.
|
|
1917
|
+
const naUniverse = deriveNaAUniverse({
|
|
1918
|
+
universe: traceLoaded ? (trace?.universe ?? null) : null,
|
|
1919
|
+
ledgerIds: traceLoaded ? ledgerIds : [],
|
|
1920
|
+
// Only true when a universe EXISTED and this call suppressed it. An absent
|
|
1921
|
+
// trace, and a degraded one that recorded no universe, both stay on the
|
|
1922
|
+
// absent ground — whose sentence is true of them (06-12 Task 5 follow-up).
|
|
1923
|
+
universeWithheld: !traceLoaded && (trace?.universe ?? null) !== null,
|
|
1924
|
+
});
|
|
1925
|
+
const naNoFile = deriveNaANoFile({
|
|
1926
|
+
candidates: inputSet.candidates,
|
|
1927
|
+
practices: opts?.practices ?? [],
|
|
1928
|
+
diffFiles: range.files,
|
|
1929
|
+
discovery: opts?.discovery ?? null,
|
|
1930
|
+
...(opts?._practices ? { _practices: opts._practices } : {}),
|
|
1931
|
+
});
|
|
1932
|
+
for (const arm of [naUniverse, naNoFile]) {
|
|
1933
|
+
if (arm.notDerived)
|
|
1934
|
+
statements.push(arm.notDerived.statement);
|
|
1935
|
+
for (const note of arm.notes)
|
|
1936
|
+
notes.push(note);
|
|
1937
|
+
}
|
|
1938
|
+
// ── The clean-render refusal, drawn LAST because it is the conclusion of
|
|
1939
|
+
// everything above: the range's file count AND whether the candidate
|
|
1940
|
+
// derivation was measured at all (D3). The predicate is the shipped one and
|
|
1941
|
+
// the grounds it names are the ones the derivation recorded — this function
|
|
1942
|
+
// restates neither.
|
|
1943
|
+
const refusalInput = { filesConsidered: range.filesConsidered, inputSet, naUniverse, naNoFile };
|
|
1944
|
+
const refusesCleanRender = reviewRefusesCleanRender(refusalInput);
|
|
1945
|
+
if (reviewRefusesCleanRender(range)) {
|
|
1946
|
+
statements.push('This review considered ZERO files, so no clean review may be rendered from it. A review that ' +
|
|
1947
|
+
'looked at nothing and reported cleanly is the rubber stamp this tier exists to prevent; the ' +
|
|
1948
|
+
'refusal is the shipped predicate\'s, not a restatement of it.');
|
|
1949
|
+
}
|
|
1950
|
+
// ── D-08-17, the review half: the refusal above says a review looked at
|
|
1951
|
+
// nothing. THIS one says WHY, by the code the blocking tier already reports,
|
|
1952
|
+
// and hands back both remedies. The two sentences stay side by side because
|
|
1953
|
+
// they are different facts — "no clean render may follow" and "the range
|
|
1954
|
+
// resolved nothing about this phase" — and an operator who is told only the
|
|
1955
|
+
// first has been told their review is worthless and nothing else.
|
|
1956
|
+
if (range.outcome === auditMod.AUDIT_RANGE_OUTCOME.RANGE_UNRESOLVABLE) {
|
|
1957
|
+
const sharedGround = range.notes.find((n) => n.startsWith(SHARED_RESOLUTION_NOTE_PREFIX));
|
|
1958
|
+
statements.push(`Range outcome ${range.outcome}: the range fell to the base-branch span (${range.range}) and ` +
|
|
1959
|
+
'considered zero files, so nothing was established about this phase — ' +
|
|
1960
|
+
`${sharedGround ?? 'no phase manifest entry and no SUMMARY trace digest resolved.'} ` +
|
|
1961
|
+
REVIEW_RANGE_UNRESOLVABLE_REMEDY);
|
|
1962
|
+
}
|
|
1963
|
+
const unmeasured = unmeasuredGrounds(refusalInput);
|
|
1964
|
+
if (unmeasured.length > 0) {
|
|
1965
|
+
statements.push(`This review could not MEASURE the inputs its candidate set is derived from (${unmeasured.join(', ')}), ` +
|
|
1966
|
+
'so no clean review may be rendered from it. An empty candidate set nobody measured is a different ' +
|
|
1967
|
+
'finding from one that was measured and held nothing, and rendering the first as though it were the ' +
|
|
1968
|
+
'second is exactly the silence this tier exists to end.');
|
|
1969
|
+
}
|
|
1970
|
+
const partition = partitionForJudgement({
|
|
1971
|
+
candidates: inputSet.candidates,
|
|
1972
|
+
naUniverse,
|
|
1973
|
+
naNoFile,
|
|
1974
|
+
});
|
|
1975
|
+
const derivedCounts = countReviewVerdicts([...naUniverse.rows, ...naNoFile.rows]);
|
|
1976
|
+
const derivedRateLine = renderRateLine(derivedCounts);
|
|
1977
|
+
// ── MECH-02 / D-10-03: the MECHANICAL tier's coverage over THIS review's own
|
|
1978
|
+
// file set, joining the two derived facts above. Gated on the SHIPPED
|
|
1979
|
+
// `unmeasuredGrounds` answer already computed above rather than on a second
|
|
1980
|
+
// reading of the same inputs: where that predicate names a ground, nothing
|
|
1981
|
+
// was measured, and a `0 of 0` line would state a measurement nobody took.
|
|
1982
|
+
const mechanicalCoverageLine = unmeasured.length > 0
|
|
1983
|
+
? null
|
|
1984
|
+
: deriveMechanicalCoverageLine({
|
|
1985
|
+
practices: opts?.practices ?? [],
|
|
1986
|
+
files: range.files,
|
|
1987
|
+
cwd: opts?.cwd,
|
|
1988
|
+
...(opts?._audit ? { _audit: opts._audit } : {}),
|
|
1989
|
+
});
|
|
1990
|
+
// ── The remainder's prose, rendered through the SHIPPED per-practice renderer.
|
|
1991
|
+
// Not a renderer of this module's own: a second one is a second place a
|
|
1992
|
+
// client-authored byte can reach an agent context unfenced (CR-02).
|
|
1993
|
+
const practiceEntries = [];
|
|
1994
|
+
const renderEntry = typeof opts?.renderEntry === 'function' ? opts.renderEntry : null;
|
|
1995
|
+
const byId = new Map();
|
|
1996
|
+
const entries = typeof opts?.matchEntries === 'function' ? opts.matchEntries(range.files) : (opts?.entries ?? []);
|
|
1997
|
+
for (const entry of entries) {
|
|
1998
|
+
if (entry && typeof entry.id === 'string')
|
|
1999
|
+
byId.set(entry.id, entry);
|
|
2000
|
+
}
|
|
2001
|
+
for (const id of partition.remainder) {
|
|
2002
|
+
const entry = byId.get(id);
|
|
2003
|
+
if (entry === undefined) {
|
|
2004
|
+
notes.push(`"${quotableId(id)}" reaches the agent as a remainder id with no matcher entry, so no fenced ` +
|
|
2005
|
+
'prose is supplied for it. The id is reported rather than dropped.');
|
|
2006
|
+
continue;
|
|
2007
|
+
}
|
|
2008
|
+
if (renderEntry === null) {
|
|
2009
|
+
notes.push('no practice renderer was supplied, so no fenced practice prose is emitted at all. Vault prose ' +
|
|
2010
|
+
'is never emitted unfenced, so the honest answer is to emit none.');
|
|
2011
|
+
break;
|
|
2012
|
+
}
|
|
2013
|
+
practiceEntries.push(renderEntry(entry));
|
|
2014
|
+
}
|
|
2015
|
+
return {
|
|
2016
|
+
status: 'reviewed',
|
|
2017
|
+
// ONE routing decision, read from ONE field. `arm` is derived from
|
|
2018
|
+
// `traceLoaded` and so is `trace.loaded`, which is why the workflow reads
|
|
2019
|
+
// the latter and never re-derives it from the former (06-12, T-6-48).
|
|
2020
|
+
arm: traceLoaded ? 'traced' : 'reconstruct',
|
|
2021
|
+
phaseDir,
|
|
2022
|
+
vault,
|
|
2023
|
+
statements,
|
|
2024
|
+
trace: {
|
|
2025
|
+
present: tracePresent,
|
|
2026
|
+
sources: asStringList(trace?.sources).map((s) => safe(s)),
|
|
2027
|
+
pinSha: typeof trace?.pinSha === 'string' ? safe(trace.pinSha) : null,
|
|
2028
|
+
state: traceState,
|
|
2029
|
+
reason: traceReason,
|
|
2030
|
+
loaded: traceLoaded,
|
|
2031
|
+
overrides,
|
|
2032
|
+
},
|
|
2033
|
+
range,
|
|
2034
|
+
refusesCleanRender,
|
|
2035
|
+
inputSet,
|
|
2036
|
+
naUniverse,
|
|
2037
|
+
naNoFile,
|
|
2038
|
+
partition,
|
|
2039
|
+
derivedCounts,
|
|
2040
|
+
derivedRateLine,
|
|
2041
|
+
// OMITTED, not zeroed, on the unmeasured path — see the field's comment.
|
|
2042
|
+
...(mechanicalCoverageLine === null ? {} : { mechanicalCoverageLine }),
|
|
2043
|
+
practiceEntries,
|
|
2044
|
+
notes,
|
|
2045
|
+
};
|
|
2046
|
+
}
|
|
2047
|
+
// ─── The artifact the agent fills in, and the lines only code writes ─────────
|
|
2048
|
+
/**
|
|
2049
|
+
* The verdict cell of a remainder row until the agent judges it. Deliberately
|
|
2050
|
+
* outside the locked vocabulary: a row left unjudged is reported by the
|
|
2051
|
+
* validator as `row-unknown-verdict`, never counted as any verdict.
|
|
2052
|
+
*/
|
|
2053
|
+
const REVIEW_UNJUDGED_VERDICT = 'TODO';
|
|
2054
|
+
/** The `## Range` heading the coverage line is stamped under. */
|
|
2055
|
+
const RANGE_SECTION_HEADING = '## Range';
|
|
2056
|
+
/** A table cell: one line, no column separator, never empty. */
|
|
2057
|
+
function tableCell(v, empty = 'none.') {
|
|
2058
|
+
const s = (typeof v === 'string' ? v : '').replace(/[\r\n]+/g, ' ').replace(/\|/g, '¦').trim();
|
|
2059
|
+
return s === '' ? empty : s;
|
|
2060
|
+
}
|
|
2061
|
+
/**
|
|
2062
|
+
* The gaps the payload recorded, one row each, in a fixed order. Every row is
|
|
2063
|
+
* written on every run; a row with nothing to report says `none.`, so "nothing
|
|
2064
|
+
* to report" and "not looked at" never look alike.
|
|
2065
|
+
*/
|
|
2066
|
+
function reviewGapRows(p) {
|
|
2067
|
+
const notDerived = (arm) => (arm.notDerived ? arm.notDerived.statement : '');
|
|
2068
|
+
const vaultGround = p.naNoFile.notDerived?.ground ?? '';
|
|
2069
|
+
const vaultGap = [
|
|
2070
|
+
NOT_DERIVED_GROUND.VAULT_STATE_UNKNOWN,
|
|
2071
|
+
NOT_DERIVED_GROUND.VAULT_NEVER_FETCHED,
|
|
2072
|
+
NOT_DERIVED_GROUND.VAULT_FETCH_MISS,
|
|
2073
|
+
NOT_DERIVED_GROUND.VAULT_UNREADABLE,
|
|
2074
|
+
].some((g) => g === vaultGround)
|
|
2075
|
+
? notDerived(p.naNoFile)
|
|
2076
|
+
: '';
|
|
2077
|
+
const traceGap = !p.trace.present
|
|
2078
|
+
? 'absent — no practice trace was found, so this review took the reconstruct arm'
|
|
2079
|
+
: p.trace.loaded
|
|
2080
|
+
? ''
|
|
2081
|
+
: `degraded — ${p.trace.reason ?? 'no reason recorded'}`;
|
|
2082
|
+
const said = (v) => v ?? '(not recorded)';
|
|
2083
|
+
const overrides = p.trace.overrides
|
|
2084
|
+
.map((o) => `${said(o.gate)} overridden by ${said(o.who)} at ${said(o.when)} in ${said(o.source)}: "${said(o.reason)}" (suppressed ${said(o.suppressedReason)})`)
|
|
2085
|
+
.join('; ');
|
|
2086
|
+
const rangeGap = p.range.outcome === auditMod.AUDIT_RANGE_OUTCOME.RANGE_UNRESOLVABLE
|
|
2087
|
+
? `${p.range.outcome}: ${REVIEW_RANGE_UNRESOLVABLE_REMEDY}`
|
|
2088
|
+
: '';
|
|
2089
|
+
const skipped = p.inputSet.exclusions
|
|
2090
|
+
.filter((e) => e.reason === AUDIT_FAIL_REASON.PRACTICE_UNLOADABLE)
|
|
2091
|
+
.map((e) => quotableId(e.id))
|
|
2092
|
+
.join(', ');
|
|
2093
|
+
const unmeasured = unmeasuredGrounds({
|
|
2094
|
+
filesConsidered: p.range.filesConsidered,
|
|
2095
|
+
inputSet: p.inputSet,
|
|
2096
|
+
naUniverse: p.naUniverse,
|
|
2097
|
+
naNoFile: p.naNoFile,
|
|
2098
|
+
});
|
|
2099
|
+
const refused = p.refusesCleanRender
|
|
2100
|
+
? `no clean review may be rendered: ${p.range.filesConsidered === 0 ? 'zero files were considered' : `inputs not measured (${unmeasured.join(', ')})`}`
|
|
2101
|
+
: '';
|
|
2102
|
+
return [
|
|
2103
|
+
['Vault absent, never fetched, missed or unreadable', vaultGap],
|
|
2104
|
+
['Practice universe not recorded at the pin', notDerived(p.naUniverse)],
|
|
2105
|
+
['Trace absent or degraded', traceGap],
|
|
2106
|
+
['Gate override recorded (who, when, reason)', overrides],
|
|
2107
|
+
['Range refused or unresolvable', rangeGap],
|
|
2108
|
+
['Empty diff — no file considered', p.range.filesConsidered === 0 ? 'zero files were considered' : ''],
|
|
2109
|
+
['Practices skipped or rejected before matching', skipped],
|
|
2110
|
+
['Clean review refused', refused],
|
|
2111
|
+
];
|
|
2112
|
+
}
|
|
2113
|
+
/**
|
|
2114
|
+
* Render the `NN-VAULT-REVIEW.md` the agent fills in, from the payload alone.
|
|
2115
|
+
*
|
|
2116
|
+
* Everything computed is written here, by code: provenance, the range, every
|
|
2117
|
+
* derived row with its reason and arm label, every gap row, the payload's
|
|
2118
|
+
* statements, the fixed limits, and — through `stampReviewLines` — the rate,
|
|
2119
|
+
* counts and coverage lines. Each remainder practice gets one row whose verdict
|
|
2120
|
+
* reads `TODO` and whose reason is empty: those two cells are the agent's whole
|
|
2121
|
+
* writable surface. The remainder's practice text follows, last, inside the
|
|
2122
|
+
* fences the shipped renderer already put around it.
|
|
2123
|
+
*
|
|
2124
|
+
* Pure: payload and date in, text out.
|
|
2125
|
+
*/
|
|
2126
|
+
function renderReviewScaffold(payload, meta = {}) {
|
|
2127
|
+
const p = payload;
|
|
2128
|
+
const token = typeof meta.phaseToken === 'string' && meta.phaseToken !== '' ? safe(meta.phaseToken) : '?';
|
|
2129
|
+
const date = typeof meta.date === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(meta.date) ? meta.date : '';
|
|
2130
|
+
const traceCell = !p.trace.present
|
|
2131
|
+
? 'absent'
|
|
2132
|
+
: p.trace.loaded
|
|
2133
|
+
? 'present and loaded'
|
|
2134
|
+
: `degraded — ${p.trace.reason ?? 'no reason recorded'}`;
|
|
2135
|
+
const advisories = p.inputSet.advisories.map((a) => a.detail).join(' ');
|
|
2136
|
+
const rows = [];
|
|
2137
|
+
for (const id of p.partition.remainder) {
|
|
2138
|
+
rows.push(`| \`${tableCell(quotableId(id))}\` | ${REVIEW_UNJUDGED_VERDICT} | | agent-judged |`);
|
|
2139
|
+
}
|
|
2140
|
+
for (const r of [...p.partition.naUniverse, ...p.partition.naNoFile]) {
|
|
2141
|
+
rows.push(`| \`${tableCell(quotableId(r.id))}\` | ${r.verdict} | ${tableCell(r.reason)} | derived (${tableCell(r.naDerivedReason ?? '')}) |`);
|
|
2142
|
+
}
|
|
2143
|
+
const lines = [
|
|
2144
|
+
'---',
|
|
2145
|
+
`phase: "${token}"`,
|
|
2146
|
+
'artifact: vault-review',
|
|
2147
|
+
`reviewed: ${date === '' ? 'unknown' : date}`,
|
|
2148
|
+
`arm: ${p.arm}`,
|
|
2149
|
+
'---',
|
|
2150
|
+
'',
|
|
2151
|
+
`# Phase ${token} — Vault Practice Review`,
|
|
2152
|
+
'',
|
|
2153
|
+
'> This review reports and never gates: no verdict here changes a phase status, a score, a gap or a',
|
|
2154
|
+
'> ship. Verdicts are agent-judged, not measured, and two runs may differ. Counts by verdict only.',
|
|
2155
|
+
'',
|
|
2156
|
+
'## Provenance',
|
|
2157
|
+
'',
|
|
2158
|
+
'| Field | Value |',
|
|
2159
|
+
'| --- | --- |',
|
|
2160
|
+
`| Phase directory | ${tableCell(p.phaseDir ?? '')} |`,
|
|
2161
|
+
`| Arm | ${p.arm} |`,
|
|
2162
|
+
`| Trace | ${tableCell(traceCell)} |`,
|
|
2163
|
+
`| Trace sources | ${tableCell(p.trace.sources.join(', '))} |`,
|
|
2164
|
+
`| Vault verdict | ${tableCell(p.vault ? `${p.vault.verdict}${p.vault.reason ? ` (${p.vault.reason})` : ''}` : '', 'not supplied')} |`,
|
|
2165
|
+
`| Pin advisory | ${tableCell(advisories)} |`,
|
|
2166
|
+
'',
|
|
2167
|
+
RANGE_SECTION_HEADING,
|
|
2168
|
+
'',
|
|
2169
|
+
'| Field | Value |',
|
|
2170
|
+
'| --- | --- |',
|
|
2171
|
+
`| \`range\` | \`${tableCell(p.range.range)}\` |`,
|
|
2172
|
+
`| \`rangeSource\` | \`${tableCell(p.range.rangeSource)}\` |`,
|
|
2173
|
+
`| \`filesConsidered\` | ${p.range.filesConsidered} |`,
|
|
2174
|
+
`| \`outcome\` | \`${tableCell(p.range.outcome)}\` |`,
|
|
2175
|
+
'',
|
|
2176
|
+
REVIEW_SECTION_HEADING,
|
|
2177
|
+
'',
|
|
2178
|
+
'| Practice | Verdict | Reason / evidence | Source |',
|
|
2179
|
+
'| --- | --- | --- | --- |',
|
|
2180
|
+
...rows,
|
|
2181
|
+
'',
|
|
2182
|
+
'## Not Derived — the gaps',
|
|
2183
|
+
'',
|
|
2184
|
+
'| Gap | Statement |',
|
|
2185
|
+
'| --- | --- |',
|
|
2186
|
+
...reviewGapRows(p).map(([gap, statement]) => `| ${gap} | ${tableCell(statement)} |`),
|
|
2187
|
+
'',
|
|
2188
|
+
'## Derivation Statements',
|
|
2189
|
+
'',
|
|
2190
|
+
...(p.statements.length + p.notes.length === 0
|
|
2191
|
+
? ['none.']
|
|
2192
|
+
: [...p.statements.map((st) => `- ${st}`), ...p.notes.map((n) => `- Note: ${n}`)]),
|
|
2193
|
+
'',
|
|
2194
|
+
'## Limits of This Review',
|
|
2195
|
+
'',
|
|
2196
|
+
'- Nothing in this artifact gates anything. A `FAIL` is a report to a human, not a halt.',
|
|
2197
|
+
'- A verdict is one agent run\'s judgement; reproducibility is not claimed.',
|
|
2198
|
+
'- The non-verdict rate is reported, not judged: there is no threshold at which a review is declared',
|
|
2199
|
+
' low-confidence.',
|
|
2200
|
+
'- A practice absent from the register was not in the input set; the gaps table says why. Absence',
|
|
2201
|
+
' is not a statement that it was followed.',
|
|
2202
|
+
'',
|
|
2203
|
+
'## Practice Text (client reference data)',
|
|
2204
|
+
'',
|
|
2205
|
+
...(p.practiceEntries.length === 0 ? ['none.'] : p.practiceEntries.flatMap((e) => [e, ''])),
|
|
2206
|
+
'',
|
|
2207
|
+
];
|
|
2208
|
+
return stampReviewLines(lines.join('\n'), { coverageLine: p.mechanicalCoverageLine ?? null }).text;
|
|
2209
|
+
}
|
|
2210
|
+
/** Index of the first unfenced line equal to `heading`, or -1. */
|
|
2211
|
+
function unfencedHeadingIndex(lines, heading) {
|
|
2212
|
+
const fenced = fencedLineMap(lines);
|
|
2213
|
+
return lines.findIndex((l, i) => !fenced[i] && l.trim() === heading);
|
|
2214
|
+
}
|
|
2215
|
+
/**
|
|
2216
|
+
* Write the three computed lines into an artifact — the ONLY writer of them.
|
|
2217
|
+
*
|
|
2218
|
+
* Every unfenced rate- or counts-shaped line inside the verdicts section, and
|
|
2219
|
+
* every unfenced coverage-shaped line anywhere, is removed; then the rate and
|
|
2220
|
+
* counts lines the validator computes from the section's own rows are written
|
|
2221
|
+
* directly under the section heading, and the payload's coverage line (when it
|
|
2222
|
+
* carries one) at the end of `## Range` — or above the verdicts section when
|
|
2223
|
+
* there is no `## Range`. A narrated figure therefore cannot survive, and the
|
|
2224
|
+
* two count populations (the payload's derived rows and the register's rows)
|
|
2225
|
+
* cannot be confused by anyone: the register's lines are always the register's.
|
|
2226
|
+
*
|
|
2227
|
+
* Fenced client prose is never touched. Without the verdicts heading nothing is
|
|
2228
|
+
* stamped and the validator reports the absent section.
|
|
2229
|
+
*/
|
|
2230
|
+
function stampReviewLines(text, opts = {}) {
|
|
2231
|
+
if (typeof text !== 'string')
|
|
2232
|
+
return { text: '', stamped: false };
|
|
2233
|
+
const src = text.split(/\r?\n/);
|
|
2234
|
+
const section = locateReviewSection(src);
|
|
2235
|
+
if (!section.present)
|
|
2236
|
+
return { text, stamped: false };
|
|
2237
|
+
const fenced = fencedLineMap(src);
|
|
2238
|
+
const kept = [];
|
|
2239
|
+
for (let i = 0; i < src.length; i += 1) {
|
|
2240
|
+
const t = src[i].trim();
|
|
2241
|
+
if (!fenced[i]) {
|
|
2242
|
+
if (COVERAGE_LINE_SHAPE.test(t))
|
|
2243
|
+
continue;
|
|
2244
|
+
if (i >= section.start && i < section.end && (RATE_LINE_SHAPE.test(t) || COUNTS_LINE_SHAPE.test(t)))
|
|
2245
|
+
continue;
|
|
2246
|
+
}
|
|
2247
|
+
kept.push(src[i]);
|
|
2248
|
+
}
|
|
2249
|
+
const probe = validateReviewArtifact(kept.join('\n'));
|
|
2250
|
+
const head = unfencedHeadingIndex(kept, REVIEW_SECTION_HEADING);
|
|
2251
|
+
let after = head + 1;
|
|
2252
|
+
while (after < kept.length && kept[after].trim() === '')
|
|
2253
|
+
after += 1;
|
|
2254
|
+
kept.splice(head + 1, after - (head + 1), '', probe.expectedRateLine, probe.expectedCountsLine, '');
|
|
2255
|
+
const coverage = typeof opts.coverageLine === 'string' ? opts.coverageLine.trim() : '';
|
|
2256
|
+
if (coverage !== '' && COVERAGE_LINE_SHAPE.test(coverage)) {
|
|
2257
|
+
const fencedNow = fencedLineMap(kept);
|
|
2258
|
+
const range = unfencedHeadingIndex(kept, RANGE_SECTION_HEADING);
|
|
2259
|
+
let at = unfencedHeadingIndex(kept, REVIEW_SECTION_HEADING);
|
|
2260
|
+
if (range !== -1) {
|
|
2261
|
+
at = kept.length;
|
|
2262
|
+
for (let i = range + 1; i < kept.length; i += 1) {
|
|
2263
|
+
if (!fencedNow[i] && (kept[i].startsWith('## ') || kept[i].trim() === '---')) {
|
|
2264
|
+
at = i;
|
|
2265
|
+
break;
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
while (at > range + 1 && kept[at - 1].trim() === '')
|
|
2269
|
+
at -= 1;
|
|
2270
|
+
kept.splice(at, 0, '', coverage);
|
|
2271
|
+
}
|
|
2272
|
+
else {
|
|
2273
|
+
kept.splice(at, 0, coverage, '');
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
// Removing a line leaves its blank neighbours behind; collapse every unfenced
|
|
2277
|
+
// run of blank lines to one, so stamping is idempotent.
|
|
2278
|
+
const fencedOut = fencedLineMap(kept);
|
|
2279
|
+
const out = kept.filter((l, i) => fencedOut[i] || l.trim() !== '' || i === 0 || kept[i - 1].trim() !== '');
|
|
2280
|
+
return { text: out.join('\n'), stamped: true };
|
|
2281
|
+
}
|
|
2282
|
+
/**
|
|
2283
|
+
* The register's rows against the population the payload derived: every
|
|
2284
|
+
* practice of `partition.population` (remainder plus both derived arms) has
|
|
2285
|
+
* exactly one row, and no row names anything else. Deleting a row is how a
|
|
2286
|
+
* hard practice disappears from a review; this makes that a reported problem.
|
|
2287
|
+
*/
|
|
2288
|
+
function compareReviewPopulation(rows, partition) {
|
|
2289
|
+
const expected = uniqueInOrder([
|
|
2290
|
+
...partition.remainder,
|
|
2291
|
+
...partition.naUniverse.map((r) => r.id),
|
|
2292
|
+
...partition.naNoFile.map((r) => r.id),
|
|
2293
|
+
]);
|
|
2294
|
+
const want = new Set(expected.map((id) => quotableId(id)));
|
|
2295
|
+
const seen = new Map();
|
|
2296
|
+
for (const r of rows) {
|
|
2297
|
+
if (typeof r?.id !== 'string' || r.id === '')
|
|
2298
|
+
continue;
|
|
2299
|
+
seen.set(r.id, (seen.get(r.id) ?? 0) + 1);
|
|
2300
|
+
}
|
|
2301
|
+
const problems = [];
|
|
2302
|
+
for (const id of want) {
|
|
2303
|
+
if (!seen.has(id)) {
|
|
2304
|
+
problems.push(reviewProblem(REVIEW_PROBLEM_KIND.ROW_POPULATION_MISSING, id, null, `"${id}" is in the population this review derived and has no row: a practice handed to the review ` +
|
|
2305
|
+
'is judged or reproduced, never dropped.'));
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
for (const [id, n] of seen) {
|
|
2309
|
+
if (!want.has(id)) {
|
|
2310
|
+
problems.push(reviewProblem(REVIEW_PROBLEM_KIND.ROW_POPULATION_UNEXPECTED, id, null, `"${safe(id)}" has a row but is not in the population this review derived, so its verdict judges ` +
|
|
2311
|
+
'something the review was not handed.'));
|
|
2312
|
+
}
|
|
2313
|
+
if (n > 1) {
|
|
2314
|
+
problems.push(reviewProblem(REVIEW_PROBLEM_KIND.ROW_DUPLICATE, id, null, `"${safe(id)}" carries ${n} rows; each practice carries exactly one verdict.`));
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
return problems;
|
|
2318
|
+
}
|
|
2319
|
+
module.exports = {
|
|
2320
|
+
// The three frozen vocabularies. These ARE the contract 06-04/06-06/06-08 and
|
|
2321
|
+
// 06-10 serialize; nothing downstream re-derives a token.
|
|
2322
|
+
REVIEW_VERDICT,
|
|
2323
|
+
NA_DERIVED_REASON,
|
|
2324
|
+
REVIEW_RANGE_SOURCE,
|
|
2325
|
+
// The input set, harvested from the shipped mechanical tier.
|
|
2326
|
+
deriveCandidateSet,
|
|
2327
|
+
// The two derived-N-A arms, deliberately two functions and two labels.
|
|
2328
|
+
deriveNaAUniverse,
|
|
2329
|
+
deriveNaANoFile,
|
|
2330
|
+
// The split that decides what may reach the agent at all.
|
|
2331
|
+
partitionForJudgement,
|
|
2332
|
+
// The arithmetic. Computed, never narrated.
|
|
2333
|
+
countReviewVerdicts,
|
|
2334
|
+
renderRateLine,
|
|
2335
|
+
// The range, and the single refusal predicate that guards it.
|
|
2336
|
+
resolveReviewRange,
|
|
2337
|
+
reviewRefusesCleanRender,
|
|
2338
|
+
// 08-05 (D-08-17): the two remedies for a range nobody could resolve, as ONE
|
|
2339
|
+
// sentence. Exported on the same harvest rule the audit's repair sentences
|
|
2340
|
+
// use — a test and an artifact quote this, never a copy of it.
|
|
2341
|
+
REVIEW_RANGE_UNRESOLVABLE_REMEDY,
|
|
2342
|
+
// 06-04 — the deterministic half of what the AGENT writes back. The problem
|
|
2343
|
+
// vocabulary joins the three above for the same stated reason: a consumer
|
|
2344
|
+
// branches on a value, and a test harvests the token it asserts on.
|
|
2345
|
+
REVIEW_PROBLEM_KIND,
|
|
2346
|
+
judgeReviewRow,
|
|
2347
|
+
validateReviewArtifact,
|
|
2348
|
+
scanForbiddenScoreTokens,
|
|
2349
|
+
// 06-04 — the ONE composition of every derivation above into the envelope the
|
|
2350
|
+
// CLI arm emits. Composed here rather than in the handler for the reason
|
|
2351
|
+
// `cmdVaultAudit` states about its own leaf: a second composition is a second
|
|
2352
|
+
// place the report can disagree with the derivation it claims to report.
|
|
2353
|
+
buildReviewPayload,
|
|
2354
|
+
// The artifact the agent fills in, and the three lines only code writes.
|
|
2355
|
+
REVIEW_UNJUDGED_VERDICT,
|
|
2356
|
+
renderReviewScaffold,
|
|
2357
|
+
stampReviewLines,
|
|
2358
|
+
compareReviewPopulation,
|
|
2359
|
+
};
|