@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
|
@@ -4,8 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
/**
|
|
6
6
|
* src/vault-audit.cts — the pure `vault audit` evaluator: the ONE engine both
|
|
7
|
-
* surfaces call (
|
|
8
|
-
*
|
|
7
|
+
* surfaces in plan 05-06 call (Vault Assurance #71924, phase 05 plan 05;
|
|
8
|
+
* CHECK-03, CHECK-04).
|
|
9
9
|
*
|
|
10
10
|
* Responsibility. Given the practices the cached vault holds and the files a
|
|
11
11
|
* phase actually changed, decide — per practice — one of exactly four verdicts
|
|
@@ -20,32 +20,31 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
20
20
|
* `MATCH_BUDGET_MS` plus `MATCHED_FILES_CAP`.
|
|
21
21
|
*
|
|
22
22
|
* Nothing on the matching path is re-implemented here and no grammar is
|
|
23
|
-
* restated in prose
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* `eslint.config.mjs`.
|
|
31
|
-
* - `./phase-manifest.cjs` (`readPhaseManifestEntry`) — the phase's recorded
|
|
32
|
-
* start commit, read from the file `phase manifest start|record` writes.
|
|
33
|
-
* - `./vault-trace.cjs` — the ledger API: SUMMARY `gitCommit` digests for the
|
|
34
|
-
* trace-digest rung, and the `check:` census the ledgers recorded.
|
|
23
|
+
* restated in prose. ADR-71924 §2 refuses to restate the glob grammar
|
|
24
|
+
* precisely so the two cannot drift, and a second matcher would be the
|
|
25
|
+
* drifting copy it refuses. §3's prohibition — no client-authored byte is
|
|
26
|
+
* ever compiled into a regular expression — is why content tokens below are
|
|
27
|
+
* tested by `String.prototype.includes` and nothing else. That prohibition
|
|
28
|
+
* is mechanised, not merely stated: `eslint-rules/practices-boundary-inputs.cjs`
|
|
29
|
+
* is wired at this file in `eslint.config.mjs`.
|
|
35
30
|
* - `./git-base-branch.cjs` (`resolveBaseBranchResult`) — the 5-tier ladder is
|
|
36
31
|
* never re-derived; issue #1146 records what the naive form cost. The RESULT
|
|
37
32
|
* form, so a REFUSED `git.base_branch` becomes a stated note rather than a
|
|
38
33
|
* silent change of range (W16).
|
|
39
|
-
* - `./shell-command-projection.cjs` (`
|
|
34
|
+
* - `./shell-command-projection.cjs` (`execGitLocal`, D-116) — the mandated, timeout-bounded
|
|
40
35
|
* git seam. `execFileSync` is not used anywhere in this module.
|
|
41
36
|
*
|
|
42
37
|
* WHAT `require` MEANS HERE, stated because a recorded number depends on it.
|
|
43
38
|
* A `require` token is satisfied when EVERY matched file contains it. This is
|
|
44
|
-
* the per-file reading
|
|
39
|
+
* the per-file reading, and it is the reading the CHECK-05 census in
|
|
40
|
+
* `docs/adr/71924-declarative-check-blocks.md` states as its premise ("`require`
|
|
41
|
+
* is read per matched file") and measured its 19-of-55 count under. It is
|
|
42
|
+
* chosen because it is the only reading under which a FAIL can name a SPECIFIC
|
|
45
43
|
* offending file — and a verdict that cannot name what offended it is a verdict
|
|
46
44
|
* an auditor cannot act on, which is the failure D-05-03's vocabulary exists to
|
|
47
|
-
* prevent. Changing this to "some matched file contains the token"
|
|
48
|
-
* decision with a paper trail, not an
|
|
45
|
+
* prevent. Changing this to "some matched file contains the token" would move
|
|
46
|
+
* the recorded census; it is a decision with a paper trail, not an
|
|
47
|
+
* implementation detail.
|
|
49
48
|
*
|
|
50
49
|
* TWO KNOWN LIMITS OF THE DIFF, recorded here rather than rediscovered later.
|
|
51
50
|
* Both are inherited deliberately from `cmdShipPractices`' scoping mechanism,
|
|
@@ -66,22 +65,17 @@ const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs")
|
|
|
66
65
|
const git_base_branch_cjs_1 = require("./git-base-branch.cjs");
|
|
67
66
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
68
67
|
const practicesMod = require("./vault-practices.cjs");
|
|
69
|
-
//
|
|
70
|
-
// `resolvePhaseRange
|
|
71
|
-
//
|
|
72
|
-
// module, so this adds no cycle.
|
|
68
|
+
// 260915-ik2 (UAT-6-F1): the trace leaf, for the SUMMARY trace-digest rung of
|
|
69
|
+
// `resolvePhaseRange`. `vault-trace.cts` imports `vault.cjs` and the practices
|
|
70
|
+
// leaf only, and neither imports this module, so this adds no cycle.
|
|
73
71
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
74
72
|
const traceMod = require("./vault-trace.cjs");
|
|
75
|
-
//
|
|
73
|
+
// 16-03: the one `.planning/` path test. Direction: this module imports the
|
|
74
|
+
// seal leaf statically; the leaf requires this module only LAZILY, inside
|
|
75
|
+
// `collectRecordedArtifacts`, so there is no load-time cycle (D-41).
|
|
76
76
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
// reaches the gate message.
|
|
80
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
81
|
-
const vaultMod = require("./vault.cjs");
|
|
82
|
-
const { parseVaultTraceBlock, readRecordedCensus } = traceMod;
|
|
83
|
-
const { readPhaseManifestEntry, PHASE_MANIFEST_READ_MISS, COMMIT_SHA_SHAPE } = phaseManifestMod;
|
|
84
|
-
const { redactRemoteText } = vaultMod;
|
|
77
|
+
const vaultSeal = require("./vault-seal.cjs");
|
|
78
|
+
const { parseVaultTraceBlock } = traceMod;
|
|
85
79
|
const { globMatch, isSupportedGlob, boundedToken, discoverPracticeFiles, MATCH_PAIR_CAP, MATCH_BUDGET_MS, MATCHED_FILES_CAP,
|
|
86
80
|
// 05-REVIEW CR-02/CR-03: the shared fencing idiom and the errno redactor —
|
|
87
81
|
// reused, never re-authored, so the two modules cannot disagree about how a
|
|
@@ -123,7 +117,12 @@ const AUDIT_VERDICT = Object.freeze({
|
|
|
123
117
|
* verdict would let a one-character typo buy a non-blocking outcome, which is
|
|
124
118
|
* the same trade `CHECK_INVALID` already refuses.
|
|
125
119
|
*
|
|
126
|
-
*
|
|
120
|
+
* Quick 261015-at3 added `VAULT_STANDARDS_ONLY`: a standards vault (INDEX.md,
|
|
121
|
+
* no practice files) read in practices mode. A FAIL and not a NO_CHECK on the
|
|
122
|
+
* `PRACTICE_UNLOADABLE` argument — a practice layer the project is not reading
|
|
123
|
+
* has unknown check-bearing-ness, so it cannot buy a non-blocking outcome.
|
|
124
|
+
*
|
|
125
|
+
* NINE members, and all nine block.
|
|
127
126
|
*/
|
|
128
127
|
const AUDIT_FAIL_REASON = Object.freeze({
|
|
129
128
|
UNSATISFIED: 'unsatisfied',
|
|
@@ -134,11 +133,12 @@ const AUDIT_FAIL_REASON = Object.freeze({
|
|
|
134
133
|
DIFF_UNREADABLE: 'diff_unreadable',
|
|
135
134
|
VAULT_UNREADABLE: 'vault_unreadable',
|
|
136
135
|
PRACTICE_UNLOADABLE: 'practice_unloadable',
|
|
136
|
+
VAULT_STANDARDS_ONLY: 'vault_standards_only',
|
|
137
137
|
});
|
|
138
138
|
/**
|
|
139
139
|
* Where the compared range came from — recorded beside the range itself.
|
|
140
140
|
*
|
|
141
|
-
* In ladder order: the phase's recorded start commit,
|
|
141
|
+
* In ladder order (260915-ik2, UAT-6-F1): the phase's recorded start commit,
|
|
142
142
|
* the parent of its first recorded commit, the parent of the ancestry-earliest
|
|
143
143
|
* SUMMARY trace digest, and only then the base-branch fallback. Every source
|
|
144
144
|
* but the last is resolved FROM THE PHASE, which is what WR-04's block predicate
|
|
@@ -162,9 +162,11 @@ const AUDIT_RANGE_SOURCE = Object.freeze({
|
|
|
162
162
|
* gate block vacuously had no way to ask it about a range they knew was the
|
|
163
163
|
* phase's. This is the diagnostic channel that answer needs.
|
|
164
164
|
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
165
|
+
* DELIBERATELY UNMAPPED in the review tier's
|
|
166
|
+
* `REVIEW_SOURCE_FOR_AUDIT_SOURCE` (`src/vault-review.cts:450-454`), for the
|
|
167
|
+
* same reason `base-branch` is: the review never accepts a range flag, so an
|
|
168
|
+
* operator range can never reach it and a mapping would assert a path that
|
|
169
|
+
* does not exist. Kebab-cased like its four siblings — it IS a source, unlike
|
|
168
170
|
* `AUDIT_RANGE_OUTCOME`'s members.
|
|
169
171
|
*/
|
|
170
172
|
OPERATOR_RANGE: 'operator-range',
|
|
@@ -218,7 +220,7 @@ function isValidOperatorRange(value) {
|
|
|
218
220
|
* `phaseCommitsFor` returned `[]` for five structurally different grounds and
|
|
219
221
|
* `resolvePhaseRange` distinguished none of them, so `rangeSource:
|
|
220
222
|
* 'base-branch'` was the whole story an operator got. The executing checkout
|
|
221
|
-
* writes the manifest itself, but a checkout that never ran
|
|
223
|
+
* writes the manifest itself (260915-ik2), but a checkout that never ran
|
|
222
224
|
* `/pp-execute-phase` — a fresh clone or a CI runner where `.planning/` is
|
|
223
225
|
* gitignored — has no manifest to read, and there the fallback fires whenever
|
|
224
226
|
* no SUMMARY trace digest verifies either. That is precisely the case where
|
|
@@ -228,15 +230,15 @@ function isValidOperatorRange(value) {
|
|
|
228
230
|
*/
|
|
229
231
|
const AUDIT_FALLBACK_REASON = Object.freeze({
|
|
230
232
|
/** The phase directory's name does not begin with an `NN` or `NN.M` token. */
|
|
231
|
-
PHASE_SLUG_UNRECOGNISED:
|
|
232
|
-
/** No `.phase-manifest.json`
|
|
233
|
-
MANIFEST_NOT_FOUND:
|
|
233
|
+
PHASE_SLUG_UNRECOGNISED: 'phase-slug-unrecognised',
|
|
234
|
+
/** No `.phase-manifest.json` exists anywhere on the walk up from the phase dir. */
|
|
235
|
+
MANIFEST_NOT_FOUND: 'manifest-not-found',
|
|
234
236
|
/** A manifest was found but is not parseable JSON of the expected shape. */
|
|
235
|
-
MANIFEST_MALFORMED:
|
|
236
|
-
/** A manifest parsed, but holds no
|
|
237
|
-
PHASE_ENTRY_MISSING:
|
|
238
|
-
/** The phase's entry exists and records
|
|
239
|
-
NO_COMMITS_RECORDED:
|
|
237
|
+
MANIFEST_MALFORMED: 'manifest-malformed',
|
|
238
|
+
/** A manifest parsed, but holds no entry for this phase under either key spelling. */
|
|
239
|
+
PHASE_ENTRY_MISSING: 'phase-entry-missing',
|
|
240
|
+
/** The phase's entry exists and records no commits. */
|
|
241
|
+
NO_COMMITS_RECORDED: 'no-commits-recorded',
|
|
240
242
|
/** Commits were recorded, and git verified none of them. */
|
|
241
243
|
COMMIT_UNVERIFIABLE: 'commit-unverifiable',
|
|
242
244
|
});
|
|
@@ -256,7 +258,7 @@ const AUDIT_FALLBACK_REASON = Object.freeze({
|
|
|
256
258
|
*
|
|
257
259
|
* `range_unresolvable` is deliberately NOT a fifth `AUDIT_RANGE_SOURCE` member
|
|
258
260
|
* (RESEARCH R7, Open Question 2): nothing was resolved, so there is no source
|
|
259
|
-
* to name, and
|
|
261
|
+
* to name, and `REVIEW_SOURCE_FOR_AUDIT_SOURCE` would then owe it a mapping it
|
|
260
262
|
* has no meaning for.
|
|
261
263
|
*
|
|
262
264
|
* CASING. These three values are underscore-cased while the kebab-cased
|
|
@@ -331,6 +333,37 @@ function classifyRangeOutcome(input) {
|
|
|
331
333
|
function isPhaseScopedSource(rangeSource) {
|
|
332
334
|
return rangeSource !== AUDIT_RANGE_SOURCE.BASE_BRANCH;
|
|
333
335
|
}
|
|
336
|
+
/**
|
|
337
|
+
* Why `recordPhaseStart` / `recordPhaseCommits` wrote nothing (260915-ik2).
|
|
338
|
+
*
|
|
339
|
+
* Two members REUSE the reader's strings, deliberately: a phase slug the
|
|
340
|
+
* writer cannot key and a manifest it refuses to overwrite are the same facts
|
|
341
|
+
* the reader reports on the same file, and two spellings of one fact is the
|
|
342
|
+
* drift this module's frozen vocabularies exist to prevent.
|
|
343
|
+
*/
|
|
344
|
+
const PHASE_MANIFEST_WRITE_REASON = Object.freeze({
|
|
345
|
+
/** The phase directory is not an existing directory inside the project root. */
|
|
346
|
+
PHASE_DIR_UNRESOLVABLE: 'phase-dir-unresolvable',
|
|
347
|
+
/** The phase directory's name does not begin with an `NN` or `NN.M` token. */
|
|
348
|
+
PHASE_SLUG_UNRECOGNISED: AUDIT_FALLBACK_REASON.PHASE_SLUG_UNRECOGNISED,
|
|
349
|
+
/** git could not name the commit HEAD points at (an unborn branch, or no repository). */
|
|
350
|
+
HEAD_UNRESOLVABLE: 'head-unresolvable',
|
|
351
|
+
/** The manifest exists and is not a JSON object with an object `phases`; it is left untouched. */
|
|
352
|
+
MANIFEST_MALFORMED: AUDIT_FALLBACK_REASON.MANIFEST_MALFORMED,
|
|
353
|
+
/** `record` found no start commit for this phase — `start` never ran for it. */
|
|
354
|
+
START_NOT_RECORDED: 'start-not-recorded',
|
|
355
|
+
/** `record` found a start commit that git does not verify. */
|
|
356
|
+
START_UNVERIFIABLE: 'start-unverifiable',
|
|
357
|
+
/**
|
|
358
|
+
* `start` found no anchor for this phase, and its directory already holds a
|
|
359
|
+
* `*-SUMMARY.md`: the phase has been executed, so HEAD sits AFTER its own
|
|
360
|
+
* commits and an anchor there would scope the audit past them (R2). Nothing
|
|
361
|
+
* is written; the SUMMARY trace-digest rung is the range source instead.
|
|
362
|
+
*/
|
|
363
|
+
PHASE_ALREADY_EXECUTED: 'phase-already-executed',
|
|
364
|
+
/** The atomic write (temp file + rename) failed; the temp file was removed. */
|
|
365
|
+
WRITE_FAILED: 'write-failed',
|
|
366
|
+
});
|
|
334
367
|
/**
|
|
335
368
|
* The bounded WHOLE-file read for a content check (1 MiB).
|
|
336
369
|
*
|
|
@@ -389,6 +422,13 @@ const AUDIT_OPERATOR_RANGE_REPAIR_SENTENCE = 'Repair: re-run the audit with a we
|
|
|
389
422
|
* and nothing else (T-4-75).
|
|
390
423
|
*/
|
|
391
424
|
const AUDIT_VAULT_REPAIR_SENTENCE = 'Repair: re-fetch the client vault with `pp-tools vault fetch`, confirm `.planning/vault` is readable, then run this audit again.';
|
|
425
|
+
/**
|
|
426
|
+
* The standards-only arm's repair (quick 261015-at3). A sibling of the vault
|
|
427
|
+
* arm's sentence and deliberately not that sentence: re-fetching reproduces
|
|
428
|
+
* the same standards vault, so the only repair is the mode. A repair, never a
|
|
429
|
+
* removal (T-4-75).
|
|
430
|
+
*/
|
|
431
|
+
const AUDIT_STANDARDS_ONLY_REPAIR_SENTENCE = 'Repair: set "vault": {"mode": "standards"} in .planning/config.json (`pp-tools config-set vault.mode standards`, pp-core 1.16.0 or later), re-plan the phase, then run this audit again.';
|
|
392
432
|
// ─── Shape predicates ─────────────────────────────────────────────────────────
|
|
393
433
|
//
|
|
394
434
|
// SHAPE-PREDICATES-BEGIN
|
|
@@ -397,7 +437,7 @@ const AUDIT_VAULT_REPAIR_SENTENCE = 'Repair: re-fetch the client vault with `pp-
|
|
|
397
437
|
// module's own frozen verdict constants and nothing else. No regular
|
|
398
438
|
// expression, no text-search primitive, no character-code probe, no inline
|
|
399
439
|
// literal may appear here or reach a boundary decision anywhere in this file —
|
|
400
|
-
//
|
|
440
|
+
// ADR-71924 §3 states the prohibition and `eslint-rules/practices-boundary-inputs.cjs`,
|
|
401
441
|
// wired at this file in `eslint.config.mjs`, is what keeps it: a future edit
|
|
402
442
|
// reintroducing one fails `npm run lint`, not a review.
|
|
403
443
|
//
|
|
@@ -520,7 +560,7 @@ const AUDIT_COVERAGE_OUTCOME = Object.freeze({
|
|
|
520
560
|
});
|
|
521
561
|
/**
|
|
522
562
|
* The counts-only coverage line (D-10-03, D-10-18), modelled on
|
|
523
|
-
*
|
|
563
|
+
* `renderRateLine` (`src/vault-review.cts:845`) and inheriting its two rules
|
|
524
564
|
* verbatim:
|
|
525
565
|
*
|
|
526
566
|
* - the figures are DERIVED from the four verdict counts and a supplied `n`
|
|
@@ -544,25 +584,6 @@ function renderMechanicalCoverageLine(counts) {
|
|
|
544
584
|
return (`${checked} of ${applicable} practices mechanically checked · ` +
|
|
545
585
|
`${pass} PASS · ${fail} FAIL · ${notMatched} NOT_MATCHED · ${noCheck} NO_CHECK`);
|
|
546
586
|
}
|
|
547
|
-
/**
|
|
548
|
-
* The one sentence every surface states about the recorded `check:` census.
|
|
549
|
-
*
|
|
550
|
-
* The census is the vault's, taken when the ledger was written; this audit's
|
|
551
|
-
* counts are the run's. The sentence keeps the two apart: it says what the
|
|
552
|
-
* LEDGER records, and never that the audit found it. Engine-composed apart
|
|
553
|
-
* from the artifact name, which is a file name in the phase directory and is
|
|
554
|
-
* bounded.
|
|
555
|
-
*/
|
|
556
|
-
function renderRecordedCensusLine(census) {
|
|
557
|
-
if (census === null)
|
|
558
|
-
return "No check: census is recorded in this phase's trace ledgers.";
|
|
559
|
-
const sha = census.resolvedSha ? census.resolvedSha.slice(0, 12) : 'an unrecorded sha';
|
|
560
|
-
const moved = census.distinct > 1
|
|
561
|
-
? ` The phase's ledgers record ${census.distinct} different censuses, so the vault changed during the phase.`
|
|
562
|
-
: '';
|
|
563
|
-
return (`The phase's trace ledger (${boundedToken(census.artifact)}) records ${census.checkBearingPractices} of ` +
|
|
564
|
-
`${census.practiceUniverse} practices carrying a check: block, at vault ${sha}.${moved}`);
|
|
565
|
-
}
|
|
566
587
|
/**
|
|
567
588
|
* Does this verdict set halt the gate? A blocking decision made on client data,
|
|
568
589
|
* so it is audited: only a FAIL blocks, and it is emitted as a strict boolean
|
|
@@ -589,7 +610,7 @@ function blocksOnVerdicts(entries) {
|
|
|
589
610
|
* gate rejects matches nothing here rather than being interpreted by some
|
|
590
611
|
* second, more permissive reading of the same string. `globMatch` does the
|
|
591
612
|
* matching; this module never writes one. That holds for the applies-to side
|
|
592
|
-
* exactly as it already held for the check side (
|
|
613
|
+
* exactly as it already held for the check side (ADR-71924 §2 and §3).
|
|
593
614
|
*/
|
|
594
615
|
function matchCheckFiles(check, files, appliesTo) {
|
|
595
616
|
const scope = Array.isArray(appliesTo) ? appliesTo : [];
|
|
@@ -634,7 +655,7 @@ function matchCheckFiles(check, files, appliesTo) {
|
|
|
634
655
|
* Does `content` contain the literal token?
|
|
635
656
|
*
|
|
636
657
|
* Plain substring containment, and nothing else, ever. The token is a
|
|
637
|
-
* client-authored byte sequence from the vault repository;
|
|
658
|
+
* client-authored byte sequence from the vault repository; ADR-71924 §3 bars it
|
|
638
659
|
* from every pattern compiler, so a token that LOOKS like a pattern is tested
|
|
639
660
|
* as the literal text it is.
|
|
640
661
|
*/
|
|
@@ -932,21 +953,173 @@ function evaluatePractices(opts) {
|
|
|
932
953
|
budgetExhausted,
|
|
933
954
|
};
|
|
934
955
|
}
|
|
956
|
+
// ─── Diff scoping (D-05-08) ───────────────────────────────────────────────────
|
|
957
|
+
/** How far up from a phase directory the manifest walk may climb. */
|
|
958
|
+
const MANIFEST_WALK_MAX = 12;
|
|
959
|
+
/** The manifest file `/pp-undo` already reads for exactly this purpose. */
|
|
960
|
+
const PHASE_MANIFEST_FILE = '.phase-manifest.json';
|
|
935
961
|
/**
|
|
936
|
-
*
|
|
937
|
-
*
|
|
938
|
-
*
|
|
939
|
-
*
|
|
940
|
-
*
|
|
941
|
-
* `
|
|
942
|
-
*
|
|
943
|
-
*
|
|
944
|
-
*
|
|
945
|
-
*
|
|
962
|
+
* An engine-authored guard over engine-written values.
|
|
963
|
+
*
|
|
964
|
+
* The shas come from `.phase-manifest.json` — written by `recordPhaseStart` /
|
|
965
|
+
* `recordPhaseCommits` below, reached through `pp-tools vault manifest
|
|
966
|
+
* start|record` from the execute-phase step files — and, on the trace-digest
|
|
967
|
+
* rung, from the `gitCommit` digests in a phase's SUMMARY `vault:` blocks.
|
|
968
|
+
* Both files are editable planning data and every sha is interpolated into a
|
|
969
|
+
* git ARGV, where a leading `-` would be read as a flag rather than as a
|
|
970
|
+
* revision. This is the one shape a sha may have. It is NOT the client-authored
|
|
971
|
+
* path: no vault byte reaches this, so ADR-71924 §3 is untouched (it bans
|
|
972
|
+
* compiling a CLIENT-authored byte into a pattern, which nothing here does).
|
|
973
|
+
*/
|
|
974
|
+
const COMMIT_SHA_SHAPE = /^[0-9a-fA-F]{7,64}$/;
|
|
975
|
+
/** The leading `NN` or `NN.M` token of a phase directory slug. */
|
|
976
|
+
const PHASE_SLUG_SHAPE = /^(\d+(?:\.\d+)?)/;
|
|
977
|
+
/**
|
|
978
|
+
* Is this manifest entry stamped for a DIFFERENT phase directory? (R1)
|
|
979
|
+
*
|
|
980
|
+
* Entries are keyed by phase NUMBER, and `/pp-new-milestone
|
|
981
|
+
* --reset-phase-numbers` reuses numbers, so a new milestone's `01-new` would
|
|
982
|
+
* otherwise inherit `01-old`'s anchor and commits. The writers stamp
|
|
983
|
+
* `phaseDir` with the directory basename; an entry whose stamp names another
|
|
984
|
+
* basename belongs to another phase. An entry with NO stamp (written before
|
|
985
|
+
* the stamp existed, or by hand) is not stale — it keeps the behaviour it had.
|
|
986
|
+
*/
|
|
987
|
+
function isStaleEntry(entry, slug) {
|
|
988
|
+
return typeof entry.phaseDir === 'string' && entry.phaseDir !== slug;
|
|
989
|
+
}
|
|
990
|
+
/** The one sentence a stale entry is disclosed with, shared by the reader and nothing else. */
|
|
991
|
+
function staleEntryNote(key, recorded, slug) {
|
|
992
|
+
return (`the phase manifest entry "${boundedToken(key)}" is stale: it was recorded for phase directory ` +
|
|
993
|
+
`"${boundedToken(recorded)}", not "${boundedToken(slug)}" (a phase number reused after a milestone reset), ` +
|
|
994
|
+
'so its startCommit and commits were ignored');
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* The commit list this phase's manifest entry records, plus the NAMED ground
|
|
998
|
+
* on which there was none (05-10).
|
|
999
|
+
*
|
|
1000
|
+
* Walks UP from the phase directory and tries the manifest at each ancestor,
|
|
1001
|
+
* nearest first, which covers both the flat `.planning/phases/NN-slug` layout
|
|
1002
|
+
* and the partitioned `.planning/workstreams/<name>/phases/NN-slug` one without
|
|
1003
|
+
* either being special-cased. Never throws: every failure at every step is a
|
|
1004
|
+
* fallback to the base-branch range, which is a WIDER comparison, never a
|
|
1005
|
+
* narrower one.
|
|
1006
|
+
*
|
|
1007
|
+
* The miss ground is recorded NEAREST-FIRST and never overwritten by a further
|
|
1008
|
+
* ancestor: the manifest closest to the phase directory is the one an operator
|
|
1009
|
+
* would go and look at, so it is the one worth naming.
|
|
1010
|
+
*
|
|
1011
|
+
* `startCommit` is ADDITIVE (260915-ik2): an entry may carry it beside
|
|
1012
|
+
* `commits`, and the `{ phases: { "05": { commits: [...] } } }` shape
|
|
1013
|
+
* `/pp-undo` reads stays valid — an entry with only `commits` resolves exactly
|
|
1014
|
+
* as it did before the field existed.
|
|
946
1015
|
*/
|
|
947
1016
|
function phaseCommitsFor(root, phaseDir) {
|
|
948
|
-
const
|
|
949
|
-
|
|
1017
|
+
const phaseDirAbs = node_path_1.default.resolve(phaseDir);
|
|
1018
|
+
const slug = node_path_1.default.basename(phaseDirAbs);
|
|
1019
|
+
const slugMatch = PHASE_SLUG_SHAPE.exec(slug);
|
|
1020
|
+
if (!slugMatch)
|
|
1021
|
+
return { commits: [], miss: AUDIT_FALLBACK_REASON.PHASE_SLUG_UNRECOGNISED, startCommit: null, notes: [] };
|
|
1022
|
+
const rawToken = slugMatch[1];
|
|
1023
|
+
const notes = [];
|
|
1024
|
+
// A directory named `05-x` and a manifest keyed `5` are the same phase; try
|
|
1025
|
+
// both spellings rather than guessing which writer produced the file.
|
|
1026
|
+
const keys = [rawToken, String(Number(rawToken))];
|
|
1027
|
+
let current = node_path_1.default.dirname(phaseDirAbs);
|
|
1028
|
+
const rootAbs = node_path_1.default.resolve(root);
|
|
1029
|
+
// Nearest-first, and never widened once set — see the header note.
|
|
1030
|
+
let miss = AUDIT_FALLBACK_REASON.MANIFEST_NOT_FOUND;
|
|
1031
|
+
const recordMiss = (ground) => {
|
|
1032
|
+
if (miss === AUDIT_FALLBACK_REASON.MANIFEST_NOT_FOUND)
|
|
1033
|
+
miss = ground;
|
|
1034
|
+
};
|
|
1035
|
+
for (let depth = 0; depth < MANIFEST_WALK_MAX; depth += 1) {
|
|
1036
|
+
const candidate = node_path_1.default.join(current, PHASE_MANIFEST_FILE);
|
|
1037
|
+
if (node_fs_1.default.existsSync(candidate)) {
|
|
1038
|
+
try {
|
|
1039
|
+
const parsed = JSON.parse(node_fs_1.default.readFileSync(candidate, 'utf8'));
|
|
1040
|
+
const phases = parsed && typeof parsed === 'object' ? parsed.phases : undefined;
|
|
1041
|
+
if (phases && typeof phases === 'object') {
|
|
1042
|
+
// A key present with an empty commit list is a DIFFERENT operator
|
|
1043
|
+
// problem from a key that is not there at all, so the two grounds
|
|
1044
|
+
// are separated rather than both reading "no entry".
|
|
1045
|
+
// A STALE entry (R1) is another phase's record, so for this phase it
|
|
1046
|
+
// is no entry at all: it counts as missing, and it is disclosed.
|
|
1047
|
+
const usable = (key) => {
|
|
1048
|
+
if (!Object.prototype.hasOwnProperty.call(phases, key))
|
|
1049
|
+
return false;
|
|
1050
|
+
const entry = phases[key];
|
|
1051
|
+
return !(entry && typeof entry === 'object' && isStaleEntry(entry, slug));
|
|
1052
|
+
};
|
|
1053
|
+
const present = keys.some(usable);
|
|
1054
|
+
recordMiss(present ? AUDIT_FALLBACK_REASON.NO_COMMITS_RECORDED : AUDIT_FALLBACK_REASON.PHASE_ENTRY_MISSING);
|
|
1055
|
+
// Both fields are read across both key spellings before returning,
|
|
1056
|
+
// so an anchor under `5` is not hidden by a commit list under `05`.
|
|
1057
|
+
let startCommit = null;
|
|
1058
|
+
let commits = null;
|
|
1059
|
+
for (const key of keys) {
|
|
1060
|
+
const entry = Object.prototype.hasOwnProperty.call(phases, key) ? phases[key] : undefined;
|
|
1061
|
+
if (!entry)
|
|
1062
|
+
continue;
|
|
1063
|
+
if (typeof entry === 'object' && isStaleEntry(entry, slug)) {
|
|
1064
|
+
const note = staleEntryNote(key, entry.phaseDir, slug);
|
|
1065
|
+
if (!notes.includes(note))
|
|
1066
|
+
notes.push(note);
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
const start = entry.startCommit;
|
|
1070
|
+
if (startCommit === null && typeof start === 'string' && COMMIT_SHA_SHAPE.test(start)) {
|
|
1071
|
+
startCommit = start;
|
|
1072
|
+
}
|
|
1073
|
+
const listed = entry.commits;
|
|
1074
|
+
if (commits === null && Array.isArray(listed) && listed.length > 0) {
|
|
1075
|
+
commits = listed.filter((c) => typeof c === 'string');
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
if (commits !== null)
|
|
1079
|
+
return { commits, miss: null, startCommit, notes };
|
|
1080
|
+
// An anchor with no commit list yet (execute:pre ran, verify:post has
|
|
1081
|
+
// not) IS a record, so it carries no miss ground.
|
|
1082
|
+
if (startCommit !== null)
|
|
1083
|
+
return { commits: [], miss: null, startCommit, notes };
|
|
1084
|
+
}
|
|
1085
|
+
else {
|
|
1086
|
+
recordMiss(AUDIT_FALLBACK_REASON.MANIFEST_MALFORMED);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
catch {
|
|
1090
|
+
// A malformed manifest is not a reason to invent a range; fall through
|
|
1091
|
+
// to the base-branch fallback, which is over-broad but honest. The
|
|
1092
|
+
// GROUND is recorded, because "the manifest you are looking at is not
|
|
1093
|
+
// valid JSON" is a repairable fact and "there is no manifest" is not
|
|
1094
|
+
// the same one.
|
|
1095
|
+
recordMiss(AUDIT_FALLBACK_REASON.MANIFEST_MALFORMED);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
const parent = node_path_1.default.dirname(current);
|
|
1099
|
+
if (parent === current)
|
|
1100
|
+
break;
|
|
1101
|
+
if (current === rootAbs)
|
|
1102
|
+
break;
|
|
1103
|
+
current = parent;
|
|
1104
|
+
}
|
|
1105
|
+
return { commits: [], miss, startCommit: null, notes };
|
|
1106
|
+
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Does the phase manifest hold a usable entry for this phase directory? (16-03, D-20)
|
|
1109
|
+
*
|
|
1110
|
+
* The seal leaf's `phase_manifest` artifact kind asks THIS reader, so "this
|
|
1111
|
+
* phase has a manifest record" means exactly what the audit's own range ladder
|
|
1112
|
+
* reads: an entry under either key spelling, not stale (R1), carrying a
|
|
1113
|
+
* `commits` list or a `startCommit`. A second walk in the leaf could disagree
|
|
1114
|
+
* with this one about a stale or malformed manifest. Never throws.
|
|
1115
|
+
*/
|
|
1116
|
+
function hasPhaseManifestEntry(root, phaseDir) {
|
|
1117
|
+
try {
|
|
1118
|
+
return phaseCommitsFor(root, phaseDir).miss === null;
|
|
1119
|
+
}
|
|
1120
|
+
catch {
|
|
1121
|
+
return false;
|
|
1122
|
+
}
|
|
950
1123
|
}
|
|
951
1124
|
/**
|
|
952
1125
|
* Resolve the git range this audit compares, and record WHERE it came from.
|
|
@@ -958,7 +1131,7 @@ function phaseCommitsFor(root, phaseDir) {
|
|
|
958
1131
|
* to prevent (D-05-08). And when work is committed straight onto the base
|
|
959
1132
|
* branch, `base...HEAD` is EMPTY — the UAT-6-F1 halt.
|
|
960
1133
|
*
|
|
961
|
-
* A FOUR-RUNG LADDER, in this order and no other:
|
|
1134
|
+
* A FOUR-RUNG LADDER, in this order and no other (260915-ik2):
|
|
962
1135
|
*
|
|
963
1136
|
* 1. `phase-start-commit` — the manifest entry's `startCommit`, the commit
|
|
964
1137
|
* HEAD sat at when `/pp-execute-phase` began the phase (written at
|
|
@@ -996,7 +1169,7 @@ function phaseCommitsFor(root, phaseDir) {
|
|
|
996
1169
|
* with it so an operator can see the third rung was tried.
|
|
997
1170
|
*/
|
|
998
1171
|
function resolvePhaseRange(opts) {
|
|
999
|
-
const exec = typeof opts.exec === 'function' ? opts.exec : shell_command_projection_cjs_1.
|
|
1172
|
+
const exec = typeof opts.exec === 'function' ? opts.exec : shell_command_projection_cjs_1.execGitLocal;
|
|
1000
1173
|
const found = (() => {
|
|
1001
1174
|
try {
|
|
1002
1175
|
return phaseCommitsFor(opts.root, opts.phaseDir);
|
|
@@ -1123,7 +1296,7 @@ function resolvePhaseRange(opts) {
|
|
|
1123
1296
|
...withLead(digest.notes),
|
|
1124
1297
|
};
|
|
1125
1298
|
}
|
|
1126
|
-
// ─── The trace-digest rung ────────────────────────────
|
|
1299
|
+
// ─── The trace-digest rung (260915-ik2, UAT-6-F1) ────────────────────────────
|
|
1127
1300
|
/** The `gitCommit` shape the trace builder records — 40- or 64-hex, lowercase. */
|
|
1128
1301
|
const TRACE_DIGEST_SHAPE = /^[0-9a-f]{40}$|^[0-9a-f]{64}$/;
|
|
1129
1302
|
/** At most this many distinct digests are verified and ranked, with a note when it bites. */
|
|
@@ -1136,7 +1309,7 @@ const TRACE_DIGEST_CANDIDATE_CAP = 64;
|
|
|
1136
1309
|
* bounded by `AUDIT_FILE_READ_MAX`), parsed with the SHIPPED trace parser, and
|
|
1137
1310
|
* its subjects' digests collected when they have the builder's own shape; the
|
|
1138
1311
|
* wider `COMMIT_SHA_SHAPE` guard is applied again before any sha reaches argv
|
|
1139
|
-
* (
|
|
1312
|
+
* (T-ik2-02). The distinct set is capped, then each is verified with
|
|
1140
1313
|
* `rev-parse --verify <sha>^{commit}`: an unverifiable digest is never an
|
|
1141
1314
|
* anchor.
|
|
1142
1315
|
*
|
|
@@ -1250,6 +1423,333 @@ function traceDigestRange(opts) {
|
|
|
1250
1423
|
return none(`the SUMMARY trace digests could not be read: ${errnoOf(err)}`);
|
|
1251
1424
|
}
|
|
1252
1425
|
}
|
|
1426
|
+
/**
|
|
1427
|
+
* Where this phase's manifest lives, or the named ground on which it cannot.
|
|
1428
|
+
*
|
|
1429
|
+
* `phaseDir` resolves against `cwd`, then against `root`; the first that is an
|
|
1430
|
+
* existing directory CONTAINED in the root wins (T-ik2-03), lexically or with
|
|
1431
|
+
* symlinks resolved on both sides (a macOS temp root is `/var` and
|
|
1432
|
+
* `/private/var` at once). The manifest sits two levels above the phase
|
|
1433
|
+
* directory: `.planning/.phase-manifest.json` on the flat layout — the file
|
|
1434
|
+
* `/pp-undo` reads — and `.planning/workstreams/<ws>/.phase-manifest.json` on
|
|
1435
|
+
* a partitioned one, which the reader's walk finds at depth one and which keeps
|
|
1436
|
+
* two workstreams' `05` entries apart.
|
|
1437
|
+
*/
|
|
1438
|
+
function resolveManifestTarget(opts) {
|
|
1439
|
+
const unresolvable = { ok: false, reason: PHASE_MANIFEST_WRITE_REASON.PHASE_DIR_UNRESOLVABLE };
|
|
1440
|
+
if (typeof opts?.root !== 'string' || opts.root === '')
|
|
1441
|
+
return unresolvable;
|
|
1442
|
+
if (typeof opts?.phaseDir !== 'string' || opts.phaseDir === '' || opts.phaseDir.includes('\0'))
|
|
1443
|
+
return unresolvable;
|
|
1444
|
+
const rootLexical = node_path_1.default.resolve(opts.root);
|
|
1445
|
+
const rootReal = resolvedRootOf(rootLexical);
|
|
1446
|
+
const bases = [typeof opts.cwd === 'string' && opts.cwd !== '' ? opts.cwd : process.cwd(), rootLexical];
|
|
1447
|
+
let phaseDirAbs = null;
|
|
1448
|
+
let rootAbs = rootLexical;
|
|
1449
|
+
for (const base of bases) {
|
|
1450
|
+
const abs = node_path_1.default.resolve(base, opts.phaseDir);
|
|
1451
|
+
try {
|
|
1452
|
+
if (!node_fs_1.default.statSync(abs).isDirectory())
|
|
1453
|
+
continue;
|
|
1454
|
+
}
|
|
1455
|
+
catch {
|
|
1456
|
+
continue;
|
|
1457
|
+
}
|
|
1458
|
+
if (isContainedIn(abs, rootLexical)) {
|
|
1459
|
+
phaseDirAbs = abs;
|
|
1460
|
+
rootAbs = rootLexical;
|
|
1461
|
+
break;
|
|
1462
|
+
}
|
|
1463
|
+
const real = resolvedRootOf(abs);
|
|
1464
|
+
if (isContainedIn(real, rootReal)) {
|
|
1465
|
+
phaseDirAbs = real;
|
|
1466
|
+
rootAbs = rootReal;
|
|
1467
|
+
break;
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
if (phaseDirAbs === null)
|
|
1471
|
+
return unresolvable;
|
|
1472
|
+
const slugMatch = PHASE_SLUG_SHAPE.exec(node_path_1.default.basename(phaseDirAbs));
|
|
1473
|
+
if (!slugMatch)
|
|
1474
|
+
return { ok: false, reason: PHASE_MANIFEST_WRITE_REASON.PHASE_SLUG_UNRECOGNISED };
|
|
1475
|
+
const manifestAbs = node_path_1.default.join(node_path_1.default.dirname(node_path_1.default.dirname(phaseDirAbs)), PHASE_MANIFEST_FILE);
|
|
1476
|
+
if (!isContainedIn(manifestAbs, rootAbs))
|
|
1477
|
+
return unresolvable;
|
|
1478
|
+
return {
|
|
1479
|
+
ok: true,
|
|
1480
|
+
target: {
|
|
1481
|
+
rootAbs,
|
|
1482
|
+
manifestAbs,
|
|
1483
|
+
manifestPath: node_path_1.default.relative(rootAbs, manifestAbs).split(node_path_1.default.sep).join('/'),
|
|
1484
|
+
rawToken: slugMatch[1],
|
|
1485
|
+
phaseDirAbs,
|
|
1486
|
+
phaseSlug: node_path_1.default.basename(phaseDirAbs),
|
|
1487
|
+
},
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
/**
|
|
1491
|
+
* The existing manifest, parsed for an ADDITIVE write — or `null` when it
|
|
1492
|
+
* must not be overwritten: unparseable JSON, a non-object document, a
|
|
1493
|
+
* non-object `phases`, or a non-object entry for this phase (T-ik2-05). An
|
|
1494
|
+
* absent file is an empty document, not a refusal.
|
|
1495
|
+
*/
|
|
1496
|
+
function readManifestForWrite(manifestAbs) {
|
|
1497
|
+
if (!node_fs_1.default.existsSync(manifestAbs))
|
|
1498
|
+
return { doc: {}, phases: {}, raw: null };
|
|
1499
|
+
try {
|
|
1500
|
+
const raw = node_fs_1.default.readFileSync(manifestAbs, 'utf8');
|
|
1501
|
+
const parsed = JSON.parse(raw);
|
|
1502
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
1503
|
+
return null;
|
|
1504
|
+
const doc = parsed;
|
|
1505
|
+
if (doc.phases === undefined)
|
|
1506
|
+
return { doc, phases: {}, raw };
|
|
1507
|
+
if (!doc.phases || typeof doc.phases !== 'object' || Array.isArray(doc.phases))
|
|
1508
|
+
return null;
|
|
1509
|
+
return { doc, phases: doc.phases, raw };
|
|
1510
|
+
}
|
|
1511
|
+
catch {
|
|
1512
|
+
return null;
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
/** The key this phase's entry lives under: an existing spelling wins, else the slug's own token. */
|
|
1516
|
+
function manifestKeyFor(phases, rawToken) {
|
|
1517
|
+
const spellings = [rawToken, String(Number(rawToken))];
|
|
1518
|
+
return spellings.find((key) => Object.prototype.hasOwnProperty.call(phases, key)) ?? rawToken;
|
|
1519
|
+
}
|
|
1520
|
+
/** The entry under `key` as a plain object, `{}` when absent, `null` when present and not an object. */
|
|
1521
|
+
function manifestEntryOf(phases, key) {
|
|
1522
|
+
if (!Object.prototype.hasOwnProperty.call(phases, key))
|
|
1523
|
+
return {};
|
|
1524
|
+
const entry = phases[key];
|
|
1525
|
+
return entry && typeof entry === 'object' && !Array.isArray(entry) ? entry : null;
|
|
1526
|
+
}
|
|
1527
|
+
/** Two-space JSON with a trailing newline — the one serialization, so a re-record is byte-stable. */
|
|
1528
|
+
function serializeManifest(doc) {
|
|
1529
|
+
return `${JSON.stringify(doc, null, 2)}\n`;
|
|
1530
|
+
}
|
|
1531
|
+
/**
|
|
1532
|
+
* Write atomically: a sibling temp file named with the pid, then `renameSync`
|
|
1533
|
+
* over the target — the pattern `src/runtime-hooks-surface.cts` uses. The temp
|
|
1534
|
+
* file is removed on any failure. Returns `null` on success, else an errno.
|
|
1535
|
+
*/
|
|
1536
|
+
function writeManifestAtomic(manifestAbs, serialized) {
|
|
1537
|
+
const tmp = `${manifestAbs}.${process.pid}.tmp`;
|
|
1538
|
+
try {
|
|
1539
|
+
node_fs_1.default.writeFileSync(tmp, serialized, 'utf8');
|
|
1540
|
+
node_fs_1.default.renameSync(tmp, manifestAbs);
|
|
1541
|
+
return null;
|
|
1542
|
+
}
|
|
1543
|
+
catch (err) {
|
|
1544
|
+
try {
|
|
1545
|
+
node_fs_1.default.unlinkSync(tmp);
|
|
1546
|
+
}
|
|
1547
|
+
catch {
|
|
1548
|
+
// Nothing to remove, or it cannot be removed; the refusal below stands.
|
|
1549
|
+
}
|
|
1550
|
+
return errnoOf(err);
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
/**
|
|
1554
|
+
* Has this phase already been executed? (R2) `true` when its directory holds
|
|
1555
|
+
* any `*-SUMMARY.md` — the same file set the trace-digest rung reads — and
|
|
1556
|
+
* `null` when the directory cannot be listed, which the caller refuses on
|
|
1557
|
+
* rather than guessing.
|
|
1558
|
+
*/
|
|
1559
|
+
function phaseHasSummary(phaseDirAbs) {
|
|
1560
|
+
try {
|
|
1561
|
+
return node_fs_1.default.readdirSync(phaseDirAbs).some((name) => name.endsWith('-SUMMARY.md'));
|
|
1562
|
+
}
|
|
1563
|
+
catch {
|
|
1564
|
+
return null;
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
/** A verified commit sha from a `rev-parse --verify` probe, or `null`. */
|
|
1568
|
+
function verifiedSha(exec, rev, cwd) {
|
|
1569
|
+
const probe = exec(['rev-parse', '--verify', rev], { cwd });
|
|
1570
|
+
if (probe.exitCode !== 0 || probe.error)
|
|
1571
|
+
return null;
|
|
1572
|
+
const sha = String(probe.stdout || '').trim();
|
|
1573
|
+
return COMMIT_SHA_SHAPE.test(sha) ? sha : null;
|
|
1574
|
+
}
|
|
1575
|
+
/**
|
|
1576
|
+
* Anchor the phase: `phases[NN].startCommit = HEAD`, WRITE-ONCE.
|
|
1577
|
+
*
|
|
1578
|
+
* Called at execute:pre, in this order:
|
|
1579
|
+
*
|
|
1580
|
+
* 1. WRITE-ONCE. An entry for THIS phase directory that already carries a
|
|
1581
|
+
* start commit is left byte-identical and reported `unchanged: true`, so
|
|
1582
|
+
* a resume or a `--gaps-only` run keeps the anchor the phase actually
|
|
1583
|
+
* started from. An unstamped (legacy) entry counts as this phase's.
|
|
1584
|
+
* 2. ALREADY EXECUTED (R2). With no usable anchor, a phase directory that
|
|
1585
|
+
* already holds a `*-SUMMARY.md` is refused as `phase-already-executed`
|
|
1586
|
+
* and nothing is written: execute:pre fires on every re-run, and HEAD on
|
|
1587
|
+
* an executed phase sits after its own commits, so an anchor there would
|
|
1588
|
+
* put the phase's work outside its own range. The trace-digest rung is
|
|
1589
|
+
* the range source for such a phase.
|
|
1590
|
+
* 3. STALE (R1). An entry stamped for another phase directory — a number
|
|
1591
|
+
* reused after a milestone reset — is REPLACED by a fresh
|
|
1592
|
+
* `{ startCommit, phaseDir }`; its old commits are dropped. The result
|
|
1593
|
+
* carries `replacedStale: true`.
|
|
1594
|
+
*
|
|
1595
|
+
* Every write stamps `phaseDir` with the directory basename. Any `commits`
|
|
1596
|
+
* beside a non-stale entry are preserved. Never throws; a refusal is
|
|
1597
|
+
* `{ ok: false, action, reason }` with a `PHASE_MANIFEST_WRITE_REASON` member.
|
|
1598
|
+
*/
|
|
1599
|
+
function recordPhaseStart(opts) {
|
|
1600
|
+
const action = 'start';
|
|
1601
|
+
try {
|
|
1602
|
+
const exec = typeof opts?.exec === 'function' ? opts.exec : shell_command_projection_cjs_1.execGitLocal;
|
|
1603
|
+
const resolved = resolveManifestTarget(opts);
|
|
1604
|
+
if (!resolved.ok)
|
|
1605
|
+
return { ok: false, action, reason: resolved.reason };
|
|
1606
|
+
const { target } = resolved;
|
|
1607
|
+
const read = readManifestForWrite(target.manifestAbs);
|
|
1608
|
+
if (read === null)
|
|
1609
|
+
return { ok: false, action, reason: PHASE_MANIFEST_WRITE_REASON.MANIFEST_MALFORMED };
|
|
1610
|
+
const key = manifestKeyFor(read.phases, target.rawToken);
|
|
1611
|
+
const entry = manifestEntryOf(read.phases, key);
|
|
1612
|
+
if (entry === null)
|
|
1613
|
+
return { ok: false, action, reason: PHASE_MANIFEST_WRITE_REASON.MANIFEST_MALFORMED };
|
|
1614
|
+
const stale = isStaleEntry(entry, target.phaseSlug);
|
|
1615
|
+
// Write-once: the anchor a phase began from is never moved by a later run.
|
|
1616
|
+
if (!stale && typeof entry.startCommit === 'string' && COMMIT_SHA_SHAPE.test(entry.startCommit)) {
|
|
1617
|
+
return {
|
|
1618
|
+
ok: true,
|
|
1619
|
+
action,
|
|
1620
|
+
manifestPath: target.manifestPath,
|
|
1621
|
+
phase: key,
|
|
1622
|
+
startCommit: entry.startCommit,
|
|
1623
|
+
unchanged: true,
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
// Checked before HEAD is read: a refusal asks git nothing (R2).
|
|
1627
|
+
const executed = phaseHasSummary(target.phaseDirAbs);
|
|
1628
|
+
if (executed === null) {
|
|
1629
|
+
return { ok: false, action, reason: PHASE_MANIFEST_WRITE_REASON.PHASE_DIR_UNRESOLVABLE };
|
|
1630
|
+
}
|
|
1631
|
+
if (executed)
|
|
1632
|
+
return { ok: false, action, reason: PHASE_MANIFEST_WRITE_REASON.PHASE_ALREADY_EXECUTED };
|
|
1633
|
+
const head = verifiedSha(exec, 'HEAD^{commit}', target.rootAbs);
|
|
1634
|
+
if (head === null)
|
|
1635
|
+
return { ok: false, action, reason: PHASE_MANIFEST_WRITE_REASON.HEAD_UNRESOLVABLE };
|
|
1636
|
+
const fresh = stale
|
|
1637
|
+
? { startCommit: head, phaseDir: target.phaseSlug }
|
|
1638
|
+
: { ...entry, startCommit: head, phaseDir: target.phaseSlug };
|
|
1639
|
+
const doc = { ...read.doc, phases: { ...read.phases, [key]: fresh } };
|
|
1640
|
+
const failure = writeManifestAtomic(target.manifestAbs, serializeManifest(doc));
|
|
1641
|
+
if (failure !== null) {
|
|
1642
|
+
return { ok: false, action, reason: PHASE_MANIFEST_WRITE_REASON.WRITE_FAILED, detail: failure };
|
|
1643
|
+
}
|
|
1644
|
+
return {
|
|
1645
|
+
ok: true,
|
|
1646
|
+
action,
|
|
1647
|
+
manifestPath: target.manifestPath,
|
|
1648
|
+
phase: key,
|
|
1649
|
+
startCommit: head,
|
|
1650
|
+
unchanged: false,
|
|
1651
|
+
...(stale ? { replacedStale: true } : {}),
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
catch (err) {
|
|
1655
|
+
return { ok: false, action, reason: PHASE_MANIFEST_WRITE_REASON.WRITE_FAILED, detail: errnoOf(err) };
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
/**
|
|
1659
|
+
* Close the phase's range: `phases[NN].commits` =
|
|
1660
|
+
* `git rev-list --reverse --no-merges <startCommit>..HEAD`, oldest first.
|
|
1661
|
+
*
|
|
1662
|
+
* Called at verify:post, before the `vault.audit` gate. Recomputed from the
|
|
1663
|
+
* anchor on every call, so two consecutive records leave the file
|
|
1664
|
+
* byte-identical (and the second writes nothing). An empty list is a valid,
|
|
1665
|
+
* honest record. A phase with no anchor answers `start-not-recorded` — the case
|
|
1666
|
+
* the trace-digest rung covers — and so does an entry stamped for ANOTHER
|
|
1667
|
+
* phase directory (R1): that anchor is another phase's, so nothing is listed
|
|
1668
|
+
* from it and the file is left for `start` to replace. A successful record
|
|
1669
|
+
* stamps `phaseDir`, which also adopts an unstamped (legacy) entry. Never
|
|
1670
|
+
* throws.
|
|
1671
|
+
*/
|
|
1672
|
+
function recordPhaseCommits(opts) {
|
|
1673
|
+
const action = 'record';
|
|
1674
|
+
try {
|
|
1675
|
+
const exec = typeof opts?.exec === 'function' ? opts.exec : shell_command_projection_cjs_1.execGitLocal;
|
|
1676
|
+
const resolved = resolveManifestTarget(opts);
|
|
1677
|
+
if (!resolved.ok)
|
|
1678
|
+
return { ok: false, action, reason: resolved.reason };
|
|
1679
|
+
const { target } = resolved;
|
|
1680
|
+
const read = readManifestForWrite(target.manifestAbs);
|
|
1681
|
+
if (read === null)
|
|
1682
|
+
return { ok: false, action, reason: PHASE_MANIFEST_WRITE_REASON.MANIFEST_MALFORMED };
|
|
1683
|
+
const key = manifestKeyFor(read.phases, target.rawToken);
|
|
1684
|
+
const entry = manifestEntryOf(read.phases, key);
|
|
1685
|
+
if (entry === null)
|
|
1686
|
+
return { ok: false, action, reason: PHASE_MANIFEST_WRITE_REASON.MANIFEST_MALFORMED };
|
|
1687
|
+
if (isStaleEntry(entry, target.phaseSlug)) {
|
|
1688
|
+
return {
|
|
1689
|
+
ok: false,
|
|
1690
|
+
action,
|
|
1691
|
+
reason: PHASE_MANIFEST_WRITE_REASON.START_NOT_RECORDED,
|
|
1692
|
+
detail: 'the entry under this phase number was recorded for another phase directory',
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
const start = entry.startCommit;
|
|
1696
|
+
if (typeof start !== 'string' || !COMMIT_SHA_SHAPE.test(start)) {
|
|
1697
|
+
return { ok: false, action, reason: PHASE_MANIFEST_WRITE_REASON.START_NOT_RECORDED };
|
|
1698
|
+
}
|
|
1699
|
+
if (verifiedSha(exec, `${start}^{commit}`, target.rootAbs) === null) {
|
|
1700
|
+
return { ok: false, action, reason: PHASE_MANIFEST_WRITE_REASON.START_UNVERIFIABLE };
|
|
1701
|
+
}
|
|
1702
|
+
const listed = exec(['rev-list', '--reverse', '--no-merges', `${start}..HEAD`], { cwd: target.rootAbs });
|
|
1703
|
+
if (listed.exitCode !== 0 || listed.error) {
|
|
1704
|
+
return {
|
|
1705
|
+
ok: false,
|
|
1706
|
+
action,
|
|
1707
|
+
reason: PHASE_MANIFEST_WRITE_REASON.HEAD_UNRESOLVABLE,
|
|
1708
|
+
detail: 'git rev-list could not list the commits since the start commit',
|
|
1709
|
+
};
|
|
1710
|
+
}
|
|
1711
|
+
const commits = String(listed.stdout || '')
|
|
1712
|
+
.split('\n')
|
|
1713
|
+
.map((line) => line.trim())
|
|
1714
|
+
.filter((line) => COMMIT_SHA_SHAPE.test(line));
|
|
1715
|
+
const doc = {
|
|
1716
|
+
...read.doc,
|
|
1717
|
+
phases: { ...read.phases, [key]: { ...entry, commits, phaseDir: target.phaseSlug } },
|
|
1718
|
+
};
|
|
1719
|
+
const serialized = serializeManifest(doc);
|
|
1720
|
+
if (serialized !== read.raw) {
|
|
1721
|
+
const failure = writeManifestAtomic(target.manifestAbs, serialized);
|
|
1722
|
+
if (failure !== null) {
|
|
1723
|
+
return { ok: false, action, reason: PHASE_MANIFEST_WRITE_REASON.WRITE_FAILED, detail: failure };
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
return {
|
|
1727
|
+
ok: true,
|
|
1728
|
+
action,
|
|
1729
|
+
manifestPath: target.manifestPath,
|
|
1730
|
+
phase: key,
|
|
1731
|
+
startCommit: start,
|
|
1732
|
+
commits,
|
|
1733
|
+
commitCount: commits.length,
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
catch (err) {
|
|
1737
|
+
return { ok: false, action, reason: PHASE_MANIFEST_WRITE_REASON.WRITE_FAILED, detail: errnoOf(err) };
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
/** The one-line operator rendering of a writer result; the step files print it and continue. */
|
|
1741
|
+
function renderManifestResult(result) {
|
|
1742
|
+
const r = (result && typeof result === 'object' ? result : {});
|
|
1743
|
+
if (r.ok !== true)
|
|
1744
|
+
return `Phase manifest not written: ${boundedToken(r.reason ?? 'unknown')}`;
|
|
1745
|
+
const sha7 = typeof r.startCommit === 'string' ? r.startCommit.slice(0, 7) : '';
|
|
1746
|
+
if (r.action === 'record') {
|
|
1747
|
+
return `Phase ${boundedToken(r.phase)}: ${Number(r.commitCount)} commit(s) recorded from ${sha7}..HEAD in ${boundedToken(r.manifestPath)}`;
|
|
1748
|
+
}
|
|
1749
|
+
return (`Phase ${boundedToken(r.phase)} anchored at ${sha7} in ${boundedToken(r.manifestPath)}` +
|
|
1750
|
+
(r.unchanged === true ? ' (already anchored; unchanged)' : '') +
|
|
1751
|
+
(r.replacedStale === true ? ' (replaced a stale entry recorded for another phase directory)' : ''));
|
|
1752
|
+
}
|
|
1253
1753
|
// ─── The audit envelope ───────────────────────────────────────────────────────
|
|
1254
1754
|
/** Is this resolved absolute path the root itself, or somewhere beneath it? */
|
|
1255
1755
|
function isContainedIn(abs, rootAbs) {
|
|
@@ -1366,7 +1866,8 @@ function diffFilesFor(root, range, exec) {
|
|
|
1366
1866
|
.map((f) => f.replace(/\\/g, '/'))
|
|
1367
1867
|
// Load-bearing: a practice glob like `**/*.md` would otherwise match the
|
|
1368
1868
|
// phase's own planning artifacts and FAIL a practice about product code.
|
|
1369
|
-
|
|
1869
|
+
// 16-03: the seal leaf owns the `.planning/` path test.
|
|
1870
|
+
.filter((f) => !vaultSeal.isPlanningPath(f));
|
|
1370
1871
|
const capped = all.length > AUDIT_DIFF_FILE_CAP;
|
|
1371
1872
|
return { ok: true, files: capped ? all.slice(0, AUDIT_DIFF_FILE_CAP) : all, capped, total: all.length };
|
|
1372
1873
|
}
|
|
@@ -1394,7 +1895,7 @@ function failItemLines(entries) {
|
|
|
1394
1895
|
return [];
|
|
1395
1896
|
// 05-REVIEW CR-02: `id` and `detail` carry vault-authored bytes (the parser
|
|
1396
1897
|
// pins `id` to the vault filename; `detail` quotes require/forbid tokens),
|
|
1397
|
-
// and
|
|
1898
|
+
// and `check-query-gate.md` prints `message` verbatim into an orchestrating
|
|
1398
1899
|
// agent's context on every run — the channel this module's own header calls
|
|
1399
1900
|
// agent-EXECUTABLE. So the itemized region is defanged and fenced with the
|
|
1400
1901
|
// idiom `renderPracticeEntry` already uses: a planted fence marker cannot
|
|
@@ -1403,15 +1904,10 @@ function failItemLines(entries) {
|
|
|
1403
1904
|
// stays OUTSIDE the fence — the notice's claim about what is inside must
|
|
1404
1905
|
// stay true. The repair sentence is appended by the callers, after this
|
|
1405
1906
|
// return, so it remains the closing line a prepend can never cut off.
|
|
1406
|
-
//
|
|
1407
|
-
// REDACTED before defanged: a `require`/`forbid` token the vault wrote can
|
|
1408
|
-
// carry a credential-bearing remote, and this message is printed verbatim.
|
|
1409
|
-
// `redactRemoteText` is the one redactor (src/vault.cts); the fence is the
|
|
1410
|
-
// one fence. Both are applied, neither is re-authored here.
|
|
1411
1907
|
const items = [];
|
|
1412
1908
|
for (const fail of fails.slice(0, AUDIT_MESSAGE_PROBLEM_CAP)) {
|
|
1413
|
-
const detail = fail.detail ? ` — ${defangFenceMarkers(
|
|
1414
|
-
items.push(`- ${defangFenceMarkers(
|
|
1909
|
+
const detail = fail.detail ? ` — ${defangFenceMarkers(fail.detail)}` : '';
|
|
1910
|
+
items.push(`- ${defangFenceMarkers(fail.id)}: ${String(fail.reason)}${detail}`);
|
|
1415
1911
|
}
|
|
1416
1912
|
const lines = [EXTERNAL_CONTENT_FENCE_START, ...FENCE_NOTICE, '', ...items, EXTERNAL_CONTENT_FENCE_END];
|
|
1417
1913
|
const omitted = fails.length - Math.min(fails.length, AUDIT_MESSAGE_PROBLEM_CAP);
|
|
@@ -1447,7 +1943,7 @@ function noteLine(note) {
|
|
|
1447
1943
|
*
|
|
1448
1944
|
* THE ONE COMPOSITION POINT, used by every return path of `runVaultAudit`
|
|
1449
1945
|
* (05-10, G-1c). Before it, `notes[]` reached neither `block` nor `message` and
|
|
1450
|
-
*
|
|
1946
|
+
* `check-query-gate.md` reads nothing else — so a truncated diff, a refused
|
|
1451
1947
|
* pair count, an unreadable practices directory and a base-branch fallback were
|
|
1452
1948
|
* all invisible at the gate. The notes go FIRST because they are the limits
|
|
1453
1949
|
* that may explain the findings below them, and because putting them first is
|
|
@@ -1534,7 +2030,7 @@ function verdictsWithoutComparison(practices, reason, detail) {
|
|
|
1534
2030
|
* `vaultUnreadable` is computed from TYPED FIELDS and from the catch, never
|
|
1535
2031
|
* from note prose. That is the whole point: before 05-10 the only vault-side
|
|
1536
2032
|
* signal was a string in `notes[]`, `notes[]` reached neither `block` nor
|
|
1537
|
-
* `message`, and the gate
|
|
2033
|
+
* `message`, and the enforcer at check-query-gate.md reads nothing else — so
|
|
1538
2034
|
* every vault-side miss rendered as a clean pass (G-1a). A `startsWith()` over
|
|
1539
2035
|
* the note would have re-created the prose-matching this codebase already
|
|
1540
2036
|
* rejected once (`matchBudgetNotes`).
|
|
@@ -1547,6 +2043,7 @@ function runDiscovery(discover, root) {
|
|
|
1547
2043
|
notes: [...(discovery?.notes ?? [])],
|
|
1548
2044
|
skipped: Array.isArray(discovery?.skipped) ? discovery.skipped : [],
|
|
1549
2045
|
vaultUnreadable: discovery?.readFailed === true || discovery?.vaultPresent === false,
|
|
2046
|
+
vaultStandardsOnly: discovery?.vaultStandardsOnly === true,
|
|
1550
2047
|
};
|
|
1551
2048
|
}
|
|
1552
2049
|
catch {
|
|
@@ -1559,6 +2056,7 @@ function runDiscovery(discover, root) {
|
|
|
1559
2056
|
notes: ['the vault could not be read; no practice was evaluated'],
|
|
1560
2057
|
skipped: [],
|
|
1561
2058
|
vaultUnreadable: true,
|
|
2059
|
+
vaultStandardsOnly: false,
|
|
1562
2060
|
};
|
|
1563
2061
|
}
|
|
1564
2062
|
}
|
|
@@ -1641,8 +2139,6 @@ function refusedOperatorRangeResult() {
|
|
|
1641
2139
|
mechanicallyChecked: counts.mechanicallyChecked,
|
|
1642
2140
|
applicablePractices: counts.applicablePractices,
|
|
1643
2141
|
evaluatedNothing: counts.evaluatedNothing,
|
|
1644
|
-
// Nothing is read for a refused input, the ledgers included.
|
|
1645
|
-
recordedCensus: null,
|
|
1646
2142
|
practices: [],
|
|
1647
2143
|
pairsEvaluated: 0,
|
|
1648
2144
|
pairCap: MATCH_PAIR_CAP,
|
|
@@ -1670,13 +2166,8 @@ function refusedOperatorRangeResult() {
|
|
|
1670
2166
|
* blocking gate.
|
|
1671
2167
|
*/
|
|
1672
2168
|
function runVaultAudit(opts) {
|
|
1673
|
-
const exec = typeof opts.exec === 'function' ? opts.exec : shell_command_projection_cjs_1.
|
|
1674
|
-
|
|
1675
|
-
// worktree-aware answer the practice trace pins against — so the audit and
|
|
1676
|
-
// the ledger it is read beside describe one vault.
|
|
1677
|
-
const discover = typeof opts.discover === 'function'
|
|
1678
|
-
? opts.discover
|
|
1679
|
-
: (root) => discoverPracticeFiles(root, vaultMod.resolveVaultCacheDir(root).dir);
|
|
2169
|
+
const exec = typeof opts.exec === 'function' ? opts.exec : shell_command_projection_cjs_1.execGitLocal;
|
|
2170
|
+
const discover = typeof opts.discover === 'function' ? opts.discover : discoverPracticeFiles;
|
|
1680
2171
|
const readFile = typeof opts.readFile === 'function' ? opts.readFile : createBoundedFileReader(opts.root);
|
|
1681
2172
|
// D-08-19. An operator range REPLACES the ladder rather than joining it: the
|
|
1682
2173
|
// operator is answering the question the ladder failed to answer, so
|
|
@@ -1693,15 +2184,6 @@ function runVaultAudit(opts) {
|
|
|
1693
2184
|
const { range, rangeSource, fallbackReason, baseBranchRefused, notes: rangeNotes } = resolved;
|
|
1694
2185
|
const discovery = runDiscovery(discover, opts.root);
|
|
1695
2186
|
const practices = discovery.practices;
|
|
1696
|
-
// Read once, from the ledgers, and carried on every arm below.
|
|
1697
|
-
const recordedCensus = (() => {
|
|
1698
|
-
try {
|
|
1699
|
-
return readRecordedCensus(opts.phaseDir);
|
|
1700
|
-
}
|
|
1701
|
-
catch {
|
|
1702
|
-
return null;
|
|
1703
|
-
}
|
|
1704
|
-
})();
|
|
1705
2187
|
const notes = [...discovery.notes];
|
|
1706
2188
|
if (operatorRange !== null) {
|
|
1707
2189
|
// THE DISCLOSURE (D-08-19, threat T-08-25 — accepted, on this sentence).
|
|
@@ -1733,11 +2215,11 @@ function runVaultAudit(opts) {
|
|
|
1733
2215
|
// the ground is a repairable fact rather than an unavoidable one. Stating
|
|
1734
2216
|
// it is what makes the range record auditable on a checkout that has no
|
|
1735
2217
|
// manifest to read — a fresh clone or CI runner where `.planning/` is
|
|
1736
|
-
// gitignored, since only the executing checkout writes one.
|
|
2218
|
+
// gitignored, since only the executing checkout writes one (260915-ik2).
|
|
1737
2219
|
notes.push(`phase range fell back to the base-branch form (${range}) because the phase-scoped range could not be ` +
|
|
1738
2220
|
`resolved: ${fallbackReason}`);
|
|
1739
2221
|
}
|
|
1740
|
-
// The ladder's own notes: a trace-digest range's under-scope
|
|
2222
|
+
// The ladder's own notes (260915-ik2): a trace-digest range's under-scope
|
|
1741
2223
|
// disclosure, or the statement that the digest rung was tried and found
|
|
1742
2224
|
// nothing verifiable. Either is a limit on what the range establishes.
|
|
1743
2225
|
for (const note of rangeNotes ?? [])
|
|
@@ -1776,7 +2258,43 @@ function runVaultAudit(opts) {
|
|
|
1776
2258
|
mechanicallyChecked: vaultArmCounts.mechanicallyChecked,
|
|
1777
2259
|
applicablePractices: vaultArmCounts.applicablePractices,
|
|
1778
2260
|
evaluatedNothing: vaultArmCounts.evaluatedNothing,
|
|
1779
|
-
|
|
2261
|
+
practices: entries,
|
|
2262
|
+
pairsEvaluated: 0,
|
|
2263
|
+
pairCap: MATCH_PAIR_CAP,
|
|
2264
|
+
budgetMs: MATCH_BUDGET_MS,
|
|
2265
|
+
filesConsidered: 0,
|
|
2266
|
+
filesCapped: false,
|
|
2267
|
+
notes,
|
|
2268
|
+
};
|
|
2269
|
+
}
|
|
2270
|
+
if (discovery.vaultStandardsOnly) {
|
|
2271
|
+
// Quick 261015-at3 (QD-06, Vince 2026-10-15): the client vault WAS read,
|
|
2272
|
+
// and it is a standards vault — INDEX.md at its root, no practice files —
|
|
2273
|
+
// read in practices mode. Discovery yields zero practices by construction,
|
|
2274
|
+
// and before this arm the audit compared nothing, warned, and did not
|
|
2275
|
+
// block. Placed after the vault-unreadable arm (an unread vault is the more
|
|
2276
|
+
// basic fact) and before the diff is read (nothing here needs it).
|
|
2277
|
+
const entries = [
|
|
2278
|
+
...verdictsWithoutComparison(practices, AUDIT_FAIL_REASON.VAULT_STANDARDS_ONLY, `the client vault holds standards (INDEX.md) and no practice files, so this practice was never compared against ${range}`),
|
|
2279
|
+
...unloadable,
|
|
2280
|
+
];
|
|
2281
|
+
const armCounts = countVerdicts(entries);
|
|
2282
|
+
const lead = `Vault audit: the client vault at .planning/vault holds standards (INDEX.md) and no practice files, ` +
|
|
2283
|
+
`but vault.mode is practices, so no practice was compared against ${range} (source: ${rangeSource}) ` +
|
|
2284
|
+
`and nothing was verified. ${failingEntries(entries).length} practice check(s) are recorded as FAILED.`;
|
|
2285
|
+
return {
|
|
2286
|
+
status: 'warn',
|
|
2287
|
+
// EXPLICITLY true, for the vault-unreadable arm's reason verbatim: on this
|
|
2288
|
+
// arm `entries` is empty by construction, `blocksOnVerdicts([])` is false,
|
|
2289
|
+
// and a gate handed no practice layer verified nothing (G-1a).
|
|
2290
|
+
block: true,
|
|
2291
|
+
message: composeWithNotes(notes, [lead, ...failItemLines(entries), AUDIT_STANDARDS_ONLY_REPAIR_SENTENCE].join('\n')),
|
|
2292
|
+
range,
|
|
2293
|
+
rangeSource,
|
|
2294
|
+
rangeOutcome: classifyRangeOutcome({ phaseScoped: isPhaseScopedSource(rangeSource), filesConsidered: 0 }),
|
|
2295
|
+
mechanicallyChecked: armCounts.mechanicallyChecked,
|
|
2296
|
+
applicablePractices: armCounts.applicablePractices,
|
|
2297
|
+
evaluatedNothing: armCounts.evaluatedNothing,
|
|
1780
2298
|
practices: entries,
|
|
1781
2299
|
pairsEvaluated: 0,
|
|
1782
2300
|
pairCap: MATCH_PAIR_CAP,
|
|
@@ -1808,7 +2326,6 @@ function runVaultAudit(opts) {
|
|
|
1808
2326
|
mechanicallyChecked: diffArmCounts.mechanicallyChecked,
|
|
1809
2327
|
applicablePractices: diffArmCounts.applicablePractices,
|
|
1810
2328
|
evaluatedNothing: diffArmCounts.evaluatedNothing,
|
|
1811
|
-
recordedCensus,
|
|
1812
2329
|
practices: entries,
|
|
1813
2330
|
pairsEvaluated: 0,
|
|
1814
2331
|
pairCap: MATCH_PAIR_CAP,
|
|
@@ -1832,7 +2349,7 @@ function runVaultAudit(opts) {
|
|
|
1832
2349
|
//
|
|
1833
2350
|
// On the FALLBACK range that reasoning does not hold, because nothing
|
|
1834
2351
|
// established that the range corresponds to this phase. The fallback is the
|
|
1835
|
-
// FOURTH rung of `resolvePhaseRange`'s ladder: it is reached
|
|
2352
|
+
// FOURTH rung of `resolvePhaseRange`'s ladder (260915-ik2): it is reached
|
|
1836
2353
|
// only when the phase has no usable manifest entry (no verifiable startCommit
|
|
1837
2354
|
// or commit) AND no verifiable SUMMARY trace digest. With
|
|
1838
2355
|
// `git.branching_strategy: none` — this project's own setting — `main...HEAD`
|
|
@@ -1865,7 +2382,7 @@ function runVaultAudit(opts) {
|
|
|
1865
2382
|
}
|
|
1866
2383
|
if (unverifiableEmptyRange) {
|
|
1867
2384
|
// PREFIXED with the outcome code (08-04, D-08-17). `message` is composed
|
|
1868
|
-
// from `notes`, and
|
|
2385
|
+
// from `notes`, and `check-query-gate.md` prints `message` verbatim — so
|
|
1869
2386
|
// putting the code at the head of this sentence is what makes
|
|
1870
2387
|
// `range_unresolvable` greppable in BOTH of the two fields the enforcer
|
|
1871
2388
|
// reads, not only in the envelope field a workflow would have to parse.
|
|
@@ -1893,7 +2410,7 @@ function runVaultAudit(opts) {
|
|
|
1893
2410
|
if (counts.evaluatedNothing) {
|
|
1894
2411
|
// PREFIXED with the outcome token, following the `range_unresolvable`
|
|
1895
2412
|
// precedent above: `message` is composed from `notes` and
|
|
1896
|
-
//
|
|
2413
|
+
// `check-query-gate.md` prints `message` verbatim, so putting the token at
|
|
1897
2414
|
// the head of this sentence is what makes the fact greppable in BOTH of the
|
|
1898
2415
|
// fields the enforcer reads, not only in an envelope field a workflow would
|
|
1899
2416
|
// have to parse. Composed entirely from engine strings and integers — no
|
|
@@ -1902,7 +2419,7 @@ function runVaultAudit(opts) {
|
|
|
1902
2419
|
notes.push(`${AUDIT_COVERAGE_OUTCOME.EVALUATED_NOTHING}: ${counts.mechanicallyChecked} of ` +
|
|
1903
2420
|
`${counts.applicablePractices} applicable practice(s) were mechanically checked over ${range} ` +
|
|
1904
2421
|
`(source: ${rangeSource}) — no check block was compared against any file, so this run verified ` +
|
|
1905
|
-
`nothing mechanically and no passing claim can be drawn from it
|
|
2422
|
+
`nothing mechanically and no passing claim can be drawn from it`);
|
|
1906
2423
|
}
|
|
1907
2424
|
return {
|
|
1908
2425
|
// D-10-11: a run in which nothing was mechanically checked is `warn`, never
|
|
@@ -1933,7 +2450,6 @@ function runVaultAudit(opts) {
|
|
|
1933
2450
|
mechanicallyChecked: counts.mechanicallyChecked,
|
|
1934
2451
|
applicablePractices: counts.applicablePractices,
|
|
1935
2452
|
evaluatedNothing: counts.evaluatedNothing,
|
|
1936
|
-
recordedCensus,
|
|
1937
2453
|
practices: audited,
|
|
1938
2454
|
pairsEvaluated: evaluated.pairsEvaluated,
|
|
1939
2455
|
pairCap: evaluated.pairCap,
|
|
@@ -1985,7 +2501,6 @@ function renderAuditReport(result) {
|
|
|
1985
2501
|
// much was audited. The two sentences answer the same question and an
|
|
1986
2502
|
// operator reads them together; counts only, never a percentage or a score.
|
|
1987
2503
|
renderMechanicalCoverageLine(counts),
|
|
1988
|
-
renderRecordedCensusLine(result.recordedCensus ?? null),
|
|
1989
2504
|
];
|
|
1990
2505
|
for (const section of AUDIT_SECTIONS) {
|
|
1991
2506
|
lines.push('');
|
|
@@ -2021,8 +2536,6 @@ module.exports = {
|
|
|
2021
2536
|
// one the day either is reworded.
|
|
2022
2537
|
AUDIT_COVERAGE_OUTCOME,
|
|
2023
2538
|
renderMechanicalCoverageLine,
|
|
2024
|
-
// The recorded census's one sentence (read through the ledger API).
|
|
2025
|
-
renderRecordedCensusLine,
|
|
2026
2539
|
// 10-06 (MECH-03, D-10-05): the ONE lead this module composes for an
|
|
2027
2540
|
// `enforced-by:` claim. Exported so the tests — and any surface that must
|
|
2028
2541
|
// prove it never says "verified by" — assert against the SHIPPED string.
|
|
@@ -2033,6 +2546,8 @@ module.exports = {
|
|
|
2033
2546
|
AUDIT_OPERATOR_RANGE_SHAPE,
|
|
2034
2547
|
AUDIT_OPERATOR_RANGE_MAX_LENGTH,
|
|
2035
2548
|
isValidOperatorRange,
|
|
2549
|
+
// 260915-ik2: why a manifest writer wrote nothing.
|
|
2550
|
+
PHASE_MANIFEST_WRITE_REASON,
|
|
2036
2551
|
AUDIT_FILE_READ_MAX,
|
|
2037
2552
|
AUDIT_MESSAGE_PROBLEM_CAP,
|
|
2038
2553
|
AUDIT_DIFF_FILE_CAP,
|
|
@@ -2043,6 +2558,8 @@ module.exports = {
|
|
|
2043
2558
|
AUDIT_REPAIR_SENTENCE,
|
|
2044
2559
|
AUDIT_DIFF_REPAIR_SENTENCE,
|
|
2045
2560
|
AUDIT_VAULT_REPAIR_SENTENCE,
|
|
2561
|
+
// Quick 261015-at3: the standards-only arm's repair, same harvest rule.
|
|
2562
|
+
AUDIT_STANDARDS_ONLY_REPAIR_SENTENCE,
|
|
2046
2563
|
// 08-04: the refused-operator-range arm's repair, exported on the same
|
|
2047
2564
|
// harvest rule as the three above.
|
|
2048
2565
|
AUDIT_OPERATOR_RANGE_REPAIR_SENTENCE,
|
|
@@ -2052,6 +2569,13 @@ module.exports = {
|
|
|
2052
2569
|
// The diff scoping, exported separately from the envelope so a surface that
|
|
2053
2570
|
// only needs to STATE the range never has to run an audit to learn it.
|
|
2054
2571
|
resolvePhaseRange,
|
|
2572
|
+
// 16-03 (D-20): the manifest-presence half of the seal leaf's artifact predicate.
|
|
2573
|
+
hasPhaseManifestEntry,
|
|
2574
|
+
// 260915-ik2 (UAT-6-F1): the manifest writers `vault manifest start|record`
|
|
2575
|
+
// reaches, and their one-line operator rendering.
|
|
2576
|
+
recordPhaseStart,
|
|
2577
|
+
recordPhaseCommits,
|
|
2578
|
+
renderManifestResult,
|
|
2055
2579
|
createBoundedFileReader,
|
|
2056
2580
|
// The whole audit, and its human rendering.
|
|
2057
2581
|
runVaultAudit,
|