@pareto4/pp-core 1.12.1 → 1.17.1

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 (196) hide show
  1. package/.claude-plugin/plugin.json +3 -3
  2. package/README.md +26 -9
  3. package/agents/pp-code-reviewer.md +3 -2
  4. package/agents/pp-debug-session-manager.md +9 -6
  5. package/agents/pp-executor.md +7 -21
  6. package/agents/pp-plan-checker.md +4 -4
  7. package/agents/pp-planner.md +23 -9
  8. package/agents/pp-vault-reviewer.md +274 -0
  9. package/agents/pp-verifier.md +7 -3
  10. package/bin/install.js +10 -3
  11. package/commands/pp-execute-phase.md +1 -3
  12. package/commands/pp-ns-review.md +2 -1
  13. package/commands/pp-plan-phase.md +1 -2
  14. package/commands/pp-vault-review.md +35 -0
  15. package/commands/pp-verify-work.md +1 -2
  16. package/gemini-extension.json +1 -1
  17. package/hooks/dist/pp-check-update.js +0 -0
  18. package/hooks/dist/pp-cursor-session-start.js +1 -1
  19. package/hooks/dist/pp-statusline.js +1 -1
  20. package/hooks/dist/pp-update-banner.js +0 -0
  21. package/hooks/pp-check-update.js +0 -0
  22. package/hooks/pp-cursor-session-start.js +1 -1
  23. package/hooks/pp-statusline.js +1 -1
  24. package/hooks/pp-update-banner.js +0 -0
  25. package/package.json +14 -19
  26. package/pareto-skills/_shared/create-project-board.sh +0 -0
  27. package/pareto-skills/pp-project-setup/SKILL.md +1 -1
  28. package/pp-core/bin/check-latest-version.cjs +0 -0
  29. package/pp-core/bin/detect-build-commands.sh +2 -2
  30. package/pp-core/bin/lib/capability-activation.cjs +83 -9
  31. package/pp-core/bin/lib/capability-registry.cjs +526 -22
  32. package/pp-core/bin/lib/capability-state.cjs +160 -4
  33. package/pp-core/bin/lib/capability-writer.cjs +19 -5
  34. package/pp-core/bin/lib/check-command-router.cjs +3867 -274
  35. package/pp-core/bin/lib/clusters.cjs +4 -0
  36. package/pp-core/bin/lib/command-aliases.cjs +0 -28
  37. package/pp-core/bin/lib/commands.cjs +9 -4
  38. package/pp-core/bin/lib/compose-bindings.cjs +253 -76
  39. package/pp-core/bin/lib/config-loader.cjs +7 -103
  40. package/pp-core/bin/lib/config-schema.cjs +1 -1
  41. package/pp-core/bin/lib/config.cjs +154 -62
  42. package/pp-core/bin/lib/decisions.cjs +104 -27
  43. package/pp-core/bin/lib/discovery-vault-check.cjs +246 -0
  44. package/pp-core/bin/lib/frontmatter.cjs +1 -1
  45. package/pp-core/bin/lib/git-base-branch.cjs +138 -10
  46. package/pp-core/bin/lib/git-provider.cjs +4 -4
  47. package/pp-core/bin/lib/init-command-router.cjs +5 -8
  48. package/pp-core/bin/lib/init-contract.cjs +653 -629
  49. package/pp-core/bin/lib/init-interactive.cjs +77 -21
  50. package/pp-core/bin/lib/init.cjs +24 -51
  51. package/pp-core/bin/lib/install-profiles.cjs +1 -1
  52. package/pp-core/bin/lib/io.cjs +8 -0
  53. package/pp-core/bin/lib/loop-resolver.cjs +169 -47
  54. package/pp-core/bin/lib/org-profile.cjs +544 -53
  55. package/pp-core/bin/lib/package-identity.cjs +3 -3
  56. package/pp-core/bin/lib/phase-command-router.cjs +1 -23
  57. package/pp-core/bin/lib/phase-scope.cjs +1 -1
  58. package/pp-core/bin/lib/phase.cjs +11 -203
  59. package/pp-core/bin/lib/platform-bindings.cjs +40 -19
  60. package/pp-core/bin/lib/practices-review-check.cjs +15 -9
  61. package/pp-core/bin/lib/profile-output.cjs +1 -1
  62. package/pp-core/bin/lib/shell-command-projection.cjs +104 -1
  63. package/pp-core/bin/lib/state.cjs +11 -85
  64. package/pp-core/bin/lib/template.cjs +1 -1
  65. package/pp-core/bin/lib/vault-audit.cjs +2594 -0
  66. package/pp-core/bin/lib/vault-command-router.cjs +2756 -60
  67. package/pp-core/bin/lib/vault-coordinates.cjs +13 -4
  68. package/pp-core/bin/lib/vault-index.cjs +699 -0
  69. package/pp-core/bin/lib/vault-mode.cjs +235 -0
  70. package/pp-core/bin/lib/vault-practices.cjs +1194 -89
  71. package/pp-core/bin/lib/vault-review.cjs +2102 -0
  72. package/pp-core/bin/lib/vault-seal.cjs +1936 -0
  73. package/pp-core/bin/lib/vault-standards.cjs +1505 -0
  74. package/pp-core/bin/lib/vault-trace.cjs +3184 -0
  75. package/pp-core/bin/lib/vault-update.cjs +242 -0
  76. package/pp-core/bin/lib/vault-write-back.cjs +78 -31
  77. package/pp-core/bin/lib/vault.cjs +2713 -634
  78. package/pp-core/bin/lib/verify.cjs +0 -11
  79. package/pp-core/bin/portable-timeout.sh +1 -1
  80. package/pp-core/bin/pp-tools.cjs +184 -37
  81. package/pp-core/bin/shared/config-defaults.manifest.json +0 -7
  82. package/pp-core/bin/shared/config-schema.manifest.json +0 -3
  83. package/pp-core/bin/shared/model-catalog.json +1 -0
  84. package/pp-core/bin/verify-reapply-patches.cjs +0 -0
  85. package/pp-core/org-profile.json +26 -0
  86. package/pp-core/references/autonomous-best-effort.md +8 -7
  87. package/pp-core/references/loop-hook-dispatch.md +0 -14
  88. package/pp-core/references/planner-gap-closure.md +0 -1
  89. package/pp-core/references/planner-tdd.md +0 -1
  90. package/pp-core/references/planner-vault-practices.md +31 -3
  91. package/pp-core/references/planner-vault-standards.md +320 -0
  92. package/pp-core/references/planning-config.md +0 -8
  93. package/pp-core/references/practice-arithmetic-contract.md +121 -0
  94. package/pp-core/references/tdd.md +0 -1
  95. package/pp-core/references/verifier-practice-verdicts.md +263 -0
  96. package/pp-core/templates/README.md +1 -0
  97. package/pp-core/templates/VAULT-REVIEW.md +202 -0
  98. package/pp-core/templates/phase-prompt.md +2 -6
  99. package/pp-core/templates/practices/README.md +137 -11
  100. package/pp-core/templates/project-charter.md +226 -0
  101. package/pp-core/workflows/_runtime-launcher.snippet.sh +0 -0
  102. package/pp-core/workflows/add-tests.md +55 -0
  103. package/pp-core/workflows/audit-fix.md +63 -0
  104. package/pp-core/workflows/audit-milestone.md +1 -1
  105. package/pp-core/workflows/autonomous.md +85 -11
  106. package/pp-core/workflows/check-todos.md +13 -2
  107. package/pp-core/workflows/code-review-fix.md +59 -1
  108. package/pp-core/workflows/code-review.md +10 -1
  109. package/pp-core/workflows/complete-milestone.md +1 -1
  110. package/pp-core/workflows/debug.md +56 -2
  111. package/pp-core/workflows/discovery-sprint/prefill.md +21 -0
  112. package/pp-core/workflows/discovery-sprint/prototype.md +46 -48
  113. package/pp-core/workflows/discovery-sprint/session-13-gates.md +2 -0
  114. package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +1 -3
  115. package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +1 -3
  116. package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +1 -3
  117. package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +20 -24
  118. package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +1 -3
  119. package/pp-core/workflows/discovery-sprint/sessions/06-security.md +1 -3
  120. package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +1 -3
  121. package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +1 -3
  122. package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +1 -3
  123. package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +1 -6
  124. package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +1 -3
  125. package/pp-core/workflows/discovery-sprint/sessions/12-delivery-model.md +1 -3
  126. package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +1 -3
  127. package/pp-core/workflows/discovery-sprint/templates/context-template.md +1 -1
  128. package/pp-core/workflows/discovery-sprint/vault-context.md +213 -0
  129. package/pp-core/workflows/discovery-sprint.md +7 -3
  130. package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +4 -4
  131. package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +43 -1
  132. package/pp-core/workflows/discovery-to-build.md +9 -1
  133. package/pp-core/workflows/discuss-phase.md +3 -10
  134. package/pp-core/workflows/execute-phase/steps/check-query-gate.md +329 -0
  135. package/pp-core/workflows/execute-phase/steps/client-context-gate.md +3 -2
  136. package/pp-core/workflows/execute-phase/steps/cross-ai-delegation.md +1 -1
  137. package/pp-core/workflows/execute-phase/steps/execute-pre-dispatch.md +98 -0
  138. package/pp-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +94 -0
  139. package/pp-core/workflows/execute-phase/steps/post-merge-gate.md +6 -8
  140. package/pp-core/workflows/execute-phase/steps/vault-fetch.md +115 -14
  141. package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +322 -18
  142. package/pp-core/workflows/execute-phase/steps/verifier-practice-payload.md +165 -0
  143. package/pp-core/workflows/execute-phase/steps/verify-post-gate.md +163 -0
  144. package/pp-core/workflows/execute-phase.md +154 -110
  145. package/pp-core/workflows/execute-plan.md +58 -4
  146. package/pp-core/workflows/fast.md +53 -0
  147. package/pp-core/workflows/help/modes/full.md +1 -0
  148. package/pp-core/workflows/init/steps/client-context-capture.md +101 -0
  149. package/pp-core/workflows/init/steps/gitlab-provisioning.md +130 -0
  150. package/pp-core/workflows/init/steps/project-md-deferral-narration.md +66 -0
  151. package/pp-core/workflows/init/steps/reachability-outcome.md +70 -0
  152. package/pp-core/workflows/init.md +232 -272
  153. package/pp-core/workflows/new-project.md +31 -23
  154. package/pp-core/workflows/plan-phase.md +99 -43
  155. package/pp-core/workflows/quick.md +120 -2
  156. package/pp-core/workflows/secure-phase.md +1 -1
  157. package/pp-core/workflows/ship.md +57 -24
  158. package/pp-core/workflows/update.md +3 -3
  159. package/pp-core/workflows/validate-phase.md +60 -1
  160. package/pp-core/workflows/vault-review.md +325 -0
  161. package/pp-core/workflows/verify-work.md +13 -12
  162. package/scripts/changeset/cli.cjs +6 -5
  163. package/scripts/changeset/lint.cjs +0 -0
  164. package/scripts/changeset/new.cjs +33 -50
  165. package/scripts/dev-env.cjs +25 -85
  166. package/scripts/gen-capability-registry.cjs +199 -0
  167. package/scripts/gen-practice-arithmetic-contract.cjs +412 -0
  168. package/scripts/lib/allowlist-ratchet.cjs +1 -103
  169. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -2
  170. package/scripts/lint-docs-required.cjs +0 -0
  171. package/scripts/lint-pr-check-project-dir.cjs +1 -3
  172. package/scripts/lint-test-file-count.allowlist.json +13 -2
  173. package/scripts/practice-arithmetic-contract.allow.json +35 -0
  174. package/scripts/prompt-injection-scan.sh +22 -15
  175. package/scripts/record-vault-mode-golden.cjs +89 -0
  176. package/scripts/release-channel.cjs +0 -0
  177. package/scripts/sync-manifest-versions.cjs +5 -4
  178. package/scripts/sync-rulesets.sh +1 -1
  179. package/scripts/update-size-baseline.cjs +30 -274
  180. package/scripts/vault-calibrate.cjs +996 -0
  181. package/scripts/verify-opengsd-sweep.cjs +3 -5
  182. package/scripts/workflow-size.cjs +9 -61
  183. package/pp-core/bin/lib/dispatch-plan.cjs +0 -472
  184. package/pp-core/bin/lib/install-run.cjs +0 -536
  185. package/pp-core/bin/lib/phase-tier.cjs +0 -467
  186. package/pp-core/bin/lib/plan-graph.cjs +0 -368
  187. package/pp-core/references/planner-plan-graph.md +0 -66
  188. package/pp-core/workflows/execute-phase/steps/checkpoint-handling.md +0 -45
  189. package/pp-core/workflows/execute-phase/steps/install-run.md +0 -29
  190. package/pp-core/workflows/execute-phase/steps/phase-close-rebaseline.md +0 -23
  191. package/pp-core/workflows/execute-phase/steps/serial-group-dispatch.md +0 -72
  192. package/pp-core/workflows/plan-phase/steps/phase-tier.md +0 -34
  193. package/scripts/lib/size-budget.cjs +0 -311
  194. package/scripts/lint-neutrality-names.cjs +0 -694
  195. package/scripts/lint-org-coordinates.allowlist.json +0 -35
  196. package/scripts/lint-org-coordinates.cjs +0 -426
