@pareto4/pp-core 1.13.0 → 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 (201) hide show
  1. package/.claude-plugin/plugin.json +3 -3
  2. package/README.md +26 -9
  3. package/agents/pp-code-reviewer.md +2 -1
  4. package/agents/pp-debug-session-manager.md +9 -6
  5. package/agents/pp-executor.md +10 -13
  6. package/agents/pp-plan-checker.md +4 -4
  7. package/agents/pp-planner.md +23 -9
  8. package/agents/pp-vault-reviewer.md +244 -21
  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 +1 -1
  13. package/commands/pp-plan-phase.md +1 -2
  14. package/commands/pp-vault-review.md +5 -3
  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 +396 -82
  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 +3334 -946
  35. package/pp-core/bin/lib/clusters.cjs +3 -0
  36. package/pp-core/bin/lib/command-aliases.cjs +0 -35
  37. package/pp-core/bin/lib/commands.cjs +9 -4
  38. package/pp-core/bin/lib/compose-bindings.cjs +251 -88
  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 +4 -4
  43. package/pp-core/bin/lib/discovery-vault-check.cjs +246 -0
  44. package/pp-core/bin/lib/frontmatter.cjs +1 -38
  45. package/pp-core/bin/lib/git-base-branch.cjs +13 -11
  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 +648 -738
  49. package/pp-core/bin/lib/init-interactive.cjs +76 -4
  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 +189 -129
  54. package/pp-core/bin/lib/org-profile.cjs +469 -152
  55. package/pp-core/bin/lib/package-identity.cjs +3 -3
  56. package/pp-core/bin/lib/phase-command-router.cjs +1 -29
  57. package/pp-core/bin/lib/phase-scope.cjs +1 -1
  58. package/pp-core/bin/lib/phase.cjs +11 -253
  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 +650 -126
  66. package/pp-core/bin/lib/vault-command-router.cjs +2604 -367
  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 +139 -72
  71. package/pp-core/bin/lib/vault-review.cjs +194 -451
  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 +607 -254
  75. package/pp-core/bin/lib/vault-update.cjs +9 -33
  76. package/pp-core/bin/lib/vault-write-back.cjs +70 -42
  77. package/pp-core/bin/lib/vault.cjs +2734 -920
  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 -54
  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 +2 -2
  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 -25
  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 +17 -34
  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 +20 -23
  100. package/pp-core/workflows/_runtime-launcher.snippet.sh +0 -0
  101. package/pp-core/workflows/add-tests.md +55 -4
  102. package/pp-core/workflows/audit-fix.md +63 -2
  103. package/pp-core/workflows/audit-milestone.md +1 -1
  104. package/pp-core/workflows/autonomous.md +85 -11
  105. package/pp-core/workflows/check-todos.md +13 -2
  106. package/pp-core/workflows/code-review-fix.md +59 -3
  107. package/pp-core/workflows/code-review.md +10 -1
  108. package/pp-core/workflows/complete-milestone.md +1 -1
  109. package/pp-core/workflows/debug.md +54 -4
  110. package/pp-core/workflows/discovery-sprint/prefill.md +19 -7
  111. package/pp-core/workflows/discovery-sprint/prototype.md +46 -49
  112. package/pp-core/workflows/discovery-sprint/session-13-gates.md +2 -0
  113. package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +1 -3
  114. package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +1 -3
  115. package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +1 -3
  116. package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +20 -24
  117. package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +1 -3
  118. package/pp-core/workflows/discovery-sprint/sessions/06-security.md +1 -3
  119. package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +1 -3
  120. package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +1 -3
  121. package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +1 -3
  122. package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +1 -6
  123. package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +1 -3
  124. package/pp-core/workflows/discovery-sprint/sessions/12-delivery-model.md +1 -3
  125. package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +1 -3
  126. package/pp-core/workflows/discovery-sprint/templates/context-template.md +1 -1
  127. package/pp-core/workflows/discovery-sprint/vault-context.md +213 -0
  128. package/pp-core/workflows/discovery-sprint.md +7 -3
  129. package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +4 -4
  130. package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +43 -1
  131. package/pp-core/workflows/discovery-to-build.md +10 -2
  132. package/pp-core/workflows/discuss-phase.md +3 -10
  133. package/pp-core/workflows/execute-phase/steps/check-query-gate.md +329 -0
  134. package/pp-core/workflows/execute-phase/steps/client-context-gate.md +3 -2
  135. package/pp-core/workflows/execute-phase/steps/cross-ai-delegation.md +1 -1
  136. package/pp-core/workflows/execute-phase/steps/execute-pre-dispatch.md +98 -0
  137. package/pp-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +94 -0
  138. package/pp-core/workflows/execute-phase/steps/post-merge-gate.md +6 -8
  139. package/pp-core/workflows/execute-phase/steps/vault-fetch.md +115 -14
  140. package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +322 -20
  141. package/pp-core/workflows/execute-phase/steps/verifier-practice-payload.md +165 -0
  142. package/pp-core/workflows/execute-phase/steps/verify-post-gate.md +163 -0
  143. package/pp-core/workflows/execute-phase.md +155 -128
  144. package/pp-core/workflows/execute-plan.md +56 -6
  145. package/pp-core/workflows/fast.md +53 -4
  146. package/pp-core/workflows/help/modes/full.md +3 -3
  147. package/pp-core/workflows/init/steps/client-context-capture.md +101 -0
  148. package/pp-core/workflows/init/steps/gitlab-provisioning.md +130 -0
  149. package/pp-core/workflows/init/steps/project-md-deferral-narration.md +66 -0
  150. package/pp-core/workflows/init/steps/reachability-outcome.md +70 -0
  151. package/pp-core/workflows/init.md +232 -272
  152. package/pp-core/workflows/new-project.md +31 -23
  153. package/pp-core/workflows/plan-phase.md +98 -45
  154. package/pp-core/workflows/quick.md +117 -3
  155. package/pp-core/workflows/secure-phase.md +1 -1
  156. package/pp-core/workflows/ship.md +57 -28
  157. package/pp-core/workflows/undo.md +1 -1
  158. package/pp-core/workflows/update.md +3 -3
  159. package/pp-core/workflows/validate-phase.md +58 -3
  160. package/pp-core/workflows/vault-review.md +282 -22
  161. package/pp-core/workflows/verify-work.md +12 -14
  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 -11
  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 +12 -10
  173. package/scripts/practice-arithmetic-contract.allow.json +35 -0
  174. package/scripts/prompt-injection-scan.sh +22 -17
  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 -483
  184. package/pp-core/bin/lib/install-run.cjs +0 -536
  185. package/pp-core/bin/lib/loop-gates.cjs +0 -213
  186. package/pp-core/bin/lib/phase-manifest.cjs +0 -527
  187. package/pp-core/bin/lib/phase-tier.cjs +0 -480
  188. package/pp-core/bin/lib/plan-graph.cjs +0 -368
  189. package/pp-core/references/executor-vault-trace.md +0 -47
  190. package/pp-core/references/planner-plan-graph.md +0 -66
  191. package/pp-core/workflows/discovery-sprint/vault-fetch.md +0 -37
  192. package/pp-core/workflows/execute-phase/steps/checkpoint-handling.md +0 -45
  193. package/pp-core/workflows/execute-phase/steps/install-run.md +0 -29
  194. package/pp-core/workflows/execute-phase/steps/phase-close-rebaseline.md +0 -23
  195. package/pp-core/workflows/execute-phase/steps/serial-group-dispatch.md +0 -73
  196. package/pp-core/workflows/execute-phase/steps/vault-door-gate.md +0 -19
  197. package/pp-core/workflows/plan-phase/steps/phase-tier.md +0 -43
  198. package/scripts/lib/size-budget.cjs +0 -311
  199. package/scripts/lint-neutrality-names.cjs +0 -694
  200. package/scripts/lint-org-coordinates.allowlist.json +0 -35
  201. package/scripts/lint-org-coordinates.cjs +0 -426
@@ -5,8 +5,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  /**
6
6
  * src/vault-trace.cts — the vault practice-trace envelope: the frozen
7
7
  * contract's constants, the statement builder, the frontmatter serializer,
8
- * the `## Practices Applied` renderer, the bespoke block parser, and the
9
- * pure validators a trace gate composes.
8
+ * the `## Practices Applied` renderer, and the bespoke block parser
9
+ * (Vault Assurance #71924, Phase 03-01).
10
10
  *
11
11
  * Responsibility (Phase 03-01 — task 1, the frozen half):
12
12
  * The five constants below ARE the contract. `docs/vault-trace-format.md`
@@ -17,10 +17,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
17
17
  * could move is not a version marker.
18
18
  *
19
19
  * Reuses (no new runtime dependency):
20
- * - `./vault.cjs` (`computeVaultVerdict`, `readManifest`) — the pin's state
21
- * is the vault verdict's, and its values are READ through the existing
22
- * manifest reader, never re-parsed here. A second reader would let the
23
- * trace disagree with the load gate about one cache.
20
+ * - `./vault.cjs` (`vaultStatus`, `readManifest`) — the pin is READ through
21
+ * the existing manifest readers, never re-parsed here. A second manifest
22
+ * parser would drift from the degrade path's prior-manifest spread and the
23
+ * trace would then disagree with the gate that validates it.
24
24
  * - `./vault-practices.cjs` (`matchPractices`, `boundedToken`) — the practice
25
25
  * denominator is the matcher's return set, and the id bounding is the
26
26
  * shared helper.
@@ -69,10 +69,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
69
69
  const node_path_1 = __importDefault(require("node:path"));
70
70
  const node_fs_1 = __importDefault(require("node:fs"));
71
71
  const node_crypto_1 = __importDefault(require("node:crypto"));
72
+ const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
72
73
  // eslint-disable-next-line @typescript-eslint/no-require-imports
73
74
  const vaultMod = require("./vault.cjs");
74
75
  // eslint-disable-next-line @typescript-eslint/no-require-imports
75
76
  const practicesMod = require("./vault-practices.cjs");
77
+ // 16-06 (D-35): the anchor grammar and the back-fill shape have ONE owner, the
78
+ // seal leaf. Static and cycle-free: the seal leaf's top level imports only
79
+ // `fs` and `path`, and it requires this module back lazily (D-41).
80
+ const vault_seal_cjs_1 = require("./vault-seal.cjs");
76
81
  // The count cap is IMPORTED, never re-declared. `derivePracticeUniverse` below
77
82
  // must bound the recorded universe by the same number `discoverPracticeFiles`
78
83
  // bounds its return set by; a second literal here would be a second cap that
@@ -110,18 +115,14 @@ function collapseControlChars(value) {
110
115
  */
