@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.
- package/.claude-plugin/plugin.json +3 -3
- package/README.md +26 -9
- package/agents/pp-code-reviewer.md +2 -1
- package/agents/pp-debug-session-manager.md +9 -6
- package/agents/pp-executor.md +10 -13
- package/agents/pp-plan-checker.md +4 -4
- package/agents/pp-planner.md +23 -9
- package/agents/pp-vault-reviewer.md +244 -21
- package/agents/pp-verifier.md +7 -3
- package/bin/install.js +10 -3
- package/commands/pp-execute-phase.md +1 -3
- package/commands/pp-ns-review.md +1 -1
- package/commands/pp-plan-phase.md +1 -2
- package/commands/pp-vault-review.md +5 -3
- package/commands/pp-verify-work.md +1 -2
- package/gemini-extension.json +1 -1
- package/hooks/dist/pp-check-update.js +0 -0
- package/hooks/dist/pp-cursor-session-start.js +1 -1
- package/hooks/dist/pp-statusline.js +1 -1
- package/hooks/dist/pp-update-banner.js +0 -0
- package/hooks/pp-check-update.js +0 -0
- package/hooks/pp-cursor-session-start.js +1 -1
- package/hooks/pp-statusline.js +1 -1
- package/hooks/pp-update-banner.js +0 -0
- package/package.json +14 -19
- package/pareto-skills/_shared/create-project-board.sh +0 -0
- package/pareto-skills/pp-project-setup/SKILL.md +1 -1
- package/pp-core/bin/check-latest-version.cjs +0 -0
- package/pp-core/bin/detect-build-commands.sh +2 -2
- package/pp-core/bin/lib/capability-activation.cjs +83 -9
- package/pp-core/bin/lib/capability-registry.cjs +396 -82
- package/pp-core/bin/lib/capability-state.cjs +160 -4
- package/pp-core/bin/lib/capability-writer.cjs +19 -5
- package/pp-core/bin/lib/check-command-router.cjs +3334 -946
- package/pp-core/bin/lib/clusters.cjs +3 -0
- package/pp-core/bin/lib/command-aliases.cjs +0 -35
- package/pp-core/bin/lib/commands.cjs +9 -4
- package/pp-core/bin/lib/compose-bindings.cjs +251 -88
- package/pp-core/bin/lib/config-loader.cjs +7 -103
- package/pp-core/bin/lib/config-schema.cjs +1 -1
- package/pp-core/bin/lib/config.cjs +154 -62
- package/pp-core/bin/lib/decisions.cjs +4 -4
- package/pp-core/bin/lib/discovery-vault-check.cjs +246 -0
- package/pp-core/bin/lib/frontmatter.cjs +1 -38
- package/pp-core/bin/lib/git-base-branch.cjs +13 -11
- package/pp-core/bin/lib/git-provider.cjs +4 -4
- package/pp-core/bin/lib/init-command-router.cjs +5 -8
- package/pp-core/bin/lib/init-contract.cjs +648 -738
- package/pp-core/bin/lib/init-interactive.cjs +76 -4
- package/pp-core/bin/lib/init.cjs +24 -51
- package/pp-core/bin/lib/install-profiles.cjs +1 -1
- package/pp-core/bin/lib/io.cjs +8 -0
- package/pp-core/bin/lib/loop-resolver.cjs +189 -129
- package/pp-core/bin/lib/org-profile.cjs +469 -152
- package/pp-core/bin/lib/package-identity.cjs +3 -3
- package/pp-core/bin/lib/phase-command-router.cjs +1 -29
- package/pp-core/bin/lib/phase-scope.cjs +1 -1
- package/pp-core/bin/lib/phase.cjs +11 -253
- package/pp-core/bin/lib/platform-bindings.cjs +40 -19
- package/pp-core/bin/lib/practices-review-check.cjs +15 -9
- package/pp-core/bin/lib/profile-output.cjs +1 -1
- package/pp-core/bin/lib/shell-command-projection.cjs +104 -1
- package/pp-core/bin/lib/state.cjs +11 -85
- package/pp-core/bin/lib/template.cjs +1 -1
- package/pp-core/bin/lib/vault-audit.cjs +650 -126
- package/pp-core/bin/lib/vault-command-router.cjs +2604 -367
- package/pp-core/bin/lib/vault-coordinates.cjs +13 -4
- package/pp-core/bin/lib/vault-index.cjs +699 -0
- package/pp-core/bin/lib/vault-mode.cjs +235 -0
- package/pp-core/bin/lib/vault-practices.cjs +139 -72
- package/pp-core/bin/lib/vault-review.cjs +194 -451
- package/pp-core/bin/lib/vault-seal.cjs +1936 -0
- package/pp-core/bin/lib/vault-standards.cjs +1505 -0
- package/pp-core/bin/lib/vault-trace.cjs +607 -254
- package/pp-core/bin/lib/vault-update.cjs +9 -33
- package/pp-core/bin/lib/vault-write-back.cjs +70 -42
- package/pp-core/bin/lib/vault.cjs +2734 -920
- package/pp-core/bin/lib/verify.cjs +0 -11
- package/pp-core/bin/portable-timeout.sh +1 -1
- package/pp-core/bin/pp-tools.cjs +184 -54
- package/pp-core/bin/shared/config-defaults.manifest.json +0 -7
- package/pp-core/bin/shared/config-schema.manifest.json +0 -3
- package/pp-core/bin/shared/model-catalog.json +2 -2
- package/pp-core/bin/verify-reapply-patches.cjs +0 -0
- package/pp-core/org-profile.json +26 -0
- package/pp-core/references/autonomous-best-effort.md +8 -7
- package/pp-core/references/loop-hook-dispatch.md +0 -25
- package/pp-core/references/planner-gap-closure.md +0 -1
- package/pp-core/references/planner-tdd.md +0 -1
- package/pp-core/references/planner-vault-practices.md +17 -34
- package/pp-core/references/planner-vault-standards.md +320 -0
- package/pp-core/references/planning-config.md +0 -8
- package/pp-core/references/practice-arithmetic-contract.md +121 -0
- package/pp-core/references/tdd.md +0 -1
- package/pp-core/references/verifier-practice-verdicts.md +263 -0
- package/pp-core/templates/README.md +1 -0
- package/pp-core/templates/VAULT-REVIEW.md +202 -0
- package/pp-core/templates/phase-prompt.md +2 -6
- package/pp-core/templates/practices/README.md +20 -23
- package/pp-core/workflows/_runtime-launcher.snippet.sh +0 -0
- package/pp-core/workflows/add-tests.md +55 -4
- package/pp-core/workflows/audit-fix.md +63 -2
- package/pp-core/workflows/audit-milestone.md +1 -1
- package/pp-core/workflows/autonomous.md +85 -11
- package/pp-core/workflows/check-todos.md +13 -2
- package/pp-core/workflows/code-review-fix.md +59 -3
- package/pp-core/workflows/code-review.md +10 -1
- package/pp-core/workflows/complete-milestone.md +1 -1
- package/pp-core/workflows/debug.md +54 -4
- package/pp-core/workflows/discovery-sprint/prefill.md +19 -7
- package/pp-core/workflows/discovery-sprint/prototype.md +46 -49
- package/pp-core/workflows/discovery-sprint/session-13-gates.md +2 -0
- package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +20 -24
- package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/06-security.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +1 -6
- package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/12-delivery-model.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +1 -3
- package/pp-core/workflows/discovery-sprint/templates/context-template.md +1 -1
- package/pp-core/workflows/discovery-sprint/vault-context.md +213 -0
- package/pp-core/workflows/discovery-sprint.md +7 -3
- package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +4 -4
- package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +43 -1
- package/pp-core/workflows/discovery-to-build.md +10 -2
- package/pp-core/workflows/discuss-phase.md +3 -10
- package/pp-core/workflows/execute-phase/steps/check-query-gate.md +329 -0
- package/pp-core/workflows/execute-phase/steps/client-context-gate.md +3 -2
- package/pp-core/workflows/execute-phase/steps/cross-ai-delegation.md +1 -1
- package/pp-core/workflows/execute-phase/steps/execute-pre-dispatch.md +98 -0
- package/pp-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +94 -0
- package/pp-core/workflows/execute-phase/steps/post-merge-gate.md +6 -8
- package/pp-core/workflows/execute-phase/steps/vault-fetch.md +115 -14
- package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +322 -20
- package/pp-core/workflows/execute-phase/steps/verifier-practice-payload.md +165 -0
- package/pp-core/workflows/execute-phase/steps/verify-post-gate.md +163 -0
- package/pp-core/workflows/execute-phase.md +155 -128
- package/pp-core/workflows/execute-plan.md +56 -6
- package/pp-core/workflows/fast.md +53 -4
- package/pp-core/workflows/help/modes/full.md +3 -3
- package/pp-core/workflows/init/steps/client-context-capture.md +101 -0
- package/pp-core/workflows/init/steps/gitlab-provisioning.md +130 -0
- package/pp-core/workflows/init/steps/project-md-deferral-narration.md +66 -0
- package/pp-core/workflows/init/steps/reachability-outcome.md +70 -0
- package/pp-core/workflows/init.md +232 -272
- package/pp-core/workflows/new-project.md +31 -23
- package/pp-core/workflows/plan-phase.md +98 -45
- package/pp-core/workflows/quick.md +117 -3
- package/pp-core/workflows/secure-phase.md +1 -1
- package/pp-core/workflows/ship.md +57 -28
- package/pp-core/workflows/undo.md +1 -1
- package/pp-core/workflows/update.md +3 -3
- package/pp-core/workflows/validate-phase.md +58 -3
- package/pp-core/workflows/vault-review.md +282 -22
- package/pp-core/workflows/verify-work.md +12 -14
- package/scripts/changeset/cli.cjs +6 -5
- package/scripts/changeset/lint.cjs +0 -0
- package/scripts/changeset/new.cjs +33 -50
- package/scripts/dev-env.cjs +25 -85
- package/scripts/gen-capability-registry.cjs +199 -11
- package/scripts/gen-practice-arithmetic-contract.cjs +412 -0
- package/scripts/lib/allowlist-ratchet.cjs +1 -103
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -2
- package/scripts/lint-docs-required.cjs +0 -0
- package/scripts/lint-pr-check-project-dir.cjs +1 -3
- package/scripts/lint-test-file-count.allowlist.json +12 -10
- package/scripts/practice-arithmetic-contract.allow.json +35 -0
- package/scripts/prompt-injection-scan.sh +22 -17
- package/scripts/record-vault-mode-golden.cjs +89 -0
- package/scripts/release-channel.cjs +0 -0
- package/scripts/sync-manifest-versions.cjs +5 -4
- package/scripts/sync-rulesets.sh +1 -1
- package/scripts/update-size-baseline.cjs +30 -274
- package/scripts/vault-calibrate.cjs +996 -0
- package/scripts/verify-opengsd-sweep.cjs +3 -5
- package/scripts/workflow-size.cjs +9 -61
- package/pp-core/bin/lib/dispatch-plan.cjs +0 -483
- package/pp-core/bin/lib/install-run.cjs +0 -536
- package/pp-core/bin/lib/loop-gates.cjs +0 -213
- package/pp-core/bin/lib/phase-manifest.cjs +0 -527
- package/pp-core/bin/lib/phase-tier.cjs +0 -480
- package/pp-core/bin/lib/plan-graph.cjs +0 -368
- package/pp-core/references/executor-vault-trace.md +0 -47
- package/pp-core/references/planner-plan-graph.md +0 -66
- package/pp-core/workflows/discovery-sprint/vault-fetch.md +0 -37
- package/pp-core/workflows/execute-phase/steps/checkpoint-handling.md +0 -45
- package/pp-core/workflows/execute-phase/steps/install-run.md +0 -29
- package/pp-core/workflows/execute-phase/steps/phase-close-rebaseline.md +0 -23
- package/pp-core/workflows/execute-phase/steps/serial-group-dispatch.md +0 -73
- package/pp-core/workflows/execute-phase/steps/vault-door-gate.md +0 -19
- package/pp-core/workflows/plan-phase/steps/phase-tier.md +0 -43
- package/scripts/lib/size-budget.cjs +0 -311
- package/scripts/lint-neutrality-names.cjs +0 -694
- package/scripts/lint-org-coordinates.allowlist.json +0 -35
- 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
|
|
9
|
-
*
|
|
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` (`
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* trace disagree with the
|
|
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.
|
|
114
|
-
*
|
|
115
|
-
* is an identifier, not an endpoint — nothing fetches
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
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://
|
|
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
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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.
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
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
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
212
|
-
|
|
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: '
|
|
215
|
-
reason:
|
|
384
|
+
state: 'degraded',
|
|
385
|
+
reason: 'vault manifest records a miss; no vault content was reached',
|
|
216
386
|
...pin,
|
|
217
|
-
manifestFiles:
|
|
218
|
-
? manifest.files.filter((f) => typeof f === 'string')
|
|
219
|
-
: [],
|
|
387
|
+
manifestFiles: null,
|
|
220
388
|
};
|
|
221
389
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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 {
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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:
|
|
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 (
|
|
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 (
|
|
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
|
-
*
|
|
1502
|
-
* preamble and ships as `pp-core/bin/pp_run`, because a bare `pp-tools`
|
|
1503
|
-
*
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
//
|
|
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
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
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
|
|
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
|
-
//
|
|
2825
|
-
//
|
|
2826
|
-
//
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
//
|
|
2830
|
-
|
|
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
|
};
|