@@ -0,0 +1,996 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * @file vault-calibrate.cjs — LANE 2 of Phase 6's validation split
6
+ * (Vault Assurance #71924, phase 06 plan 08; D-06-05 … D-06-08, D-06-12,
7
+ * VERIFY-03).
8
+ *
9
+ * ## What this is, and what it deliberately is not
10
+ *
11
+ * Calibration answers a question no deterministic test can: does the agent that
12
+ * judges prose practices actually catch a seeded violation, and does it stay
13
+ * quiet on a compliant near-miss? An agent run is non-deterministic and needs a
14
+ * live session, so this harness NEVER makes a model call. It owns only the
15
+ * halves that are code:
16
+ *
17
+ * `--prepare` materialises the four graded fixture projects from the SHARED
18
+ * fixture module and prints the operator instruction — the
19
+ * `/pp-vault-review` run happens in a live session, through the
20
+ * normal command and agent machinery.
21
+ * `--ingest` reads the four produced `*-VAULT-REVIEW.md` artifacts,
22
+ * validates each one's SHAPE with the shipped validator, derives
23
+ * the two directions, and writes the dated record.
24
+ *
25
+ * ## When to run it (D-06-07)
26
+ *
27
+ * When the reviewer prompt changes (`agents/pp-vault-reviewer.md` or
28
+ * `pp-core/references/verifier-practice-verdicts.md`) or when the practice set
29
+ * changes. NOT on every commit, NOT in CI, and NOT in `npm test`: suite `all`
30
+ * collects every `tests/*.test.cjs` marker, and this repository's whole
31
+ * verification method is a failing-NAME diff over that suite. A live model run
32
+ * inside it would corrupt the one instrument this project trusts. This file is
33
+ * therefore in `scripts/`, reachable only through `npm run vault:calibrate`,
34
+ * and it is absent from `lint:ci` and from the test chain.
35
+ *
36
+ * ## It records; it never gates (D-06-08, VERIFY-03)
37
+ *
38
+ * EVERY path exits 0. A failed direction is written into the record behind a
39
+ * leading marker line and changes nothing else: no config is written anywhere
40
+ * in this file, no gate consults the record, and nothing is auto-disabled. A
41
+ * human reads it and decides. A non-zero exit appears only for an OPERATIONAL
42
+ * failure the operator must fix before a run means anything — an unwritable
43
+ * record home — never because a fixture drew a FAIL.
44
+ *
45
+ * ## Lane 3 limit
46
+ *
47
+ * Four known cases bound the judge on four known inputs. They say nothing about
48
+ * the fifty-fourth practice on a real phase. Calibration is not coverage, and
49
+ * the record says so in its own text.
50
+ */
51
+
52
+ const fs = require('node:fs');
53
+ const os = require('node:os');
54
+ const path = require('node:path');
55
+ const crypto = require('node:crypto');
56
+ const { execFileSync } = require('node:child_process');
57
+
58
+ const REPO_ROOT = path.join(__dirname, '..');
59
+
60
+ /** The compiled leaf. Shape validation and both arithmetic lines come from here. */
61
+ const reviewModule = require(path.join(REPO_ROOT, 'pp-core', 'bin', 'lib', 'vault-review.cjs'));
62
+
63
+ /** Where the shared graded fixture module (06-03) lives, relative to the repo root. */
64
+ const FIXTURE_MODULE_REL = path.join('tests', 'helpers', 'vault-review-fixtures.cjs');
65
+
66
+ let _fixtures = null;
67
+
68
+ /**
69
+ * The shared graded fixture module (06-03), required LAZILY and cached.
70
+ *
71
+ * Lane 1 measures the same four projects this lane runs the agent against, so
72
+ * the two cannot disagree about what was measured — but the module lives under
73
+ * `tests/`, and `tests/` is not in this package's published `files` list while
74
+ * `scripts/` is. An eager require at the top of this file would therefore throw
75
+ * on load inside an installed copy of the package, turning a repo-only
76
+ * maintenance harness into a crash on a path a consumer might stumble onto. The
77
+ * lazy require fails only when a mode that actually needs the fixtures is asked
78
+ * for, and says exactly why.
79
+ */
80
+ function loadFixtures() {
81
+ if (_fixtures !== null) return _fixtures;
82
+ const abs = path.join(REPO_ROOT, FIXTURE_MODULE_REL);
83
+ try {
84
+ _fixtures = require(abs);
85
+ } catch (err) {
86
+ throw new Error(
87
+ `the shared graded fixture module ${FIXTURE_MODULE_REL} could not be loaded, so no calibration ` +
88
+ 'fixture could be built or read. This harness runs from a checkout of the pp-core repository; ' +
89
+ `it is not part of the published package's test surface. (${err && err.message})`,
90
+ );
91
+ }
92
+ return _fixtures;
93
+ }
94
+
95
+ // ─── Names the two halves and the Lane 1 shape tests share ───────────────────
96
+
97
+ /** The phase directory `--prepare` scaffolds inside each fixture project. */
98
+ const CALIBRATION_PHASE_DIRNAME = '06-calibration';
99
+
100
+ /** The trace-carrying artifact that puts the fixture practice into the ledger. */
101
+ const CALIBRATION_TRACE_ARTIFACT = '06-01-SUMMARY.md';
102
+
103
+ /** The artifact `/pp-vault-review` writes, and `--ingest` reads back. */
104
+ const CALIBRATION_REVIEW_ARTIFACT = '06-VAULT-REVIEW.md';
105
+
106
+ /** The record home — COMMITTED, and deliberately outside the gitignored `.planning/`. */
107
+ const RECORD_HOME_REL = path.join('docs', 'vault-calibration');
108
+
109
+ /** The default working directory, stable so `--prepare` and `--ingest` agree with no flag. */
110
+ const DEFAULT_WORK_DIR = path.join(os.tmpdir(), 'pp-vault-calibrate');
111
+
112
+ /** Whether one fixture was read at all, and if not, on what ground. */
113
+ const OBSERVATION_STATUS = Object.freeze({
114
+ MEASURED: 'measured',
115
+ NOT_MEASURED: 'not-measured',
116
+ });
117
+
118
+ const NOT_MEASURED_GROUND = Object.freeze({
119
+ ARTIFACT_ABSENT: 'artifact-absent',
120
+ ARTIFACT_UNREADABLE: 'artifact-unreadable',
121
+ SHAPE_INVALID: 'shape-invalid',
122
+ });
123
+
124
+ /**
125
+ * A direction's outcome. `INCOMPLETE` and `NOT_MEASURED` exist so a direction
126
+ * derived over fewer fixtures than it names can never render as one that held
127
+ * (T-6-34): an absence is reported as an absence.
128
+ */
129
+ const DIRECTION_OUTCOME = Object.freeze({
130
+ HELD: 'held',
131
+ FAILED: 'failed',
132
+ INCOMPLETE: 'incomplete',
133
+ NOT_MEASURED: 'not-measured',
134
+ });
135
+
136
+ /** The leading marker. A failed calibration is not allowed to read like a quiet one. */
137
+ const FAILURE_MARKER = 'FAILED CALIBRATION';
138
+
139
+ /** D-06-08, stated in the record's own body so a reader meets it where the failure is. */
140
+ const NEVER_AUTO_DISABLES =
141
+ 'Nothing reads this record to change behaviour: no gate consults it, no configuration is written ' +
142
+ 'from it, and a failed calibration disables nothing at all. A human reads this file and decides ' +
143
+ 'whether to repair the reviewer prompt or to stop trusting this tier.';
144
+
145
+ /** The Lane 3 bound, carried by the record rather than left in a plan nobody reopens. */
146
+ const LANE3_LIMIT =
147
+ 'These four known cases bound the judge on four known inputs and bound nothing else. They say ' +
148
+ 'nothing about the fifty-fourth practice on a real phase, and nothing about whether any single ' +
149
+ 'verdict here would reproduce on a second run. Calibration is not coverage.';
150
+
151
+ /** The record's sections, in order. The Lane 1 shape test walks exactly this list. */
152
+ const RECORD_SECTIONS = Object.freeze([
153
+ '## What this record is',
154
+ '## Engine and prompt fingerprint',
155
+ '## Per-fixture observations',
156
+ '## The two directions (D-06-06)',
157
+ '## Observed non-verdict rate (D-06-12)',
158
+ '## What this record does NOT bound (Lane 3)',
159
+ ]);
160
+
161
+ /** The two files whose bytes decide whether a past record still describes today's judge. */
162
+ const PROMPT_SOURCES = Object.freeze([
163
+ path.join('agents', 'pp-vault-reviewer.md'),
164
+ path.join('pp-core', 'references', 'verifier-practice-verdicts.md'),
165
+ ]);
166
+
167
+ // ─── The trace `--prepare` seeds ─────────────────────────────────────────────
168
+
169
+ /**
170
+ * The execute-stage practice trace the scaffolded phase carries.
171
+ *
172
+ * WHY A TRACE AT ALL, stated so a later reader does not remove it as clutter:
173
+ * without one the review takes the RECONSTRUCT arm, and on that arm the input
174
+ * set is derived from the ledger — which is empty — so the remainder is empty
175
+ * and NO practice reaches the agent. A calibration session against an empty
176
+ * remainder would judge nothing while producing a well-formed artifact, which
177
+ * is exactly the hollow-artifact failure mode this workstream exists to end.
178
+ * Seeding the ledger with the fixture's one practice is what makes the live run
179
+ * a measurement instead of a formality.
180
+ *
181
+ * @param {string} resolvedSha the fixture vault pin, so the review's pin advisory stays quiet
182
+ * @param {string} practiceId the one practice all four grades are judged against
183
+ * @param {string[]} changedFiles the graded files, as the trace subject
184
+ * @returns {string} the artifact body, frontmatter included
185
+ */
186
+ function renderCalibrationTrace(resolvedSha, practiceId, changedFiles) {
187
+ const subject = changedFiles.map((name) => [` - name: ${name}`, ' uri: ' + name]).flat();
188
+ return [
189
+ '---',
190
+ 'phase: 06-calibration',
191
+ 'plan: 1',
192
+ 'status: complete',
193
+ 'vault:',
194
+ ' _type: https://in-toto.io/Statement/v1',
195
+ ' predicateType: https://github.com/pareto000/pp-core/attestation/vault-practice-trace/v1',
196
+ ' schemaVersion: 1',
197
+ ' subject:',
198
+ ...subject,
199
+ ' predicate:',
200
+ ' stage: execute',
201
+ ' plan: "06-01"',
202
+ ' generatedAt: "2026-01-01T00:00:00.000Z"',
203
+ ' vault:',
204
+ ' state: loaded',
205
+ ' reason: null',
206
+ ' ref: calibration-fixture',
207
+ ` resolvedSha: ${resolvedSha}`,
208
+ ' tier: client',
209
+ ' source: cache',
210
+ ' repo: calibration-fixture',
211
+ ' practiceUniverse:',
212
+ ` - ${practiceId}`,
213
+ ' practices:',
214
+ ` - id: "${practiceId}"`,
215
+ ' scope: scoped',
216
+ ' severity: warning',
217
+ ' status: applied',
218
+ ' reason: "seeded by the calibration harness so the practice reaches the judge"',
219
+ ' tasks: []',
220
+ ' override: null',
221
+ '---',
222
+ '',
223
+ '# Calibration fixture phase',
224
+ '',
225
+ 'A one-plan phase whose single commit carries the graded file. It exists so that',
226
+ '`/pp-vault-review` has a completed phase to review and a ledger to review it against.',
227
+ '',
228
+ ].join('\n');
229
+ }
230
+
231
+ /** A minimal planning workspace, so the phase resolver can find the scaffolded phase. */
232
+ function renderCalibrationState() {
233
+ return [
234
+ '---',
235
+ 'current_phase: 6',
236
+ 'status: complete',
237
+ '---',
238
+ '',
239
+ '# Project State',
240
+ '',
241
+ '- Current phase: 6 — calibration fixture',
242
+ '',
243
+ ].join('\n');
244
+ }
245
+
246
+ function renderCalibrationRoadmap() {
247
+ return [
248
+ '# Roadmap',
249
+ '',
250
+ '## Phase 6: Calibration fixture',
251
+ '',
252
+ '**Status:** Complete',
253
+ '',
254
+ '- [x] 06-01 — the graded file',
255
+ '',
256
+ ].join('\n');
257
+ }
258
+
259
+ /**
260
+ * Scaffold the calibration phase inside an already-built fixture project: the
261
+ * trace-carrying summary, plus the minimal planning workspace the phase
262
+ * resolver needs to answer `/pp-vault-review 6`.
263
+ *
264
+ * Exported as its own seam so a deterministic test can build ONE fixture, run
265
+ * the shipped CLI arm inside it, and pin that the vault-ENABLED path is live —
266
+ * without materialising all four or waiting for a live session.
267
+ *
268
+ * @param {string} projectDir a directory produced by `mkGradedReviewProject`
269
+ * @param {{resolvedSha: string, practiceId: string, changedFiles: string[]}} fixture
270
+ * @returns {{phaseDirRel: string, phaseDirAbs: string}}
271
+ */
272
+ function scaffoldCalibrationPhase(projectDir, { resolvedSha, practiceId, changedFiles }) {
273
+ const phaseDirRel = path.join('.planning', 'phases', CALIBRATION_PHASE_DIRNAME);
274
+ const phaseDirAbs = path.join(projectDir, phaseDirRel);
275
+ fs.mkdirSync(phaseDirAbs, { recursive: true });
276
+ fs.writeFileSync(
277
+ path.join(phaseDirAbs, CALIBRATION_TRACE_ARTIFACT),
278
+ renderCalibrationTrace(resolvedSha, practiceId, changedFiles),
279
+ 'utf-8',
280
+ );
281
+ fs.writeFileSync(path.join(projectDir, '.planning', 'STATE.md'), renderCalibrationState(), 'utf-8');
282
+ fs.writeFileSync(path.join(projectDir, '.planning', 'ROADMAP.md'), renderCalibrationRoadmap(), 'utf-8');
283
+ return { phaseDirRel, phaseDirAbs };
284
+ }
285
+
286
+ // ─── `--prepare` ─────────────────────────────────────────────────────────────
287
+
288
+ function rmDeep(target) {
289
+ fs.rmSync(target, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
290
+ }
291
+
292
+ /**
293
+ * Materialise the four fixture projects under `dir`, one directory per grade,
294
+ * each carrying the scaffolded phase.
295
+ *
296
+ * IDEMPOTENT IN CONTENT, AND SAYS SO. A second run removes and rebuilds every
297
+ * slot, so the working-tree bytes of every fixture are identical to the first
298
+ * run's. The git objects are not — a fresh `git init` writes fresh commit
299
+ * timestamps — and the returned `refreshed` flag records which slots were
300
+ * replaced rather than created, so the operator is never left guessing whether
301
+ * a stale artifact from a previous session is still sitting there. It is not:
302
+ * a rebuilt slot has no review artifact until a new session writes one.
303
+ *
304
+ * @param {{dir: string}} opts
305
+ * @returns {Array<object>} one entry per grade
306
+ */
307
+ function prepareFixtures({ dir }) {
308
+ fs.mkdirSync(dir, { recursive: true });
309
+ const prepared = [];
310
+
311
+ const fixtures = loadFixtures();
312
+
313
+ for (const fixtureId of fixtures.GRADED_FIXTURES) {
314
+ const target = path.join(dir, fixtureId);
315
+ const refreshed = fs.existsSync(target);
316
+ if (refreshed) rmDeep(target);
317
+
318
+ const built = fixtures.mkGradedReviewProject(fixtureId);
319
+ try {
320
+ fs.cpSync(built.dir, target, { recursive: true });
321
+ } finally {
322
+ built.cleanup();
323
+ }
324
+
325
+ const scaffold = scaffoldCalibrationPhase(target, built);
326
+
327
+ prepared.push({
328
+ fixtureId,
329
+ dir: target,
330
+ refreshed,
331
+ practiceId: built.practiceId,
332
+ changedFiles: built.changedFiles,
333
+ expectFail: built.expectFail,
334
+ gloss: built.gloss,
335
+ phaseDirRel: scaffold.phaseDirRel,
336
+ });
337
+ }
338
+
339
+ return prepared;
340
+ }
341
+
342
+ /**
343
+ * Ask the SHIPPED CLI arm, inside a prepared fixture, whether the review is
344
+ * live there — vault ENABLED, trace found, one practice in the remainder.
345
+ *
346
+ * This is a deterministic exercise of the vault-enabled derive path and it is
347
+ * NOT a substitute for the agent run: it proves the fixture is reviewable and
348
+ * that the practice prose reaches the prompt, and it proves nothing whatsoever
349
+ * about any judgement. Read-and-swallow: a probe that cannot run reports that
350
+ * it could not, and `--prepare` still completes.
351
+ *
352
+ * ONE RUNNER, ONE CLAIM (06-11). The projections below `entries` — `refuses`,
353
+ * `counts`, `statements` and `trace` — were added so a test can assert on the
354
+ * clean-render refusal, the derived counts and the trace provenance WITHOUT a
355
+ * second `execFileSync` of the same command living in the test file. Two runners
356
+ * of one command are two places the invocation can drift, which is the 04-18
357
+ * lesson one level up from the predicate it was first learned on. `--prepare`
358
+ * reads only the original fields and is unaffected.
359
+ *
360
+ * `trace` (06-12 / G1) projects the payload's own provenance answer — the CLI
361
+ * decides loaded-vs-degraded and this runner only reads it back, so a test can
362
+ * never accidentally pin a rule the shipped arm does not hold.
363
+ *
364
+ * @param {string} projectDir
365
+ * @param {string} phaseDirRel
366
+ * @returns {{ok: boolean, status: string|null, arm: string|null, remainder: number|null,
367
+ * entries: number|null, refuses: boolean|null, counts: object|null,
368
+ * statements: string[]|null,
369
+ * trace: {present: boolean|null, state: string|null, reason: string|null,
370
+ * loaded: boolean|null}|null,
371
+ * detail: string|null}}
372
+ */
373
+ function probeReviewArm(projectDir, phaseDirRel) {
374
+ const tools = path.join(REPO_ROOT, 'pp-core', 'bin', 'pp-tools.cjs');
375
+ try {
376
+ const out = execFileSync(process.execPath, [tools, 'check', 'vault-review', phaseDirRel, '--raw'], {
377
+ cwd: projectDir,
378
+ encoding: 'utf-8',
379
+ timeout: 60_000,
380
+ });
381
+ const payload = JSON.parse(out);
382
+ return {
383
+ ok: true,
384
+ status: typeof payload.status === 'string' ? payload.status : null,
385
+ arm: typeof payload.arm === 'string' ? payload.arm : null,
386
+ remainder: Array.isArray(payload?.partition?.remainder) ? payload.partition.remainder.length : null,
387
+ entries: Array.isArray(payload?.practiceEntries) ? payload.practiceEntries.length : null,
388
+ refuses: typeof payload?.refusesCleanRender === 'boolean' ? payload.refusesCleanRender : null,
389
+ counts: payload && typeof payload.derivedCounts === 'object' ? payload.derivedCounts : null,
390
+ statements: Array.isArray(payload?.statements) ? payload.statements : null,
391
+ trace:
392
+ payload && typeof payload.trace === 'object' && payload.trace !== null
393
+ ? {
394
+ present: typeof payload.trace.present === 'boolean' ? payload.trace.present : null,
395
+ state: typeof payload.trace.state === 'string' ? payload.trace.state : null,
396
+ reason: typeof payload.trace.reason === 'string' ? payload.trace.reason : null,
397
+ loaded: typeof payload.trace.loaded === 'boolean' ? payload.trace.loaded : null,
398
+ }
399
+ : null,
400
+ detail: null,
401
+ };
402
+ } catch (err) {
403
+ return {
404
+ ok: false,
405
+ status: null,
406
+ arm: null,
407
+ remainder: null,
408
+ entries: null,
409
+ refuses: null,
410
+ counts: null,
411
+ statements: null,
412
+ trace: null,
413
+ detail: err && err.message ? String(err.message).split('\n')[0] : 'the probe did not run',
414
+ };
415
+ }
416
+ }
417
+
418
+ // ─── `--ingest` ──────────────────────────────────────────────────────────────
419
+
420
+ /**
421
+ * Read and judge the SHAPE of one fixture's produced artifact.
422
+ *
423
+ * MEASURED requires two things: the artifact exists and is readable, and the
424
+ * shipped validator found no problem in it. Anything else is NOT MEASURED with
425
+ * a named ground — never a pass, and never a throw. An artifact whose shape the
426
+ * validator could not vouch for is an artifact whose counts nobody should quote.
427
+ *
428
+ * @param {{dir: string, fixtureId: string}} opts
429
+ * @returns {object} the observation
430
+ */
431
+ function observeFixture({ dir, fixtureId }) {
432
+ const fixtures = loadFixtures();
433
+ const spec = Object.prototype.hasOwnProperty.call(fixtures.GRADED_FIXTURE_SPECS, fixtureId)
434
+ ? fixtures.GRADED_FIXTURE_SPECS[fixtureId]
435
+ : null;
436
+ if (!spec) {
437
+ throw new Error(
438
+ `unknown graded fixture "${String(fixtureId)}" — the four grades are: ${fixtures.GRADED_FIXTURES.join(', ')}`,
439
+ );
440
+ }
441
+
442
+ const artifactPath = path.join(
443
+ dir,
444
+ fixtureId,
445
+ '.planning',
446
+ 'phases',
447
+ CALIBRATION_PHASE_DIRNAME,
448
+ CALIBRATION_REVIEW_ARTIFACT,
449
+ );
450
+ const base = {
451
+ fixtureId,
452
+ expectFail: spec.expectFail,
453
+ gloss: spec.gloss,
454
+ artifactPath,
455
+ counts: null,
456
+ rateLine: null,
457
+ countsLine: null,
458
+ drewFail: null,
459
+ problemKinds: [],
460
+ };
461
+
462
+ let text;
463
+ try {
464
+ text = fs.readFileSync(artifactPath, 'utf-8');
465
+ } catch (err) {
466
+ const absent = err && err.code === 'ENOENT';
467
+ return {
468
+ ...base,
469
+ status: OBSERVATION_STATUS.NOT_MEASURED,
470
+ ground: absent ? NOT_MEASURED_GROUND.ARTIFACT_ABSENT : NOT_MEASURED_GROUND.ARTIFACT_UNREADABLE,
471
+ statement: absent
472
+ ? `"${fixtureId}" produced no review artifact at ${CALIBRATION_REVIEW_ARTIFACT}, so this grade was ` +
473
+ 'NOT MEASURED. A fixture nobody reviewed is not a fixture that came out clean.'
474
+ : `"${fixtureId}" has a review artifact that could not be read, so this grade was NOT MEASURED.`,
475
+ };
476
+ }
477
+
478
+ const result = reviewModule.validateReviewArtifact(text);
479
+ const problemKinds = [...new Set(result.problems.map((p) => String(p.kind)))].sort();
480
+ if (!result.sectionPresent || problemKinds.length > 0) {
481
+ return {
482
+ ...base,
483
+ status: OBSERVATION_STATUS.NOT_MEASURED,
484
+ ground: NOT_MEASURED_GROUND.SHAPE_INVALID,
485
+ problemKinds,
486
+ statement:
487
+ `"${fixtureId}" produced an artifact the shipped validator could not vouch for, so this grade ` +
488
+ `was NOT MEASURED. Reported: ${problemKinds.join(', ') || 'no verdict section'}.`,
489
+ };
490
+ }
491
+
492
+ return {
493
+ ...base,
494
+ status: OBSERVATION_STATUS.MEASURED,
495
+ ground: null,
496
+ counts: result.counts,
497
+ rateLine: result.expectedRateLine,
498
+ countsLine: result.expectedCountsLine,
499
+ drewFail: result.counts.fail > 0,
500
+ statement:
501
+ `"${fixtureId}" was reviewed and its artifact validated. ` +
502
+ (result.counts.fail > 0 ? 'It drew at least one FAIL.' : 'It drew no FAIL.'),
503
+ };
504
+ }
505
+
506
+ /** Observe all four grades, in the order the shared module lists them. */
507
+ function observeAllFixtures({ dir }) {
508
+ return loadFixtures().GRADED_FIXTURES.map((fixtureId) => observeFixture({ dir, fixtureId }));
509
+ }
510
+
511
+ /**
512
+ * Derive one direction over the fixtures whose `expectFail` matches `expectFail`.
513
+ *
514
+ * A direction HELD only when every one of its fixtures was measured and every
515
+ * measured one agrees. One contradiction FAILS it; one unmeasured fixture makes
516
+ * it INCOMPLETE and names the omission; nothing measured makes it NOT MEASURED.
517
+ * A direction is never allowed to hold on a subset while the subset goes
518
+ * unmentioned.
519
+ */
520
+ function deriveDirection(observations, expectFail, label) {
521
+ const mine = observations.filter((o) => o.expectFail === expectFail);
522
+ const measured = mine.filter((o) => o.status === OBSERVATION_STATUS.MEASURED);
523
+ const notMeasured = mine.filter((o) => o.status !== OBSERVATION_STATUS.MEASURED).map((o) => o.fixtureId);
524
+ const contradicted = measured.filter((o) => o.drewFail !== expectFail).map((o) => o.fixtureId);
525
+
526
+ let outcome;
527
+ if (contradicted.length > 0) outcome = DIRECTION_OUTCOME.FAILED;
528
+ else if (measured.length === 0) outcome = DIRECTION_OUTCOME.NOT_MEASURED;
529
+ else if (notMeasured.length > 0) outcome = DIRECTION_OUTCOME.INCOMPLETE;
530
+ else outcome = DIRECTION_OUTCOME.HELD;
531
+
532
+ const statements = {
533
+ [DIRECTION_OUTCOME.HELD]: `every fixture in this direction was measured and agreed: ${measured
534
+ .map((o) => o.fixtureId)
535
+ .join(', ')}.`,
536
+ [DIRECTION_OUTCOME.FAILED]: `${contradicted.join(', ')} contradicted this direction.`,
537
+ [DIRECTION_OUTCOME.INCOMPLETE]:
538
+ `the measured fixtures agreed, but ${notMeasured.join(', ')} went unmeasured, so this direction ` +
539
+ 'is derived over a subset and does not hold as stated.',
540
+ [DIRECTION_OUTCOME.NOT_MEASURED]:
541
+ 'no fixture in this direction was measured, so nothing was derived about it at all.',
542
+ };
543
+
544
+ return {
545
+ expectFail,
546
+ label,
547
+ outcome,
548
+ fixtures: mine.map((o) => o.fixtureId),
549
+ measured: measured.map((o) => o.fixtureId),
550
+ notMeasured,
551
+ contradicted,
552
+ statement: statements[outcome],
553
+ };
554
+ }
555
+
556
+ /**
557
+ * Both D-06-06 directions. A verifier that fails everything catches every
558
+ * violation and is worthless, so direction two is not a courtesy check — it is
559
+ * the one that distinguishes a calibrated judge from an indiscriminate one.
560
+ */
561
+ function deriveDirections(observations) {
562
+ const one = deriveDirection(
563
+ observations,
564
+ true,
565
+ 'Direction 1 — each seeded violation drew at least one FAIL',
566
+ );
567
+ const two = deriveDirection(
568
+ observations,
569
+ false,
570
+ 'Direction 2 — each compliant fixture drew no FAIL at all',
571
+ );
572
+ return {
573
+ one,
574
+ two,
575
+ failed: one.outcome === DIRECTION_OUTCOME.FAILED || two.outcome === DIRECTION_OUTCOME.FAILED,
576
+ };
577
+ }
578
+
579
+ // ─── The record ──────────────────────────────────────────────────────────────
580
+
581
+ /** `<YYYY-MM-DD>-<vault-sha8>.md` — the date and the pin the fixtures carry. */
582
+ function recordFileName(date, resolvedSha) {
583
+ const sha8 = String(resolvedSha ?? '').slice(0, 8) || 'unknown8';
584
+ return `${date}-${sha8}.md`;
585
+ }
586
+
587
+ /**
588
+ * The fixture ids a RECORD ALREADY ON DISK reports as measured.
589
+ *
590
+ * WHY THIS EXISTS: the record filename varies only by date and fixture sha, so
591
+ * two ingests on one day address the same file — and `--prepare` deliberately
592
+ * clears each fixture's prior review artifact, so the documented
593
+ * prepare → live-run → ingest sequence can arrive at `--ingest` with nothing to
594
+ * read. Writing then would replace a record that measured four grades with one
595
+ * that measured none. A record nobody re-measured is not a record that came out
596
+ * empty, which is the same distinction `observeFixture` already makes one tier
597
+ * down — enforced here against the file rather than against a fixture.
598
+ *
599
+ * Parses the per-fixture observations table rather than any prose: the Observed
600
+ * cell is written by `renderRecord` as `NOT MEASURED (<ground>)` or as a drew-*
601
+ * statement, and nothing else lands in that column.
602
+ *
603
+ * @param {string} text a rendered record's full bytes
604
+ * @param {string[]} [knownIds] the graded fixture ids; defaults to the shared module's four
605
+ * @returns {string[]} fixture ids whose Observed cell is not a NOT MEASURED cell
606
+ */
607
+ function readRecordMeasuredIds(text, knownIds = loadFixtures().GRADED_FIXTURES) {
608
+ const out = [];
609
+ for (const line of String(text ?? '').split('\n')) {
610
+ const cells = line.split('|').map((c) => c.trim());
611
+ // `| \`id\` | expected | observed | counts |` splits to 6 with empty ends.
612
+ if (cells.length !== 6) continue;
613
+ const id = cells[1].replace(/^`|`$/g, '');
614
+ if (!knownIds.includes(id)) continue;
615
+ if (cells[3].startsWith('NOT MEASURED')) continue;
616
+ out.push(id);
617
+ }
618
+ return out;
619
+ }
620
+
621
+ const OUTCOME_LABEL = Object.freeze({
622
+ [DIRECTION_OUTCOME.HELD]: 'HELD',
623
+ [DIRECTION_OUTCOME.FAILED]: 'FAILED',
624
+ [DIRECTION_OUTCOME.INCOMPLETE]: 'INCOMPLETE',
625
+ [DIRECTION_OUTCOME.NOT_MEASURED]: 'NOT MEASURED',
626
+ });
627
+
628
+ /**
629
+ * Render the dated record. PURE over its input, so two renders of the same
630
+ * observations are byte-identical — the same serialization discipline
631
+ * `scripts/update-size-baseline.cjs` states for its baseline, and for the same
632
+ * reason: a record whose bytes drift for no reason is a record nobody can diff.
633
+ */
634
+ function renderRecord({ date, engine, promptFingerprint, observations, directions, notes = [] }) {
635
+ const lines = [];
636
+ lines.push(`# Vault practice-review calibration — ${date}`);
637
+ lines.push('');
638
+ if (directions.failed) {
639
+ lines.push(
640
+ `> **${FAILURE_MARKER}.** At least one direction was contradicted by a measured fixture. ` +
641
+ 'Read the directions section below before trusting any review this judge produces.',
642
+ );
643
+ lines.push('');
644
+ }
645
+
646
+ lines.push(RECORD_SECTIONS[0]);
647
+ lines.push('');
648
+ lines.push(
649
+ 'One on-demand run of the four graded fixtures (D-06-05) through `/pp-vault-review`, with the ' +
650
+ 'shape of every produced artifact validated by the shipped validator before anything below was ' +
651
+ 'derived from it.',
652
+ );
653
+ lines.push('');
654
+ lines.push(NEVER_AUTO_DISABLES);
655
+ lines.push('');
656
+
657
+ lines.push(RECORD_SECTIONS[1]);
658
+ lines.push('');
659
+ lines.push(`- Engine commit: \`${engine}\``);
660
+ lines.push(`- Reviewer prompt fingerprint: \`${promptFingerprint}\``);
661
+ lines.push('');
662
+ lines.push(
663
+ 'The fingerprint is a digest over the reviewer agent file and the verdict reference. A record ' +
664
+ 'whose fingerprint differs from today\'s describes a judge that no longer exists, which is the ' +
665
+ 'signal to run calibration again (D-06-07).',
666
+ );
667
+ lines.push('');
668
+
669
+ lines.push(RECORD_SECTIONS[2]);
670
+ lines.push('');
671
+ lines.push('| Fixture | Expected direction | Observed | Counts by verdict |');
672
+ lines.push('| --- | --- | --- | --- |');
673
+ for (const o of observations) {
674
+ const expected = o.expectFail ? 'must draw FAIL' : 'must draw no FAIL';
675
+ const observed =
676
+ o.status === OBSERVATION_STATUS.MEASURED
677
+ ? o.drewFail
678
+ ? 'drew FAIL'
679
+ : 'drew no FAIL'
680
+ : `NOT MEASURED (${o.ground})`;
681
+ const counts = o.status === OBSERVATION_STATUS.MEASURED ? o.countsLine : '—';
682
+ lines.push(`| \`${o.fixtureId}\` | ${expected} | ${observed} | ${counts} |`);
683
+ }
684
+ lines.push('');
685
+ for (const o of observations) {
686
+ if (o.status !== OBSERVATION_STATUS.MEASURED) lines.push(`- **NOT MEASURED** — ${o.statement}`);
687
+ }
688
+ if (observations.every((o) => o.status === OBSERVATION_STATUS.MEASURED)) {
689
+ lines.push('- Every one of the four grades produced an artifact the validator vouched for.');
690
+ }
691
+ lines.push('');
692
+
693
+ lines.push(RECORD_SECTIONS[3]);
694
+ lines.push('');
695
+ for (const direction of [directions.one, directions.two]) {
696
+ lines.push(`### ${direction.label}`);
697
+ lines.push('');
698
+ lines.push(`**${OUTCOME_LABEL[direction.outcome]}** — ${direction.statement}`);
699
+ lines.push('');
700
+ if (direction.notMeasured.length > 0) {
701
+ lines.push(`Unmeasured in this direction: ${direction.notMeasured.join(', ')}.`);
702
+ lines.push('');
703
+ }
704
+ if (direction.contradicted.length > 0) {
705
+ lines.push(`Contradicting fixtures: ${direction.contradicted.join(', ')}.`);
706
+ lines.push('');
707
+ }
708
+ }
709
+
710
+ lines.push(RECORD_SECTIONS[4]);
711
+ lines.push('');
712
+ lines.push('| Fixture | Non-verdict rate line |');
713
+ lines.push('| --- | --- |');
714
+ for (const o of observations) {
715
+ lines.push(`| \`${o.fixtureId}\` | ${o.status === OBSERVATION_STATUS.MEASURED ? o.rateLine : 'NOT MEASURED'} |`);
716
+ }
717
+ lines.push('');
718
+ lines.push(
719
+ 'The rate line is the shipped renderer\'s own output, computed from the rows and never narrated. ' +
720
+ 'D-06-12 surfaces the rate and its deferred sibling deliberately refuses a threshold, so there ' +
721
+ 'is no number here to pass or fail against — only one to read and to compare with the next run.',
722
+ );
723
+ lines.push('');
724
+
725
+ lines.push(RECORD_SECTIONS[5]);
726
+ lines.push('');
727
+ lines.push(LANE3_LIMIT);
728
+ lines.push('');
729
+ for (const note of notes) lines.push(`- ${note}`);
730
+ if (notes.length > 0) lines.push('');
731
+
732
+ return lines.join('\n');
733
+ }
734
+
735
+ // ─── Fingerprints and the engine describe ────────────────────────────────────
736
+
737
+ /** A digest over the reviewer prompt surfaces. Missing files are named, not silently skipped. */
738
+ function promptFingerprint() {
739
+ const hash = crypto.createHash('sha256');
740
+ const missing = [];
741
+ for (const rel of PROMPT_SOURCES) {
742
+ try {
743
+ hash.update(fs.readFileSync(path.join(REPO_ROOT, rel)));
744
+ } catch {
745
+ missing.push(rel);
746
+ hash.update(`ABSENT:${rel}`);
747
+ }
748
+ }
749
+ return { digest: hash.digest('hex').slice(0, 16), missing };
750
+ }
751
+
752
+ /** `git rev-parse --short HEAD`, tolerant of every failure. */
753
+ function engineDescribe() {
754
+ try {
755
+ return execFileSync('git', ['rev-parse', '--short', 'HEAD'], {
756
+ cwd: REPO_ROOT,
757
+ encoding: 'utf-8',
758
+ timeout: 10_000,
759
+ }).trim();
760
+ } catch {
761
+ return 'unknown';
762
+ }
763
+ }
764
+
765
+ function today() {
766
+ return new Date().toISOString().slice(0, 10);
767
+ }
768
+
769
+ // ─── The two modes ───────────────────────────────────────────────────────────
770
+
771
+ function runPrepare({ dir, probe = true, log = console.log }) {
772
+ const prepared = prepareFixtures({ dir });
773
+
774
+ log('');
775
+ log('pp-core ► VAULT PRACTICE REVIEW CALIBRATION — prepare');
776
+ log('');
777
+ log(`Working directory: ${dir}`);
778
+ log(`Four graded fixtures materialised from ${FIXTURE_MODULE_REL}.`);
779
+ log('');
780
+
781
+ for (const entry of prepared) {
782
+ if (probe) entry.probe = probeReviewArm(entry.dir, entry.phaseDirRel);
783
+ log(`── ${entry.fixtureId} ${entry.refreshed ? '(refreshed)' : '(created)'}`);
784
+ log(` ${entry.gloss}`);
785
+ log(` expected: ${entry.expectFail ? 'the judge must draw FAIL' : 'the judge must draw no FAIL'}`);
786
+ log(` project: ${entry.dir}`);
787
+ if (entry.probe) {
788
+ log(
789
+ entry.probe.ok
790
+ ? ` arm: status=${entry.probe.status} arm=${entry.probe.arm} ` +
791
+ `remainder=${entry.probe.remainder} fenced-practice-entries=${entry.probe.entries}`
792
+ : ` arm: the derive probe did not run — ${entry.probe.detail}`,
793
+ );
794
+ if (entry.probe.ok && entry.probe.remainder === 0) {
795
+ log(' WARNING: the remainder is empty, so no practice would reach the judge in this fixture.');
796
+ }
797
+ }
798
+ log('');
799
+ }
800
+
801
+ log('Now run the live session — this harness makes no model call:');
802
+ log('');
803
+ for (const entry of prepared) {
804
+ log(` cd ${entry.dir} && /pp-vault-review 6`);
805
+ }
806
+ log('');
807
+ log('Each run writes .planning/phases/' + CALIBRATION_PHASE_DIRNAME + '/' + CALIBRATION_REVIEW_ARTIFACT + '.');
808
+ log('When all four exist, ingest them:');
809
+ log('');
810
+ log(` npm run vault:calibrate -- --ingest --dir ${dir}`);
811
+ log('');
812
+ return prepared;
813
+ }
814
+
815
+ function runIngest({
816
+ dir,
817
+ out = path.join(REPO_ROOT, RECORD_HOME_REL),
818
+ now = today(),
819
+ log = console.log,
820
+ force = false,
821
+ }) {
822
+ const observations = observeAllFixtures({ dir });
823
+ const directions = deriveDirections(observations);
824
+ const fingerprint = promptFingerprint();
825
+ const notes = fingerprint.missing.map(
826
+ (rel) => `The reviewer prompt source \`${rel}\` was NOT readable, so the fingerprint above covers less than it names.`,
827
+ );
828
+
829
+ const text = renderRecord({
830
+ date: now,
831
+ engine: engineDescribe(),
832
+ promptFingerprint: fingerprint.digest,
833
+ observations,
834
+ directions,
835
+ notes,
836
+ });
837
+
838
+ const fileName = recordFileName(now, loadFixtures().FIXTURE_RESOLVED_SHA);
839
+ const target = path.join(out, fileName);
840
+
841
+ // The record home is an audit trail, so a write that REMOVES a measurement is
842
+ // refused rather than performed quietly. Only that direction is refused: a run
843
+ // that measures at least everything the record on disk already measured
844
+ // overwrites it, which is what re-running a calibration is for.
845
+ const regressed = [];
846
+ let existingText = null;
847
+ if (fs.existsSync(target)) {
848
+ try {
849
+ existingText = fs.readFileSync(target, 'utf-8');
850
+ } catch {
851
+ existingText = null;
852
+ }
853
+ }
854
+ if (existingText !== null) {
855
+ const before = readRecordMeasuredIds(existingText);
856
+ const after = new Set(
857
+ observations.filter((o) => o.status === OBSERVATION_STATUS.MEASURED).map((o) => o.fixtureId),
858
+ );
859
+ for (const id of before) if (!after.has(id)) regressed.push(id);
860
+ }
861
+ if (regressed.length > 0 && !force) {
862
+ log('');
863
+ log('pp-core ► VAULT PRACTICE REVIEW CALIBRATION — ingest REFUSED');
864
+ log('');
865
+ log(` ${path.relative(REPO_ROOT, target)} already records these fixtures as MEASURED,`);
866
+ log(' and this run measured none of them:');
867
+ log('');
868
+ for (const id of regressed) log(` ${id}`);
869
+ log('');
870
+ log(' Nothing was written. A record nobody re-measured is not a record that came');
871
+ log(' out empty, so replacing the one on disk would delete a measurement rather');
872
+ log(' than update it. `--prepare` clears each fixture\'s prior review artifact, so');
873
+ log(' a same-day re-ingest reaches this point whenever the live session has not');
874
+ log(' been re-run.');
875
+ log('');
876
+ log(' Re-run the live session (see --prepare), or pass --force to overwrite.');
877
+ log('');
878
+ return { target, text, observations, directions, written: false, regressed };
879
+ }
880
+
881
+ fs.mkdirSync(out, { recursive: true });
882
+ fs.writeFileSync(target, text.endsWith('\n') ? text : `${text}\n`, 'utf-8');
883
+
884
+ const leaked = reviewModule.scanForbiddenScoreTokens(text);
885
+
886
+ log('');
887
+ log('pp-core ► VAULT PRACTICE REVIEW CALIBRATION — ingest');
888
+ log('');
889
+ for (const o of observations) log(` ${o.status === OBSERVATION_STATUS.MEASURED ? 'measured ' : 'NOT MEASURED'} ${o.fixtureId}`);
890
+ log('');
891
+ log(` ${OUTCOME_LABEL[directions.one.outcome]} ${directions.one.label}`);
892
+ log(` ${OUTCOME_LABEL[directions.two.outcome]} ${directions.two.label}`);
893
+ log('');
894
+ if (directions.failed) {
895
+ log(` ${FAILURE_MARKER} — recorded, and nothing was disabled. A human decides.`);
896
+ log('');
897
+ }
898
+ if (leaked.length > 0) {
899
+ log(` WARNING: the record carries ${leaked.length} forbidden score form(s); D-06-13 forbids them.`);
900
+ log('');
901
+ }
902
+ // Relative when the record landed inside the repo, absolute when `--out` put
903
+ // it elsewhere: a `../../../..` prefix names the file without locating it.
904
+ const rel = path.relative(REPO_ROOT, target);
905
+ log(` Record: ${rel.startsWith('..') ? target : rel}`);
906
+ log('');
907
+
908
+ return { target, text, observations, directions, written: true, regressed };
909
+ }
910
+
911
+ // ─── CLI ─────────────────────────────────────────────────────────────────────
912
+
913
+ const USAGE = [
914
+ 'Usage: npm run vault:calibrate -- --prepare [--dir <path>]',
915
+ ' npm run vault:calibrate -- --ingest [--dir <path>] [--out <path>] [--force]',
916
+ '',
917
+ ' --prepare materialise the four graded fixture projects and print the operator instruction',
918
+ ' --ingest read the produced review artifacts, derive both directions, write the dated record',
919
+ ' --dir working directory for the fixture projects (default: ' + DEFAULT_WORK_DIR + ')',
920
+ ' --out record home (default: ' + RECORD_HOME_REL + ')',
921
+ ' --no-probe skip the vault-enabled derive probe during --prepare',
922
+ ' --force let --ingest overwrite a dated record that measured MORE than this run did',
923
+ '',
924
+ 'This harness never makes a model call. The agent runs happen in a live session.',
925
+ ].join('\n');
926
+
927
+ function parseArgs(argv) {
928
+ const opts = { mode: null, dir: DEFAULT_WORK_DIR, out: null, probe: true, force: false, unknown: [] };
929
+ for (let i = 0; i < argv.length; i += 1) {
930
+ const tok = argv[i];
931
+ if (tok === '--prepare' || tok === '--ingest') opts.mode = tok.slice(2);
932
+ else if (tok === '--dir') opts.dir = path.resolve(argv[(i += 1)] ?? '');
933
+ else if (tok === '--out') opts.out = path.resolve(argv[(i += 1)] ?? '');
934
+ else if (tok === '--no-probe') opts.probe = false;
935
+ else if (tok === '--force') opts.force = true;
936
+ else if (tok === '--help' || tok === '-h') opts.mode = 'help';
937
+ else opts.unknown.push(tok);
938
+ }
939
+ return opts;
940
+ }
941
+
942
+ function main(argv) {
943
+ const opts = parseArgs(argv);
944
+ if (opts.unknown.length > 0) {
945
+ console.log(`Unrecognised argument(s): ${opts.unknown.join(', ')} — nothing was calibrated.`);
946
+ console.log(USAGE);
947
+ return 0;
948
+ }
949
+ if (opts.mode === null || opts.mode === 'help') {
950
+ console.log(USAGE);
951
+ return 0;
952
+ }
953
+ if (opts.mode === 'prepare') {
954
+ runPrepare({ dir: opts.dir, probe: opts.probe });
955
+ return 0;
956
+ }
957
+ const ingested = runIngest({ dir: opts.dir, force: opts.force, ...(opts.out ? { out: opts.out } : {}) });
958
+ // A refusal is a command that did not do its job — it exits non-zero so an
959
+ // operator's `&&` chain stops. Nothing about the REVIEW tier gates on this;
960
+ // this is the harness declining to delete a measurement.
961
+ return ingested.written ? 0 : 1;
962
+ }
963
+
964
+ if (require.main === module) {
965
+ process.exitCode = main(process.argv.slice(2));
966
+ }
967
+
968
+ module.exports = {
969
+ CALIBRATION_PHASE_DIRNAME,
970
+ CALIBRATION_TRACE_ARTIFACT,
971
+ CALIBRATION_REVIEW_ARTIFACT,
972
+ RECORD_HOME_REL,
973
+ DEFAULT_WORK_DIR,
974
+ RECORD_SECTIONS,
975
+ PROMPT_SOURCES,
976
+ NEVER_AUTO_DISABLES,
977
+ LANE3_LIMIT,
978
+ FAILURE_MARKER,
979
+ OBSERVATION_STATUS,
980
+ NOT_MEASURED_GROUND,
981
+ DIRECTION_OUTCOME,
982
+ scaffoldCalibrationPhase,
983
+ prepareFixtures,
984
+ probeReviewArm,
985
+ observeFixture,
986
+ observeAllFixtures,
987
+ deriveDirections,
988
+ renderRecord,
989
+ recordFileName,
990
+ readRecordMeasuredIds,
991
+ runPrepare,
992
+ runIngest,
993
+ main,
994
+ /** The compiled leaf, re-exposed so a consumer harvests the shipped renderers rather than copying them. */
995
+ __reviewModule: reviewModule,
996
+ };