111
116
  const TRACE_STATEMENT_TYPE = 'https://in-toto.io/Statement/v1';
112
117
  /**
113
- * The custom predicate type, and the MAJOR version carrier. pp-core's own
114
- * namespace: every trace this engine WRITES carries exactly this URI. The URI
115
- * is an identifier, not an endpoint — nothing fetches it.
116
- *
117
- * READERS accept this URI plus any URI the project's org profile lists under
118
- * `vault.trace.accepted_predicates` (`acceptedPredicateTypes`), so a project
119
- * that switches engine mid-engagement keeps reading the traces its earlier
120
- * engine wrote. An accepted URI must carry the same major as this one
121
- * (`/v<TRACE_SCHEMA_VERSION>`): the URI major and the schema version move
122
- * together, or not at all.
118
+ * The custom predicate type, and the MAJOR version carrier. The namespace is
119
+ * Pareto-owned: `github.com/pareto000/pp-core` is the canonical home of this
120
+ * distribution. The URI is an identifier, not an endpoint — nothing fetches
121
+ * it. A separate distribution of this capability would need its OWN predicate
122
+ * namespace rather than reusing this one, which would otherwise attribute
123
+ * every trace in the fleet to an organisation that did not produce it.
123
124
  */
124
- const TRACE_PREDICATE_TYPE = 'https://pareto.ooo/attestation/vault-practice-trace/v1';
125
+ const TRACE_PREDICATE_TYPE = 'https://github.com/pareto000/pp-core/attestation/vault-practice-trace/v1';
125
126
  /**
126
127
  * The schema version, mirroring the `PRACTICE_SCHEMA_VERSION` discipline in
127
128
  * `src/vault-practices.cts`. An integer, not a string. A consumer reading a
@@ -152,6 +153,159 @@ const TRACE_STATUS_VALUES = Object.freeze(['applied', 'not-applicable', 'deviate
152
153
  * calls and would return alternating results for the same input.
153
154
  */
154
155
  const TRACE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
