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