156
+ // ─── The override: environment-only, by construction ──────────────────────────
157
+ /**
158
+ * The per-run override reason (GATE-07 / D-14). A REASON, deliberately — not a
159
+ * boolean: the reason IS the switch, so it is structurally impossible to take
160
+ * the override without stating one.
161
+ *
162
+ * An ENVIRONMENT value, never a config key, on EVERY path. This is
163
+ * load-bearing, not stylistic: a config-settable override would be reachable
164
+ * from the org-profile allowlist that plan 02-04 widened, and one package
165
+ * publish could then disable every gate in the fleet at once. Nothing in this
166
+ * section reads configuration, and the property is pinned by a test that
167
+ * writes this key at three config levels, proves each write is readable back,
168
+ * and still expects a null override.
169
+ *
170
+ * NOTE for the next reader: `src/check-command-router.cts` carries the
171
+ * Phase-2 copy of this reader (`evaluateVaultOverride`), which is not exported
172
+ * and which a leaf module must not import (adapters import leaves; leaves
173
+ * never import adapters). THIS is the canonical home going forward — the
174
+ * router's copy is to be rewired to call this one when the router is next
175
+ * touched. The duplication is recorded here so it is not silently forked.
176
+ */
177
+ const VAULT_OVERRIDE_ENV = 'PP_VAULT_OVERRIDE_REASON';
178
+ /** Long enough that a placeholder is uncomfortable to type and obvious in review. */
179
+ const VAULT_OVERRIDE_MIN_LENGTH = 20;
180
+ /**
181
+ * THE SINGLE HOME OF THE REASON RULES (GATE-07's triad: a length floor, at
182
+ * least one letter, not a repeated character). Extracted from
183
+ * `evaluateTraceOverride` by Phase 7 (plan 07-01, OD-2) so that the two
184
+ * places a bypass reason can enter the engine share one implementation:
185
+ *
186
+ * - the trace override — ENV-sourced, via `evaluateTraceOverride` below;
187
+ * - `/pp-init`'s reachability-proof deferral (D-07-03) — CLI-sourced, via
188
+ * `recordProbeDeferral` in `src/init-contract.cts`.
189
+ *
190
+ * The point of the extraction is that a future tightening of what counts as
191
+ * a real reason lands in ONE place and applies to both. Re-deriving these
192
+ * checks in a second module is what `tests/vault-status-check.test.cjs`'s
193
+ * SINGLE IMPLEMENTATION guard exists to catch; this function is the
194
+ * sanctioned way to reuse them.
195
+ *
196
+ * The caller owns trimming and owns where the string came from — this
197
+ * function judges a reason and nothing else.
198
+ */
199
+ function validateOverrideReason(reason) {
200
+ if (reason.length < VAULT_OVERRIDE_MIN_LENGTH) {
201
+ return {
202
+ state: 'rejected',
203
+ rejection: `the reason is ${reason.length} characters; at least ${VAULT_OVERRIDE_MIN_LENGTH} are required`,
204
+ };
205
+ }
206
+ if (!/[A-Za-z]/.test(reason)) {
207
+ return { state: 'rejected', rejection: 'the reason contains no letters' };
208
+ }
209
+ // A single character repeated ("aaaaaaaa…") is a placeholder, not a reason.
210
+ if (new Set(reason.replace(/\s/g, '')).size <= 2) {
211
+ return { state: 'rejected', rejection: 'the reason is a repeated character, not an explanation' };
212
+ }
213
+ return { state: 'accepted', reason };
214
+ }
215
+ function evaluateTraceOverride() {
216
+ const raw = process.env[VAULT_OVERRIDE_ENV];
217
+ if (typeof raw !== 'string' || raw.trim() === '')
218
+ return { state: 'absent' };
219
+ return validateOverrideReason(raw.trim());
220
+ }
221
+ /**
222
+ * The override reason is operator-supplied, not manifest-sourced, but it still
223
+ * becomes durable record content — so it gets the same newline/control-
224
+ * character collapse and length bound the enforcer's message applies.
225
+ */
226
+ function sanitizeOverrideReason(reason) {
227
+ const flat = reason.replace(/[\u0000-\u001F\u007F]+/g, ' ').replace(/\s+/g, ' ').trim();
228
+ return flat.length > 200 ? `${flat.slice(0, 200)}…` : flat;
229
+ }
230
+ /**
231
+ * Resolve who took the override (D-14's `who`). No existing helper in this
232
+ * repository resolves an author identity — `src/vault-write-back.cts` only
233
+ * SETS `user.name` on a throwaway clone — so this is new code, and its
234
+ * fallback chain is stated here rather than left to be inferred:
235
+ *
236
+ * 1. `git config user.name` in the project — the identity that will appear
237
+ * on the commits this run produces, so the override and the work it
238
+ * unblocked are attributable to the same name. Read through
239
+ * `execGitLocal` (D-116), so it is the cwd repository's identity, never
240
+ * that of a repository an inherited `GIT_DIR` names.
241
+ * 2. `GIT_AUTHOR_NAME`, then `USER`, then `USERNAME` — a CI runner often has
242
+ * no git identity configured but does export one of these.
243
+ * 3. A literal `unknown` marker. NEVER an empty string and never an omitted
244
+ * key: "we did not record who" must be readable as a fact, because an
245
+ * absent `who` and an unattributable one would otherwise look identical.
246
+ *
247
+ * Never throws: `execGitLocal` is a bounded `spawnSync` that reports ENOENT and
248
+ * timeouts as a result rather than an exception.
249
+ */
250
+ function resolveOverrideIdentity(cwd) {
251
+ try {
252
+ const res = (0, shell_command_projection_cjs_1.execGitLocal)(['config', 'user.name'], { cwd, timeout: 5_000 });
253
+ if (res.exitCode === 0 && res.stdout.trim() !== '')
254
+ return boundedToken(res.stdout.trim());
255
+ }
256
+ catch {
257
+ /* fall through to the environment */
258
+ }
259
+ for (const key of ['GIT_AUTHOR_NAME', 'USER', 'USERNAME']) {
260
+ const v = process.env[key];
261
+ if (typeof v === 'string' && v.trim() !== '')
262
+ return boundedToken(v.trim());
263
+ }
264
+ return 'unknown';
265
+ }
266
+ /**
267
+ * Build the override record, or null.
268
+ *
269
+ * Null in three distinct situations, and they are deliberately collapsed to
270
+ * one value because they share a meaning — "no bypass happened here":
271
+ * - no override context at all;
272
+ * - a context whose verdict did NOT block (recording one would manufacture
273
+ * evidence of a bypass that never happened);
274
+ * - an environment value that is absent or REFUSED (a refused override does
275
+ * not suppress anything — the block stands — so there is nothing to record).
276
+ */
277
+ function buildOverrideRecord(cwd, ctx, when) {
278
+ if (!ctx || ctx.block !== true)
279
+ return null;
280
+ const ov = evaluateTraceOverride();
281
+ if (ov.state !== 'accepted')
282
+ return null;
283
+ return {
284
+ gate: boundedToken(ctx.gate),
285
+ reason: sanitizeOverrideReason(ov.reason),
286
+ suppressedReason: boundedToken(ctx.suppressedReason),
287
+ who: resolveOverrideIdentity(cwd),
288
+ when,
289
+ };
290
+ }
291
+ /**
292
+ * The override record for a standards record (15-07, D-08): the SAME builder
293
+ * above, with the timestamp taken here, now. It exists so the standards block
294
+ * verb reaches `buildOverrideRecord` without that function joining the export
295
+ * surface, which two tests pin closed: a caller of this wrapper can no more
296
+ * choose `when` than a caller of `buildTraceStatement` can. The validation,
297
+ * sanitisation and identity chain are not restated; this is one call.
298
+ */
299
+ function buildStandardsOverrideRecord(cwd, ctx) {
300
+ return buildOverrideRecord(cwd, ctx, new Date().toISOString());
301
+ }
302
+ /**
303
+ * The eighth degraded cause's reason (quick 261015-at3, QD-05/QD-07). Contract
304
+ * surface, exported so the degraded-cause tests HARVEST it rather than restate
305
+ * it. Phrased outside `unprovablePairTail`'s miss/stale/tier substrings; it is
306
+ * routed to its own lead there (the repair is the mode, not a fetch).
307
+ */
308
+ const TRACE_REASON_STANDARDS_ONLY = 'vault holds standards (INDEX.md) and no practice files, but vault.mode is practices; no practice was read — set vault.mode to standards (pp-core 1.16.0 or later) and re-plan';
155
309
  const NULL_PIN = Object.freeze({
156
310
  ref: null,
157
311
  resolvedSha: null,
@@ -160,113 +314,210 @@ const NULL_PIN = Object.freeze({
160
314
  repo: null,
161
315
  });
162
316
  /**
163
- * The degraded reason sentences, one per cause. Contract surface: the format
164
- * page lists them, and a reader six months later needs to know WHICH failure
165
- * a degraded trace records. Every interpolated value is bounded.
166
- */
167
- const PIN_REASON = Object.freeze({
168
- noCache: 'no vault cache present at .planning/vault',
169
- manifestMissing: 'vault manifest missing at .planning/vault/.manifest.json',
170
- manifestMalformed: 'vault manifest unreadable or malformed at .planning/vault/.manifest.json',
171
- miss: 'vault manifest records a miss; no vault content was reached',
172
- empty: 'vault cache holds no files; no vault content was reached',
173
- noBindings: 'no client vault is bound (.platform-bindings.json needs client.vaultRepo and client.vaultRef); '
174
- + 'only a client-tier load at the configured ref records loaded',
175
- configUnreadable: 'the project config cannot be read, so the vault state is unknown',
176
- disabled: 'vault.enabled is not true',
177
- });
178
- /**
179
- * Read the pin. The STATE is the vault verdict's — `computeVaultVerdict` in
180
- * src/vault.cts is the one owner of "is the client vault loaded" (pack 6), so
181
- * the trace and the load gate cannot disagree about one cache: `loaded`
182
- * exactly when the verdict is `loaded` (the client tier, at the configured
183
- * repo and ref, fresh). The pin VALUES (ref, sha, tier, source, repo) are
184
- * read from the same cache's manifest through `readManifest`, which already
185
- * returns the display-safe repo; this module parses `.manifest.json` nowhere.
317
+ * Read the pin THROUGH the existing readers — `vaultStatus` for the freshness
318
+ * signal it already computes from `vault.cache_ttl_hours`, and `readManifest`
319
+ * for `repo`, which the status shape does not carry. This module opens
320
+ * `.manifest.json` nowhere: a second manifest parser would drift from the
321
+ * degrade path's prior-manifest spread, and the trace would then disagree with
322
+ * the gate that validates it. For the same reason it reads the CONFIGURED
323
+ * coordinates nowhere either — the drift arm below asks the verdict leaf's
324
+ * `resolveRefDrift`, the one owner of that predicate (D-08-35).
186
325
  *
187
326
  * A degraded run is a FIRST-CLASS path, not an error branch — the caller
188
- * always gets a pin object, and a statement is always produced. A stale,
189
- * fallback-tier or drifted cache keeps its pin: the cached copy really did
190
- * come from that SHA, and only the reason says why it does not prove the
191
- * configured standards were applied.
192
- *
193
- * WORKTREES. `vaultDirPath` is the directory `buildTraceStatement` resolved
194
- * once through `vaultMod.resolveVaultCacheDir`: the local `.planning/vault`,
195
- * or, for the root of a linked git worktree with no local manifest, the
196
- * owning main checkout's cache. The verdict is judged against that same
197
- * directory.
327
+ * always gets a pin object, and a statement is always produced.
328
+ *
329
+ * WORKTREES (260915-ms2, UAT-6-F3). `vaultDirPath` is the directory
330
+ * `buildTraceStatement` resolved ONCE through `vaultMod.resolveVaultCacheDir`:
331
+ * the local `<cwd>/.planning/vault`, or — only for the root of a linked git
332
+ * worktree with no local manifest — the owning main checkout's cache, located
333
+ * with `git rev-parse --git-common-dir`. A main checkout's own `.git` is a
334
+ * directory, so it never asks git and can never recurse into itself. A project
335
+ * whose main checkout has no cache still resolves the local path and degrades
336
+ * with the same reasons as before. The three reason strings below are
337
+ * deliberately unchanged whichever directory was read: they are contract
338
+ * surface pinned by the frozen document's degraded-cause list.
339
+ *
340
+ * FIVE reason strings now, not three (D-08-25, then D-08-34): the tier arm's
341
+ * sentence and the ref-drift arm's join them as contract surface, pinned by the
342
+ * same degraded-cause list and by the tail's seven-reason loop. The three that
343
+ * predate them are unchanged — they are quoted verbatim in two other suites and
344
+ * in the frozen document.
345
+ *
346
+ * EIGHT degraded causes in all (quick 261015-at3 adds the last,
347
+ * `TRACE_REASON_STANDARDS_ONLY`): a client-tier cache at the configured ref,
348
+ * fresh, that holds standards (INDEX.md) and no practice files, read in
349
+ * practices mode. It sits LAST, after the stale arm and immediately before
350
+ * `loaded` (QD-05): every earlier arm says the cache is not the configured
351
+ * client vault, or not a current copy of it; this one says it IS, read in a
352
+ * mode it cannot serve. The union stays frozen (A3).
198
353
  */
199
354
  function resolvePin(cwd, vaultDirPath = node_path_1.default.join(cwd, '.planning', 'vault')) {
200
- const v = vaultMod.computeVaultVerdict(cwd, { vaultDir: vaultDirPath });
201
- const manifest = vaultMod.readManifest(vaultDirPath);
202
- const str = (x) => (typeof x === 'string' && x !== '' ? x : null);
203
- const pin = manifest
204
- ? {
205
- ref: str(manifest.ref),
206
- resolvedSha: str(manifest.resolvedSha),
207
- tier: str(manifest.tier),
208
- source: str(manifest.source),
209
- repo: vaultMod.redactCloneTarget(str(manifest.repo)),
355
+ const status = vaultMod.vaultStatus(cwd, { vaultDir: vaultDirPath });
356
+ if (!status.present) {
357
+ // `vaultStatus` collapses every problem to `{present:false}`, so name the
358
+ // actual failure here — an operator reading a degraded trace six months
359
+ // later needs to know WHICH of these it was.
360
+ let reason;
361
+ if (!node_fs_1.default.existsSync(vaultDirPath)) {
362
+ reason = 'no vault cache present at .planning/vault';
363
+ }
364
+ else if (!node_fs_1.default.existsSync(node_path_1.default.join(vaultDirPath, '.manifest.json'))) {
365
+ reason = 'vault manifest missing at .planning/vault/.manifest.json';
210
366
  }
211
- : { ...NULL_PIN };
212
- if (v.verdict === 'loaded') {
367
+ else {
368
+ reason = 'vault manifest unreadable or malformed at .planning/vault/.manifest.json';
369
+ }
370
+ return { state: 'degraded', reason, ...NULL_PIN, manifestFiles: null };
371
+ }
372
+ const manifest = vaultMod.readManifest(vaultDirPath);
373
+ const pin = {
374
+ ref: status.ref ?? null,
375
+ resolvedSha: status.resolvedSha ?? null,
376
+ tier: status.tier ?? null,
377
+ source: status.source ?? null,
378
+ repo: manifest && manifest.repo ? manifest.repo : null,
379
+ };
380
+ // One owner for the miss predicate (261003-jby's `manifestRecordsMiss`; this
381
+ // copy adopted it in 261003-j89 as the follow-up that change recorded).
382
+ if (vaultMod.manifestRecordsMiss(status)) {
213
383
  return {
214
- state: 'loaded',
215
- reason: null,
384
+ state: 'degraded',
385
+ reason: 'vault manifest records a miss; no vault content was reached',
216
386
  ...pin,
217
- manifestFiles: manifest && Array.isArray(manifest.files)
218
- ? manifest.files.filter((f) => typeof f === 'string')
219
- : [],
387
+ manifestFiles: null,
220
388
  };
221
389
  }
222
- let reason;
223
- switch (v.reason) {
224
- case 'never_fetched':
225
- if (!node_fs_1.default.existsSync(vaultDirPath))
226
- reason = PIN_REASON.noCache;
227
- else if (!node_fs_1.default.existsSync(node_path_1.default.join(vaultDirPath, '.manifest.json')))
228
- reason = PIN_REASON.manifestMissing;
229
- else
230
- reason = PIN_REASON.manifestMalformed;
231
- break;
232
- case 'fetch_miss':
233
- reason = vaultMod.manifestRecordsMiss(manifest) ? PIN_REASON.miss : PIN_REASON.empty;
234
- break;
235
- case 'fallback_tier':
236
- reason =
237
- `vault loaded on the ${boundedToken(pin.tier ?? 'unknown')} tier `
238
- + `(${boundedToken(pin.repo ?? 'no repo recorded')}); no client standards were applied `
239
- + '— only a client-tier load records loaded';
240
- break;
241
- case 'ref_drift':
242
- // Both refs come FIRST so they survive an 80-character head, and both go
243
- // through `boundedToken`: a ref is project-authored text reaching an
244
- // agent's context unfenced.
245
- reason = pin.ref !== v.configuredRef
246
- ? `vault cache was fetched from ref ${boundedToken(pin.ref ?? 'unknown')} `
247
- + `but client.vaultRef is now ${boundedToken(v.configuredRef ?? 'unknown')}; `
248
- + 'the cached standards are not the configured ones — only a client-tier load '
249
- + 'at the configured ref records loaded'
250
- : `vault cache was fetched from ${boundedToken(pin.repo ?? 'unknown')} `
251
- + `but client.vaultRepo is now ${boundedToken(v.configuredRepo ?? 'unknown')}; `
252
- + 'the cached standards are not the configured ones — only a client-tier load '
253
- + 'at the configured repo records loaded';
254
- break;
255
- case 'stale_cache':
256
- reason =
257
- `vault cache is stale (fetched ${boundedToken(str(manifest?.fetchedAt) ?? 'unknown')}); `
258
- + 'it exceeds vault.cache_ttl_hours';
259
- break;
260
- case 'no_bindings':
261
- reason = PIN_REASON.noBindings;
262
- break;
263
- case 'config_unreadable':
264
- reason = PIN_REASON.configUnreadable;
265
- break;
266
- default:
267
- reason = PIN_REASON.disabled;
390
+ // D-08-25 / HGATE-08 (RESEARCH M8, reproduced live through this same
391
+ // writer). `state` tracked STALENESS only, so every present, non-miss,
392
+ // fresh manifest returned `loaded` whatever tier had actually loaded — and
393
+ // arms 9, 10 and 12 of the v1.0 UAT duly wrote `state: loaded, reason: null`,
394
+ // the strongest statement this schema can make, for a load that applied NO
395
+ // client standards. The shared vault being reachable is not the client's
396
+ // vault being governed.
397
+ //
398
+ // ORDER, BOTH SIDES. After the miss arm: a miss reached no vault at all and
399
+ // so has no tier worth naming — "loaded on the unknown tier" would misreport
400
+ // a fetch that never landed. Before the stale arm: the tier is the STRONGER
401
+ // fact. A stale fallback cache is still not the client's vault, and its age
402
+ // is the lesser of the two problems; the load gate's own suffix keeps
403
+ // reporting that age, so nothing is lost by leading with the tier here.
404
+ //
405
+ // The pin SURVIVES, for the stale arm's reason below: the shared vault
406
+ // really was read at that SHA.
407
+ //
408
+ // The union stays FROZEN at `'loaded' | 'degraded'` (Assumption A3). A third
409
+ // state would be a schema-version change and would leave every existing
410
+ // consumer — `VAULT_STATE_INVALID`, the strict coverage gate — with a value
411
+ // it has no rule for. Routing the tier to `degraded` with a reason that
412
+ // names it is the smaller change and the honest one: nothing here is proven.
413
+ if (status.tier !== 'client') {
414
+ return {
415
+ state: 'degraded',
416
+ reason: `vault loaded on the ${boundedToken(status.tier ?? 'unknown')} tier `
417
+ + `(${boundedToken(pin.repo ?? 'no repo recorded')}); no client standards were applied `
418
+ + '— only a client-tier load records loaded',
419
+ ...pin,
420
+ manifestFiles: null,
421
+ };
422
+ }
423
+ // D-08-34 / HGATE-08 (UAT F5, reproduced live through this same writer).
424
+ // `state` knew about the manifest's presence, its source, its tier and its
425
+ // age — and nothing about the ref the project CONFIGURES. So a client-tier
426
+ // cache fetched from a ref the project has since re-pinned recorded
427
+ // `state: loaded, reason: null`, byte-identical to the no-drift control: the
428
+ // strongest statement this schema can make, for a vault that is not the
429
+ // client's currently configured standards. Measured under `vault.required:
430
+ // false` — the SCHEMA DEFAULT, and the posture where D-08-29 keeps the status
431
+ // envelope thin, leaving this trace as the only durable record of the load.
432
+ //
433
+ // The tier arm's own principle applies unchanged: a cache from an
434
+ // unconfigured ref is the same class of untruth as a reachable shared vault.
435
+ //
436
+ // ORDER, BOTH SIDES. AFTER the tier arm, because a fallback manifest's `ref`
437
+ // belongs to the SHARED vault and is not a client pin to drift from — sending
438
+ // that reader to fetch a configured ref would point them at the wrong vault
439
+ // entirely. (The owner's TIER gate answers `false` there anyway, so this is
440
+ // belt and braces; the ORDER test pins it so a reorder is loud.) BEFORE the
441
+ // stale arm, on D-08-23's ground, quoted verbatim from the verdict: drift is
442
+ // reported ahead of staleness "because it is the more specific fact — a stale
443
+ // cache from a ref the project has re-pinned is not made relevant by
444
+ // re-fetching that same ref". The verdict already orders them that way, and
445
+ // two surfaces telling different stories about one cache is the defect class
446
+ // D-08-35 exists to close.
447
+ //
448
+ // The pin SURVIVES, for the stale arm's reason below: the cache really did
449
+ // come from that SHA. "degraded" is not "no pin".
450
+ //
451
+ // The union stays FROZEN at `'loaded' | 'degraded'` (Assumption A3), so
452
+ // `VAULT_STATE_INVALID` and the strict coverage gate need no new rule —
453
+ // both key on `state`, neither enumerates causes.
454
+ //
455
+ // THE OWNER IS CALLED THROUGH `vaultMod` AT CALL TIME, not destructured at
456
+ // module scope, and that is deliberate rather than an oversight a later tidy-
457
+ // up should "fix": it is what lets `SEAM (D-08-35)` stub the owner and prove
458
+ // this arm consumes it. A behaviour test cannot tell one owner from two
459
+ // identical copies, and this phase has shipped that exact defect four times.
460
+ // `computeVaultVerdict` is NOT called here and must not be: since 08-10 it
461
+ // short-circuits on `vault.enabled` and resolves `vault.required`, and a trace
462
+ // that consulted policy would stop recording drift on exactly the
463
+ // default-config projects F5 was measured on.
464
+ const drift = vaultMod.resolveRefDrift({
465
+ cwd, tier: status.tier ?? null, cachedRef: status.ref ?? null,
466
+ });
467
+ if (drift.refDrift) {
468
+ return {
469
+ state: 'degraded',
470
+ // Both refs come FIRST so they survive the 80-character head the
471
+ // strict-point tail is budgeted against, and both go through
472
+ // `boundedToken` because a ref is project-authored text reaching an
473
+ // agent's context unfenced (T-08-54). Phrased outside `unprovablePairTail`'s
474
+ // three classifier substrings on purpose (D-08-36): this cause belongs to
475
+ // the re-fetch-first lead, because `vault fetch` reads the CONFIGURED ref.
476
+ reason: `vault cache was fetched from ref ${boundedToken(status.ref ?? 'unknown')} `
477
+ + `but client.vaultRef is now ${boundedToken(drift.configuredRef ?? 'unknown')}; `
478
+ + 'the cached standards are not the configured ones — only a client-tier load '
479
+ + 'at the configured ref records loaded',
480
+ ...pin,
481
+ manifestFiles: null,
482
+ };
483
+ }
484
+ if (status.stale === true) {
485
+ // The pin SURVIVES a stale cache: the cached copy really did come from
486
+ // that SHA. "degraded" is not "no pin", and a validator that nulled the
487
+ // pin here would lose the only provable thing about the run.
488
+ return {
489
+ state: 'degraded',
490
+ reason: `vault cache is stale (fetched ${boundedToken(status.fetchedAt ?? 'unknown')}); it exceeds vault.cache_ttl_hours`,
491
+ ...pin,
492
+ manifestFiles: null,
493
+ };
494
+ }
495
+ // Quick 261015-at3 (QD-05, Vince 2026-10-15): the configured client vault,
496
+ // fresh, read in a mode it cannot serve — a standards vault (INDEX.md, no
497
+ // practice files) read in practices mode. Before this arm it recorded
498
+ // `loaded` with an empty universe: the strongest statement the schema allows,
499
+ // for a run in which no practice was read. The pin SURVIVES (the cache really
500
+ // was read at that SHA); the universe does not (D-04).
501
+ //
502
+ // THE OWNER IS CALLED THROUGH `practicesMod` AT CALL TIME, on the D-08-35
503
+ // seam argument verbatim: a test stubs `discoverPracticeFiles` and proves this
504
+ // arm consumes the owner rather than a re-derived copy of the predicate.
505
+ if (practicesMod.discoverPracticeFiles(cwd, vaultDirPath).vaultStandardsOnly === true) {
506
+ return {
507
+ state: 'degraded',
508
+ reason: TRACE_REASON_STANDARDS_ONLY,
509
+ ...pin,
510
+ manifestFiles: null,
511
+ };
268
512
  }
269
- return { state: 'degraded', reason, ...pin, manifestFiles: null };
513
+ return {
514
+ state: 'loaded',
515
+ reason: null,
516
+ ...pin,
517
+ manifestFiles: manifest && Array.isArray(manifest.files)
518
+ ? manifest.files.filter((f) => typeof f === 'string')
519
+ : [],
520
+ };
270
521
  }
271
522
  // ─── The universe ──────────────────────────────────────────────────────────────
272
523
  /** The manifest-relative directory prefix under which practice files live. */
@@ -497,83 +748,10 @@ function deriveCheckBearingCensus(universe, facts) {
497
748
  }
498
749
  return checkBearing;
499
750
  }
500
- /** At most this many PLAN/SUMMARY artifacts are read for the census. */
501
- const CENSUS_ARTIFACT_CAP = 200;
502
- /** Each artifact is read whole or not at all, up to this many bytes. */
503
- const CENSUS_ARTIFACT_BYTE_CAP = 1024 * 1024;
504
- /**
505
- * Read the `check:` census this phase's ledgers RECORDED — never re-derive it.
506
- *
507
- * The census is taken once, by `buildTraceStatement`, against the pinned vault
508
- * the plan or summary was written under. A consumer (the mechanical audit)
509
- * reads that record back here instead of counting the vault again: a second
510
- * count could describe a different vault than the ledger does.
511
- *
512
- * Execute-stage ledgers (SUMMARY.md) are preferred over plan-stage ones, and
513
- * within a stage the last artifact in name order — the latest plan — is the
514
- * one reported. `distinct` counts how many different censuses the phase's
515
- * ledgers record, so a vault that moved during the phase is visible. A ledger
516
- * with no census (a degraded run) is skipped. `null` when no ledger records
517
- * one. Pure read; never throws.
518
- */
519
- function readRecordedCensus(phaseDir) {
520
- try {
521
- const names = node_fs_1.default
522
- .readdirSync(phaseDir)
523
- .filter((name) => /(^|-)(PLAN|SUMMARY)\.md$/.test(name))
524
- .sort()
525
- .slice(0, CENSUS_ARTIFACT_CAP);
526
- const found = [];
527
- for (const name of names) {
528
- const abs = node_path_1.default.join(phaseDir, name);
529
- let content;
530
- try {
531
- const stat = node_fs_1.default.lstatSync(abs);
532
- if (!stat.isFile() || stat.size > CENSUS_ARTIFACT_BYTE_CAP)
533
- continue;
534
- content = node_fs_1.default.readFileSync(abs, 'utf8');
535
- }
536
- catch {
537
- continue;
538
- }
539
- const parsed = parseVaultTraceBlock(content, name);
540
- const vault = parsed.present && parsed.statement ? parsed.statement.predicate?.vault : null;
541
- if (!vault)
542
- continue;
543
- const census = vault.checkBearingPractices;
544
- if (typeof census !== 'number' || !Number.isInteger(census) || census < 0)
545
- continue;
546
- if (!Array.isArray(vault.practiceUniverse))
547
- continue;
548
- found.push({
549
- census,
550
- universe: vault.practiceUniverse.length,
551
- sha: typeof vault.resolvedSha === 'string' ? vault.resolvedSha : null,
552
- artifact: name,
553
- execute: /(^|-)SUMMARY\.md$/.test(name),
554
- });
555
- }
556
- if (found.length === 0)
557
- return null;
558
- const pool = found.some((f) => f.execute) ? found.filter((f) => f.execute) : found;
559
- const chosen = pool[pool.length - 1];
560
- const distinct = new Set(found.map((f) => `${f.census}/${f.universe}/${f.sha ?? ''}`)).size;
561
- return {
562
- checkBearingPractices: chosen.census,
563
- practiceUniverse: chosen.universe,
564
- resolvedSha: chosen.sha,
565
- artifact: chosen.artifact,
566
- distinct,
567
- };
568
- }
569
- catch {
570
- return null;
571
- }
572
- }
573
751
  // ─── The builder ───────────────────────────────────────────────────────────────
574
752
  /**
575
753
  * The statement note recorded when the cache was read from the owning main git
576
- * worktree (worktree executors). A fact, never a path.
754
+ * worktree (260915-ms2, UAT-6-F3). A fact, never a path.
577
755
  */
578
756
  const TRACE_WORKTREE_CACHE_NOTE = 'vault cache read from the owning main git worktree; this checkout carries no local .planning/vault';
579
757
  /**
@@ -589,7 +767,7 @@ function buildTraceStatement(opts) {
589
767
  const files = Array.isArray(opts.files) ? opts.files : [];
590
768
  const notes = [];
591
769
  const generatedAt = new Date().toISOString();
592
- // ONE cache directory for the pin AND the matcher (worktree executors), so
770
+ // ONE cache directory for the pin AND the matcher (260915-ms2, UAT-6-F3), so
593
771
  // the practice set and the ref it is recorded against come from one vault.
594
772
  const cache = vaultMod.resolveVaultCacheDir(cwd);
595
773
  if (cache.source === 'main-worktree') {
@@ -670,7 +848,7 @@ function buildTraceStatement(opts) {
670
848
  const clause = pinnedFacts.clauseById.get(id);
671
849
  if (typeof clause === 'string' && clause !== '')
672
850
  entry.enforcedBy = clause;
673
- // THE TEXT DIGEST (D-05-07). Digested from the SAME unwrapped excerpt
851
+ // THE TEXT DIGEST (261003-j89). Digested from the SAME unwrapped excerpt
674
852
  // `renderPracticeEntry` places under `ask:`/`excerpt:` in the body block
675
853
  // — `unwrapFencedExcerpt` is the practices module's own, exported for
676
854
  // exactly this, so the two readers cannot strip the fence differently.
@@ -691,6 +869,18 @@ function buildTraceStatement(opts) {
691
869
  // never worth losing to an unexpected read failure.
692
870
  notes.push(`practice matching failed: ${boundedToken(e.message)}`);
693
871
  }
872
+ // 16-06: the anchors sit right after `generatedAt`, the order the serializer
873
+ // writes them in; the spread of an empty object adds no key, so a plan-stage
874
+ // statement is the object it was before.
875
+ const anchorFields = {};
876
+ if (stage === 'execute' && opts.anchors) {
877
+ if (typeof opts.anchors.startCommit === 'string' && vault_seal_cjs_1.ANCHOR_SHA_RE.test(opts.anchors.startCommit)) {
878
+ anchorFields.startCommit = opts.anchors.startCommit;
879
+ }
880
+ if (typeof opts.anchors.endCommit === 'string' && vault_seal_cjs_1.ANCHOR_SHA_RE.test(opts.anchors.endCommit)) {
881
+ anchorFields.endCommit = opts.anchors.endCommit;
882
+ }
883
+ }
694
884
  return {
695
885
  _type: TRACE_STATEMENT_TYPE,
696
886
  predicateType: TRACE_PREDICATE_TYPE,
@@ -700,6 +890,7 @@ function buildTraceStatement(opts) {
700
890
  stage,
701
891
  plan,
702
892
  generatedAt,
893
+ ...anchorFields,
703
894
  vault: {
704
895
  state: pin.state,
705
896
  reason: pin.reason,
@@ -713,35 +904,10 @@ function buildTraceStatement(opts) {
713
904
  },
714
905
  practices,
715
906
  notes,
716
- override: null,
907
+ override: buildOverrideRecord(cwd, opts.override, generatedAt),
717
908
  },
718
909
  };
719
910
  }
720
- // ─── The reader allowlist ──────────────────────────────────────────────────────
721
- /**
722
- * The predicate types a trace READER accepts for the project at `cwd`:
723
- * `TRACE_PREDICATE_TYPE` first, then each URI the org profile lists under
724
- * `vault.trace.accepted_predicates` that carries the same major
725
- * (`/v<TRACE_SCHEMA_VERSION>`). A listed URI of another major is ignored —
726
- * the URI major and the schema version move together. Never throws; writers
727
- * never call it.
728
- */
729
- function acceptedPredicateTypes(cwd) {
730
- const out = [TRACE_PREDICATE_TYPE];
731
- try {
732
- // eslint-disable-next-line @typescript-eslint/no-require-imports
733
- const orgProfile = require('./org-profile.cjs');
734
- const majorSuffix = `/v${TRACE_SCHEMA_VERSION}`;
735
- for (const uri of orgProfile.resolveAcceptedTracePredicates(cwd)) {
736
- if (typeof uri === 'string' && uri.endsWith(majorSuffix) && !out.includes(uri))
737
- out.push(uri);
738
- }
739
- }
740
- catch {
741
- /* no profile reader: pp-core's own type only */
742
- }
743
- return out;
744
- }
745
911
  // ─── The serializer ────────────────────────────────────────────────────────────
746
912
  const INDENT = ' ';
747
913
  /**
@@ -820,6 +986,12 @@ function serializeTraceBlock(statement) {
820
986
  lines.push(scalarLine(2, 'stage', p.stage));
821
987
  lines.push(scalarLine(2, 'plan', p.plan));
822
988
  lines.push(scalarLine(2, 'generatedAt', p.generatedAt));
989
+ // 16-06 (D-35): the anchors, right after `generatedAt`. NOTHING is emitted
990
+ // for an absent key, so a plan-stage ledger and a pre-16 ledger keep their
991
+ // bytes (golden-pinned). Re-validated at emit time on the id filter's rule:
992
+ // a value that is not an anchor is dropped WITH a note, never written as a
993
+ // commit a later gate would place in a git argv.
994
+ lines.push(...anchorLines(p, notes));
823
995
  lines.push(`${INDENT.repeat(2)}vault:`);
824
996
  lines.push(scalarLine(3, 'state', p.vault.state));
825
997
  lines.push(scalarLine(3, 'reason', p.vault.reason));
@@ -902,7 +1074,7 @@ function serializeTraceBlock(statement) {
902
1074
  const clause = defangFenceMarkers(renderSafeField(entry.enforcedBy));
903
1075
  if (clause !== '')
904
1076
  lines.push(scalarLine(4, 'enforcedBy', clause));
905
- // The text digest (D-05-07), between `enforcedBy` and `tasks`. NOTHING
1077
+ // The text digest (261003-j89), between `enforcedBy` and `tasks`. NOTHING
906
1078
  // is emitted when the entry carries none, so a ledger written from a
907
1079
  // statement without digests keeps the shape it had. Re-validated at emit
908
1080
  // time on the same rule as the id filter: only a 64-character lowercase
@@ -943,6 +1115,60 @@ function serializeTraceBlock(statement) {
943
1115
  // `collectBlockLines` already skips.
944
1116
  return `${lines.join('\n')}\n`;
945
1117
  }
1118
+ /**
1119
+ * The predicate's anchor lines (16-06), in the one order they are written and
1120
+ * back-filled: `startCommit`, `endCommit`, then the `anchorBackfill` block.
1121
+ * Exported (as `traceAnchorLines`) so `vault anchor backfill` splices exactly
1122
+ * the lines the serializer would have written, never a second spelling.
1123
+ */
1124
+ function anchorLines(p, notes) {
1125
+ const out = [];
1126
+ for (const key of ['startCommit', 'endCommit']) {
1127
+ const v = p[key];
1128
+ if (v === undefined || v === null)
1129
+ continue;
1130
+ if (typeof v === 'string' && vault_seal_cjs_1.ANCHOR_SHA_RE.test(v))
1131
+ out.push(scalarLine(2, key, v));
1132
+ else
1133
+ notes.push(`${key} dropped at serialize time: not a full lowercase commit id`);
1134
+ }
1135
+ const b = p.anchorBackfill;
1136
+ if (b !== undefined && b !== null) {
1137
+ if ((0, vault_seal_cjs_1.isAnchorBackfill)(b)) {
1138
+ out.push(`${INDENT.repeat(2)}anchorBackfill:`);
1139
+ out.push(scalarLine(3, 'by', b.by));
1140
+ out.push(scalarLine(3, 'at', b.at));
1141
+ }
1142
+ else {
1143
+ notes.push('anchorBackfill dropped at serialize time: not { by, at }');
1144
+ }
1145
+ }
1146
+ return out;
1147
+ }
1148
+ /**
1149
+ * The anchor lines a back-fill splices into an existing ledger, after its
1150
+ * `generatedAt` line (16-06, D-32). The SAME `anchorLines` the serializer
1151
+ * uses, so a later `trace render` round trip of the back-filled ledger and a
1152
+ * fresh serialization of the same statement agree. Throws on a value that
1153
+ * would be dropped: the back-fill verb validated it first, so a throw is an
1154
+ * engine bug.
1155
+ */
1156
+ function traceAnchorLines(anchors) {
1157
+ const notes = [];
1158
+ const out = anchorLines(anchors, notes);
1159
+ if (notes.length > 0)
1160
+ throw new Error(`traceAnchorLines: ${notes[0]}`);
1161
+ return out;
1162
+ }
1163
+ /**
1164
+ * The `by` a back-fill records: the trace's one identity chain
1165
+ * (`resolveOverrideIdentity`: `git config user.name`, then the environment,
1166
+ * then `unknown`), normalised by the seal leaf so it is accepted by both record
1167
+ * families' parsers.
1168
+ */
1169
+ function resolveBackfillIdentity(cwd) {
1170
+ return (0, vault_seal_cjs_1.normaliseAnchorBackfillWho)(resolveOverrideIdentity(cwd));
1171
+ }
946
1172
  // ─── The rendered surface ──────────────────────────────────────────────────────
947
1173
  const PRACTICES_APPLIED_HEADING = '## Practices Applied';
948
1174
  /**
@@ -979,6 +1205,12 @@ function renderSafeField(value) {
979
1205
  ? `${flat.slice(0, TRACE_RENDERED_FIELD_MAX)}…`
980
1206
  : flat;
981
1207
  }
1208
+ /**
1209
+ * The rendered line for an empty ledger whose trace is DEGRADED (quick
1210
+ * 261015-at3). Constant text: the recorded `reason` is artifact text and is not
1211
+ * echoed into a section another agent reads unfenced.
1212
+ */
1213
+ const TRACE_DEGRADED_RENDER_LINE = 'No client-vault practice was read for this plan: its recorded vault trace is degraded (see its `reason`), so no practice is recorded here as applied or as out of scope.';
982
1214
  /**
983
1215
  * Render the `## Practices Applied` section from the ledger.
984
1216
  *
@@ -995,6 +1227,15 @@ function renderPracticesApplied(statement) {
995
1227
  const entries = statement?.predicate?.practices ?? [];
996
1228
  const lines = [PRACTICES_APPLIED_HEADING, ''];
997
1229
  if (entries.length === 0) {
1230
+ // Quick 261015-at3 (entry-point sweep): a DEGRADED ledger is empty because
1231
+ // no practice was read, not because none was in scope — "were in scope" is
1232
+ // a claim about a practice set that was never computed, written into a
1233
+ // SUMMARY on every one of the eight degraded causes. The empty-and-loaded
1234
+ // line below is unchanged (pinned byte-identical).
1235
+ if (statement?.predicate?.vault?.state === 'degraded') {
1236
+ lines.push(TRACE_DEGRADED_RENDER_LINE);
1237
+ return lines.join('\n');
1238
+ }
998
1239
  // Never an empty section: D-04 requires a degraded or empty run to be
999
1240
  // READABLE as a fact. An omitted section and an unrecorded one look
1000
1241
  // identical to every downstream reader.
@@ -1295,7 +1536,7 @@ function parseVaultTraceBlock(content, artifactPath = 'the artifact') {
1295
1536
  // parsed into then. An `undefined` property would not be that.
1296
1537
  if (typeof p.enforcedBy === 'string' && p.enforcedBy !== '')
1297
1538
  entry.enforcedBy = p.enforcedBy;
1298
- // Optional (D-05-07): reconstructed ONLY when the document carried it,
1539
+ // Optional (261003-j89): reconstructed ONLY when the document carried it,
1299
1540
  // on the same reasoning as `enforcedBy` above.
1300
1541
  if (typeof p.textSha256 === 'string' && p.textSha256 !== '')
1301
1542
  entry.textSha256 = p.textSha256;
@@ -1340,6 +1581,33 @@ function parseVaultTraceBlock(content, artifactPath = 'the artifact') {
1340
1581
  when: asString(o.when),
1341
1582
  };
1342
1583
  }
1584
+ // 16-06 (D-35): the optional anchors, reconstructed ONLY when the document
1585
+ // carried them and they pass their grammar (the `enforcedBy` precedent), so a
1586
+ // ledger written before 16-06 parses into the exact statement it did then. A
1587
+ // key that is present but fails is DROPPED, and its NAME (never its value)
1588
+ // is listed in `anchorsDropped`, so the validator can type it
1589
+ // (`anchor_invalid`) and no caller ever reads it as a commit.
1590
+ const anchorFields = {};
1591
+ const dropped = [];
1592
+ for (const key of ['startCommit', 'endCommit']) {
1593
+ if (!(key in predicate))
1594
+ continue;
1595
+ const v = predicate[key];
1596
+ if (typeof v === 'string' && vault_seal_cjs_1.ANCHOR_SHA_RE.test(v))
1597
+ anchorFields[key] = v;
1598
+ else
1599
+ dropped.push(key);
1600
+ }
1601
+ if ('anchorBackfill' in predicate) {
1602
+ const b = asObject(predicate.anchorBackfill);
1603
+ const candidate = { by: b.by, at: b.at };
1604
+ if ((0, vault_seal_cjs_1.isAnchorBackfill)(candidate))
1605
+ anchorFields.anchorBackfill = { by: candidate.by, at: candidate.at };
1606
+ else
1607
+ dropped.push('anchorBackfill');
1608
+ }
1609
+ if (dropped.length > 0)
1610
+ anchorFields.anchorsDropped = dropped;
1343
1611
  const statement = {
1344
1612
  _type: asString(root._type),
1345
1613
  predicateType: asString(root.predicateType),
@@ -1349,6 +1617,7 @@ function parseVaultTraceBlock(content, artifactPath = 'the artifact') {
1349
1617
  stage: asString(predicate.stage),
1350
1618
  plan: asString(predicate.plan),
1351
1619
  generatedAt: asString(predicate.generatedAt),
1620
+ ...anchorFields,
1352
1621
  vault: {
1353
1622
  state: asString(vault.state),
1354
1623
  reason: asStringOrNull(vault.reason),
@@ -1498,13 +1767,25 @@ const EXECUTE_DIGEST_REMEDY = ' A governed file in this trace has no git commit,
1498
1767
  * validator now needs it too, and the handler already imports this module —
1499
1768
  * the reverse edge would be a cycle (the D-12 split's own lesson).
1500
1769
  *
1501
- * The remedy names `pp_run`, the launcher every workflow binds in its
1502
- * preamble and ships as `pp-core/bin/pp_run`, because a bare `pp-tools` can
1503
- * resolve to a different install than the project's own. INVARIANT 1 derives
1770
+ * UAT-4-F2: the remedy names `pp_run`, the launcher every workflow binds in
1771
+ * its preamble and ships as `pp-core/bin/pp_run`, because a bare `pp-tools`
1772
+ * resolved to a different install on a consultant machine. INVARIANT 1 derives
1504
1773
  * its floor from the command's first token, so the floor moved with the rename.
1505
1774
  * The literal pins live in the two vault-trace suites and vault-status-check.
1506
1775
  */
1507
1776
  const TRACE_REGENERATE_COMMAND = 'pp_run vault trace scaffold --stage <plan|execute> --plan <plan-id> <files_modified...>';
1777
+ /**
1778
+ * The trusted tail of every `anchor_invalid` detail: the repair, never a
1779
+ * deletion. 16 review WR-03: it names only a repair that cannot widen
1780
+ * coverage, restoring the value the CLI wrote. Regenerating the ledger is NOT
1781
+ * offered (16-06 offered it): it records today's HEAD as the end, so the
1782
+ * record would claim every commit since its real end, the restamp D-32
1783
+ * forbids and D-63 removed from the SEAL-01 footer for the same reason. A
1784
+ * back-fill is not offered either: the verb refuses a record that carries an
1785
+ * anchor, malformed or not (D-61), so naming it would be a remedy that cannot
1786
+ * clear the problem. A value that cannot be recovered is the operator's call.
1787
+ */
1788
+ const TRACE_ANCHOR_TAIL = ' Restore the value exactly as the CLI wrote it when the record was made; an anchor is written by the CLI, never typed. Regenerating the ledger is not a repair here: it would record today\'s HEAD as the end and claim commits this record never covered. If the value cannot be recovered, the operator decides how this record is re-anchored.';
1508
1789
  /**
1509
1790
  * Stable, machine-readable problem kinds. A consumer branches on these; the
1510
1791
  * `detail` string is for a human and is not a contract.
@@ -1665,11 +1946,11 @@ const TRACE_PROBLEM_KIND = Object.freeze({
1665
1946
  ID_UNRESOLVED: 'practice_id_unresolved',
1666
1947
  STATUS_OUT_OF_VOCABULARY: 'status_out_of_vocabulary',
1667
1948
  DEVIATED_REASON_MISSING: 'deviated_reason_missing',
1668
- // ── THE BODY-BLOCK FAMILY (D-05-07) ────────────────────
1949
+ // ── THE BODY-BLOCK FAMILY (261003-j89; closes D-05-07) ────────────────────
1669
1950
  //
1670
1951
  // Seven kinds that compare the `vault:` LEDGER against the `<vault_practices>`
1671
- // BODY BLOCK of the same plan artifact. Measured on a fresh project: seven
1672
- // perturbations that removed or
1952
+ // BODY BLOCK of the same plan artifact. Measured 2026-10-02 on a fresh 1.14.3
1953
+ // project (vault-gate-probe, test 1): seven perturbations that removed or
1673
1954
  // gutted the body block — the only practice text the executor reads — all
1674
1955
  // answered `block: false`, because nothing read the block back. A plan could
1675
1956
  // record full governance and deliver none. These kinds end that silence.
@@ -1704,6 +1985,16 @@ const TRACE_PROBLEM_KIND = Object.freeze({
1704
1985
  PRACTICES_BLOCK_ID_UNEXPECTED: 'practices_block_id_unexpected',
1705
1986
  PRACTICES_BLOCK_TEXT_MISMATCH: 'practices_block_text_mismatch',
1706
1987
  VALIDATOR_ERROR: 'validator_error',
1988
+ // 16-06 (D-35; T-16-23): a statement-shape kind, appended LAST. An anchor key
1989
+ // (`startCommit`, `endCommit`, `anchorBackfill`) that the record carries but
1990
+ // that fails its grammar — a short or uppercase sha, an option-shaped value,
1991
+ // a back-fill without a valid `by` or `at`. The parser dropped it, so it can
1992
+ // never reach a git argv; this kind makes the drop visible and blocking,
1993
+ // because an anchor is CLI-written and a hand-edited one is a record that
1994
+ // claims coverage it was never given. A record with NO anchor has no problem
1995
+ // here: write points are silent on a missing anchor (D-16), and the strict
1996
+ // points' `anchor_missing` is the handler's (16-07).
1997
+ ANCHOR_INVALID: 'anchor_invalid',
1707
1998
  });
1708
1999
  function isPlainObject(v) {
1709
2000
  return typeof v === 'object' && v !== null && !Array.isArray(v);
@@ -1855,16 +2146,8 @@ function validateTraceStatement(input, options) {
1855
2146
  if (statement['_type'] !== TRACE_STATEMENT_TYPE) {
1856
2147
  add(TRACE_PROBLEM_KIND.STATEMENT_TYPE_MISMATCH, `_type must be exactly "${TRACE_STATEMENT_TYPE}"; found "${describeValue(statement['_type'])}".`);
1857
2148
  }
1858
- const acceptedTypes = [
1859
- TRACE_PREDICATE_TYPE,
1860
- ...(Array.isArray(opts.acceptedPredicateTypes)
1861
- ? opts.acceptedPredicateTypes.filter((t) => typeof t === 'string' && t !== '')
1862
- : []),
1863
- ];
1864
- if (!acceptedTypes.includes(statement['predicateType'])) {
1865
- add(TRACE_PROBLEM_KIND.PREDICATE_TYPE_MISMATCH, `predicateType must be exactly "${TRACE_PREDICATE_TYPE}"` +
1866
- (acceptedTypes.length > 1 ? ` or one of the ${acceptedTypes.length - 1} the org profile accepts` : '') +
1867
- `; found "${describeValue(statement['predicateType'])}". The URI carries the MAJOR version — this reader is pinned to it and will not silently accept another.`);
2149
+ if (statement['predicateType'] !== TRACE_PREDICATE_TYPE) {
2150
+ add(TRACE_PROBLEM_KIND.PREDICATE_TYPE_MISMATCH, `predicateType must be exactly "${TRACE_PREDICATE_TYPE}"; found "${describeValue(statement['predicateType'])}". The URI carries the MAJOR version — this reader is pinned to it and will not silently accept another.`);
1868
2151
  }
1869
2152
  const schemaVersion = statement['schemaVersion'];
1870
2153
  if (typeof schemaVersion !== 'number' || !Number.isInteger(schemaVersion)) {
@@ -1882,6 +2165,28 @@ function validateTraceStatement(input, options) {
1882
2165
  add(TRACE_PROBLEM_KIND.STAGE_MISMATCH, `predicate.stage must be "${stage}" for this artifact; found "${describeValue(predicate['stage'])}".`);
1883
2166
  }
1884
2167
  }
2168
+ // ── the anchors (16-06, D-35) ──
2169
+ // Present-but-invalid only: an absent anchor is not this check's business.
2170
+ // The detail names the KEY, never the value (the seal leaf's message
2171
+ // discipline): the value failed its grammar, so it is not trusted text.
2172
+ for (const key of ['startCommit', 'endCommit']) {
2173
+ if (!(key in predicate))
2174
+ continue;
2175
+ const v = predicate[key];
2176
+ if (!(typeof v === 'string' && vault_seal_cjs_1.ANCHOR_SHA_RE.test(v))) {
2177
+ add(TRACE_PROBLEM_KIND.ANCHOR_INVALID, `predicate.${key} in ${file} is not a full lowercase commit id (40 or 64 hex), so it was not read as an anchor.`, TRACE_ANCHOR_TAIL);
2178
+ }
2179
+ }
2180
+ if ('anchorBackfill' in predicate && !(0, vault_seal_cjs_1.isAnchorBackfill)(predicate['anchorBackfill'])) {
2181
+ add(TRACE_PROBLEM_KIND.ANCHOR_INVALID, `predicate.anchorBackfill in ${file} is not { by, at } with a bounded name and an ISO-8601 instant, so the back-fill was not read.`, TRACE_ANCHOR_TAIL);
2182
+ }
2183
+ if (Array.isArray(predicate['anchorsDropped'])) {
2184
+ for (const key of predicate['anchorsDropped']) {
2185
+ if (key !== 'startCommit' && key !== 'endCommit' && key !== 'anchorBackfill')
2186
+ continue;
2187
+ add(TRACE_PROBLEM_KIND.ANCHOR_INVALID, `predicate.${key} in ${file} failed its grammar and was dropped when the ledger was read, so it was not read as an anchor.`, TRACE_ANCHOR_TAIL);
2188
+ }
2189
+ }
1885
2190
  // ── the pin, scoped to a LOADED state ──
1886
2191
  // A degraded statement legitimately carries a null pin (D-04), and a stale
1887
2192
  // cache is degraded WITH a live pin. A validator that checked the pin
@@ -2125,10 +2430,17 @@ function traceGeneratedAtOf(statement) {
2125
2430
  * all three classifier substrings below, deliberately, and lands on the
2126
2431
  * re-fetch-first lead (D-08-36).
2127
2432
  *
2433
+ * - a fresh client-tier cache at the configured ref that holds standards
2434
+ * (INDEX.md) and no practice files, read in practices mode (quick
2435
+ * 261015-at3). The eighth cause, and its own lead: the vault is the right
2436
+ * one, so neither re-fetching nor restoring access clears it — only setting
2437
+ * `vault.mode` to `standards` does.
2438
+ *
2128
2439
  * So ALL THREE tails name BOTH routes, and the second route — turning the practices
2129
2440
  * capability off for this project, a local `.planning/config.json` edit — is
2130
2441
  * performable with the vault down, from the same checkout the gate ran in.
2131
- * There is no third route: nothing overrides this kind.
2442
+ * There is no third route: `PP_VAULT_OVERRIDE_REASON` records a `vault.status`
2443
+ * override, not a trace-gate one, and does not suppress this kind.
2132
2444
  *
2133
2445
  * WHAT THE CLASSIFIER DOES, AND WHAT IT DELIBERATELY CANNOT DO. It reads the
2134
2446
  * recorded reason only to choose which route to state FIRST, so a reader on the
@@ -2141,7 +2453,9 @@ function traceGeneratedAtOf(statement) {
2141
2453
  * mechanism here and would not be if it gated the routes themselves.
2142
2454
  */
2143
2455
  function unprovablePairTail(reason) {
2144
- // THREE leads for SEVEN causes now (D-08-25, then D-08-34). The tier cause is tested FIRST
2456
+ // FOUR leads for EIGHT causes now (D-08-25, D-08-34, then 261015-at3). The
2457
+ // standards-only cause is tested first only because its phrase is unique to
2458
+ // it; among the original causes the tier cause is tested FIRST
2145
2459
  // because it is the only one neither existing lead describes: the vault was
2146
2460
  // reached, freshly, successfully — at the WRONG TIER. Regenerating alone
2147
2461
  // reproduces it (the scaffold reads the cache, it does not fetch), and so
@@ -2169,14 +2483,21 @@ function unprovablePairTail(reason) {
2169
2483
  // fact that a fetch must follow it. The fuller remedy (re-point
2170
2484
  // `client.vaultRepo` / `client.vaultRef`, re-fetch, regenerate) is spelled
2171
2485
  // out in `docs/vault-trace-format.md`, which has no cap.
2172
- const lead = /no client standards were applied/.test(reason)
2173
- ? ' Wrong vault: fix client.vaultRepo, re-fetch, then '
2174
- // The two reasons the resolver emits when the vault was REACHED FOR and not
2175
- // reached. Anything else — including an empty reason — falls to the
2176
- // re-fetch-first tail, which also names the disable route.
2177
- : /records a miss|is stale/.test(reason)
2178
- ? ' Regenerating alone reproduces this: restore vault access first, then '
2179
- : " Re-fetch the vault and regenerate that stage's trace: ";
2486
+ // The fourth lead (quick 261015-at3): the standards-only cause. The vault was
2487
+ // reached, freshly, at the configured ref and tier — it is the RIGHT vault,
2488
+ // read in a mode it cannot serve. Neither a re-fetch nor restored access
2489
+ // moves it; only the mode does. Kept inside the same 62-character budget as
2490
+ // the tier lead.
2491
+ const lead = /but vault\.mode is practices/.test(reason)
2492
+ ? ' Standards vault: set vault.mode to standards, then '
2493
+ : /no client standards were applied/.test(reason)
2494
+ ? ' Wrong vault: fix client.vaultRepo, re-fetch, then '
2495
+ // The two reasons the resolver emits when the vault was REACHED FOR and not
2496
+ // reached. Anything else — including an empty reason — falls to the
2497
+ // re-fetch-first tail, which also names the disable route.
2498
+ : /records a miss|is stale/.test(reason)
2499
+ ? ' Regenerating alone reproduces this: restore vault access first, then '
2500
+ : " Re-fetch the vault and regenerate that stage's trace: ";
2180
2501
  return (lead
2181
2502
  + TRACE_REGENERATE_COMMAND
2182
2503
  // The SECOND route, and it is the same sentence in all three tails on
@@ -2445,7 +2766,7 @@ function comparePracticeCoverage(planStatement, summaryStatement, options) {
2445
2766
  ];
2446
2767
  }
2447
2768
  }
2448
- // ─── The body block, read back against the ledger (D-05-07) ─
2769
+ // ─── The body block, read back against the ledger (261003-j89; closes D-05-07) ─
2449
2770
  //
2450
2771
  // A vault-enabled plan carries TWO surfaces: the `vault:` frontmatter LEDGER
2451
2772
  // (the audit record) and the `<vault_practices>` BODY BLOCK (the practice text
@@ -2517,7 +2838,7 @@ function practiceTextDigest(text) {
2517
2838
  * markdown code fence and an HTML comment are skipped too — a tag quoted as an
2518
2839
  * example or commented out is not a block the executor reads as one, and
2519
2840
  * counting it would let an example satisfy the presence check (plan-checker
2520
- * remark 2 on D-05-07). Tags are matched at column 0 only, which is where
2841
+ * remark 2 on 261003-j89). Tags are matched at column 0 only, which is where
2521
2842
  * the renderer puts them and where the planner reference says they go; an
2522
2843
  * entry is a fenced region whose FIRST `id:` line
2523
2844
  * precedes its `ask:`/`excerpt:` label — the degraded and refused sections are
@@ -2794,7 +3115,7 @@ module.exports = {
2794
3115
  // judgement about a trace in the leaf, which is the property that stops a
2795
3116
  // second, drifting implementation from growing in an adapter.
2796
3117
  comparePracticeCoverage,
2797
- // D-05-07: the body-block check, its digest and its remedy
3118
+ // 261003-j89 (closes D-05-07): the body-block check, its digest and its remedy
2798
3119
  // command. The router composes the problems exactly as it composes the
2799
3120
  // validator's; the two digest sites (builder and body check) share
2800
3121
  // `practiceTextDigest`, so the normalisation is written once.
@@ -2820,12 +3141,44 @@ module.exports = {
2820
3141
  // validator's missing-universe remedy and the handler's block message must
2821
3142
  // name the SAME command, and the handler already imports this module.
2822
3143
  TRACE_REGENERATE_COMMAND,
3144
+ // Quick 261015-at3: the eighth degraded cause's reason, harvested by tests.
3145
+ TRACE_REASON_STANDARDS_ONLY,
3146
+ // Quick 261015-at3: the degraded empty-ledger render line, harvested by tests.
3147
+ TRACE_DEGRADED_RENDER_LINE,
3148
+ VAULT_OVERRIDE_ENV,
2823
3149
  PRACTICES_APPLIED_HEADING,
2824
- // The reader allowlist: pp-core's own predicate type plus any the org
2825
- // profile lists (same major). The validator takes the list through its
2826
- // options, so a gate passes `acceptedPredicateTypes(root)`.
2827
- acceptedPredicateTypes,
2828
- TRACE_WORKTREE_CACHE_NOTE,
2829
- // The recorded check: census, read back for the mechanical audit.
2830
- readRecordedCensus,
3150
+ // Additive exports (Phase 04-01): both functions already existed above —
3151
+ // this widens the export list ONLY; no function body and no call site
3152
+ // changes. Reason: src/check-command-router.cts carries a duplicated
3153
+ // override evaluator and sanitiser which plan 04-03 deletes and rewires to
3154
+ // THIS pair, and two copies of an override validator would let a future
3155
+ // tightening land in one and not the other. `buildOverrideRecord` stays
3156
+ // deliberately UNexported — it is reached through `buildTraceStatement`,
3157
+ // which owns the `generatedAt` value the override's `when` field reuses,
3158
+ // and exporting it would invite the scaffold arm to bypass that seam.
3159
+ //
3160
+ // Widened again additively by Phase 7 (plan 07-01) with the pair below:
3161
+ // `/pp-init`'s reachability-proof deferral (D-07-03, OD-2) is an ATTRIBUTED
3162
+ // bypass of exactly the same shape as the trace override, so it reuses this
3163
+ // module's reason triad (`validateOverrideReason`, extracted out of
3164
+ // `evaluateTraceOverride` for that purpose) and its identity chain
3165
+ // (`resolveOverrideIdentity`) rather than growing a second copy in
3166
+ // `src/init-contract.cts`. `buildOverrideRecord` STILL stays unexported for
3167
+ // the reason stated just above — Phase 7 builds its own record shape from
3168
+ // the four `init.probe_*` config keys and never borrows the trace record's.
3169
+ evaluateTraceOverride,
3170
+ sanitizeOverrideReason,
3171
+ validateOverrideReason,
3172
+ resolveOverrideIdentity,
3173
+ // 15-07 (D-08): the standards block verb's override line. A wrapper that
3174
+ // stamps its own `when`, so `buildOverrideRecord` itself stays unexported.
3175
+ buildStandardsOverrideRecord,
3176
+ // 16-06 (D-32): what `vault anchor backfill` splices into a practices
3177
+ // ledger, and who it records as having done it. One spelling of each.
3178
+ traceAnchorLines,
3179
+ resolveBackfillIdentity,
3180
+ // 16-06 (D-12): the scaffold's own pin reader, so `vault door-record` in
3181
+ // practices mode records the pin the practices record would, never a third
3182
+ // reading of the manifest.
3183
+ resolvePin,
2831
3184
  };