@pareto4/pp-core 1.17.1 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +5 -3
- package/agents/pp-code-fixer.md +9 -16
- package/agents/pp-code-reviewer.md +2 -78
- package/agents/pp-codebase-mapper.md +3 -0
- package/agents/pp-debugger.md +3 -2
- package/agents/pp-executor.md +25 -30
- package/agents/pp-integration-checker.md +1 -0
- package/agents/pp-phase-researcher.md +3 -1
- package/agents/pp-plan-checker.md +3 -0
- package/agents/pp-planner.md +16 -12
- package/agents/pp-research-synthesizer.md +1 -1
- package/agents/pp-ui-auditor.md +1 -0
- package/agents/pp-ui-researcher.md +3 -1
- package/agents/pp-vault-reviewer.md +43 -189
- package/agents/pp-verifier.md +10 -8
- package/commands/pp-add-tests.md +1 -1
- package/commands/pp-debug.md +1 -0
- package/commands/pp-init.md +4 -3
- package/commands/pp-ns-manage.md +1 -1
- package/commands/pp-ns-review.md +1 -1
- package/commands/pp-quick.md +3 -0
- package/commands/pp-review-backlog.md +2 -1
- package/commands/pp-vault-promote.md +7 -8
- package/commands/pp-vault-review.md +6 -7
- package/gemini-extension.json +1 -1
- package/package.json +3 -6
- package/pp-core/bin/lib/capability-registry.cjs +15 -357
- package/pp-core/bin/lib/capability-state.cjs +15 -79
- package/pp-core/bin/lib/capability-writer.cjs +2 -16
- package/pp-core/bin/lib/check-command-router.cjs +342 -2298
- package/pp-core/bin/lib/clusters.cjs +2 -2
- package/pp-core/bin/lib/commands.cjs +79 -4
- package/pp-core/bin/lib/compose-bindings.cjs +99 -16
- package/pp-core/bin/lib/config.cjs +3 -6
- package/pp-core/bin/lib/discovery-rollup.cjs +9 -6
- package/pp-core/bin/lib/discovery-screens.cjs +20 -2
- package/pp-core/bin/lib/discovery-sweep.cjs +12 -3
- package/pp-core/bin/lib/discovery-vault-check.cjs +37 -3
- package/pp-core/bin/lib/git-base-branch.cjs +10 -13
- package/pp-core/bin/lib/git-provider.cjs +5 -6
- package/pp-core/bin/lib/init-contract.cjs +55 -42
- package/pp-core/bin/lib/init-interactive.cjs +6 -20
- package/pp-core/bin/lib/io.cjs +2 -8
- package/pp-core/bin/lib/loop-resolver.cjs +8 -32
- package/pp-core/bin/lib/org-profile.cjs +16 -92
- package/pp-core/bin/lib/vault-command-router.cjs +174 -1206
- package/pp-core/bin/lib/vault-coordinates.cjs +4 -4
- package/pp-core/bin/lib/vault-index.cjs +44 -74
- package/pp-core/bin/lib/vault-seal.cjs +37 -122
- package/pp-core/bin/lib/vault-standards-promote.cjs +577 -0
- package/pp-core/bin/lib/vault-standards-review.cjs +293 -0
- package/pp-core/bin/lib/vault-standards.cjs +15 -16
- package/pp-core/bin/lib/vault-trace.cjs +40 -3016
- package/pp-core/bin/lib/vault-update.cjs +5 -26
- package/pp-core/bin/lib/vault.cjs +122 -316
- package/pp-core/bin/pp-tools.cjs +15 -36
- package/pp-core/org-profile.json +1 -1
- package/pp-core/references/autonomous-best-effort.md +4 -3
- package/pp-core/references/autonomous-lifecycle.md +1 -0
- package/pp-core/references/autonomous-smart-discuss.md +5 -1
- package/pp-core/references/discovery-id-conventions.md +35 -2
- package/pp-core/references/execute-mvp-tdd.md +3 -3
- package/pp-core/references/git-integration.md +57 -33
- package/pp-core/references/git-planning-commit.md +31 -9
- package/pp-core/references/phase-argument-parsing.md +1 -0
- package/pp-core/references/planner-gap-closure.md +1 -0
- package/pp-core/references/planner-revision.md +3 -1
- package/pp-core/references/planner-tdd.md +4 -4
- package/pp-core/references/planner-vault-standards.md +16 -25
- package/pp-core/references/planning-config.md +3 -3
- package/pp-core/references/shell-portability.md +28 -0
- package/pp-core/references/tdd.md +20 -16
- package/pp-core/references/worktree-path-safety.md +1 -0
- package/pp-core/templates/README.md +1 -1
- package/pp-core/templates/VAULT-REVIEW.md +24 -170
- package/pp-core/templates/milestone.md +5 -5
- package/pp-core/workflows/add-backlog.md +1 -1
- package/pp-core/workflows/add-tests.md +8 -5
- package/pp-core/workflows/add-todo.md +3 -1
- package/pp-core/workflows/ai-integration-phase.md +2 -1
- package/pp-core/workflows/audit-fix.md +7 -6
- package/pp-core/workflows/audit-milestone.md +1 -0
- package/pp-core/workflows/autonomous.md +6 -0
- package/pp-core/workflows/check-todos.md +2 -1
- package/pp-core/workflows/cleanup.md +3 -1
- package/pp-core/workflows/code-review-fix.md +15 -8
- package/pp-core/workflows/code-review.md +16 -58
- package/pp-core/workflows/complete-milestone.md +13 -3
- package/pp-core/workflows/debug.md +4 -2
- package/pp-core/workflows/diagnose-issues.md +1 -1
- package/pp-core/workflows/discovery-sprint/prototype.md +2 -2
- package/pp-core/workflows/discovery-sprint/session-13-gates.md +1 -1
- package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +51 -7
- package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +5 -3
- package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +10 -8
- package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +11 -2
- package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +5 -5
- package/pp-core/workflows/discovery-sprint/sessions/06-security.md +3 -3
- package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +2 -2
- package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +14 -2
- package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +2 -2
- package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +10 -10
- package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +11 -8
- package/pp-core/workflows/discovery-sprint/sessions/12-delivery-model.md +21 -18
- package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +5 -3
- package/pp-core/workflows/discovery-sprint/templates/context-template.md +9 -3
- package/pp-core/workflows/discovery-sprint/vault-context.md +17 -12
- package/pp-core/workflows/discovery-sprint.md +11 -8
- package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +1 -0
- package/pp-core/workflows/discuss-phase-assumptions.md +5 -3
- package/pp-core/workflows/discuss-phase.md +8 -3
- package/pp-core/workflows/docs-update.md +1 -0
- package/pp-core/workflows/eval-review.md +2 -1
- package/pp-core/workflows/execute-phase/steps/check-query-gate.md +13 -14
- package/pp-core/workflows/execute-phase/steps/cross-ai-delegation.md +1 -0
- package/pp-core/workflows/execute-phase/steps/execute-pre-dispatch.md +5 -25
- package/pp-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -0
- package/pp-core/workflows/execute-phase/steps/vault-fetch.md +18 -6
- package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +6 -109
- package/pp-core/workflows/execute-phase/steps/verify-post-gate.md +1 -17
- package/pp-core/workflows/execute-phase.md +21 -12
- package/pp-core/workflows/execute-plan.md +24 -15
- package/pp-core/workflows/explore.md +1 -1
- package/pp-core/workflows/forensics.md +4 -2
- package/pp-core/workflows/health.md +1 -0
- package/pp-core/workflows/help/modes/full.md +1 -1
- package/pp-core/workflows/import.md +2 -1
- package/pp-core/workflows/ingest-docs.md +1 -0
- package/pp-core/workflows/init/steps/client-context-capture.md +1 -0
- package/pp-core/workflows/init/steps/github-provisioning.md +145 -0
- package/pp-core/workflows/init/steps/gitlab-provisioning.md +18 -4
- package/pp-core/workflows/init/steps/reachability-outcome.md +3 -1
- package/pp-core/workflows/init.md +56 -66
- package/pp-core/workflows/map-codebase.md +5 -1
- package/pp-core/workflows/milestone-summary.md +2 -1
- package/pp-core/workflows/mvp-phase.md +1 -0
- package/pp-core/workflows/new-milestone.md +9 -5
- package/pp-core/workflows/new-project.md +9 -8
- package/pp-core/workflows/new-workspace.md +1 -0
- package/pp-core/workflows/next.md +2 -1
- package/pp-core/workflows/pause-work.md +6 -4
- package/pp-core/workflows/plan-milestone-gaps.md +3 -1
- package/pp-core/workflows/plan-phase/steps/chunked-planning.md +1 -1
- package/pp-core/workflows/plan-phase.md +21 -6
- package/pp-core/workflows/plan-review-convergence.md +2 -0
- package/pp-core/workflows/plant-seed.md +4 -2
- package/pp-core/workflows/pr-branch.md +2 -0
- package/pp-core/workflows/progress.md +5 -0
- package/pp-core/workflows/quick.md +19 -12
- package/pp-core/workflows/reapply-patches.md +3 -0
- package/pp-core/workflows/remove-phase.md +1 -1
- package/pp-core/workflows/resume-project.md +3 -0
- package/pp-core/workflows/review.md +7 -1
- package/pp-core/workflows/secure-phase.md +2 -1
- package/pp-core/workflows/session-report.md +1 -0
- package/pp-core/workflows/ship.md +8 -1
- package/pp-core/workflows/sketch-wrap-up.md +1 -1
- package/pp-core/workflows/sketch.md +3 -2
- package/pp-core/workflows/spec-phase.md +2 -1
- package/pp-core/workflows/spike-wrap-up.md +1 -1
- package/pp-core/workflows/spike.md +4 -3
- package/pp-core/workflows/sync-skills.md +5 -2
- package/pp-core/workflows/thread.md +3 -2
- package/pp-core/workflows/transition.md +6 -0
- package/pp-core/workflows/ui-phase.md +3 -1
- package/pp-core/workflows/ui-review.md +2 -1
- package/pp-core/workflows/ultraplan-phase.md +1 -0
- package/pp-core/workflows/undo.md +14 -10
- package/pp-core/workflows/update.md +5 -0
- package/pp-core/workflows/validate-phase.md +9 -6
- package/pp-core/workflows/vault-review.md +65 -186
- package/pp-core/workflows/vault-write-back.md +78 -110
- package/pp-core/workflows/verify-phase.md +5 -0
- package/pp-core/workflows/verify-work.md +9 -2
- package/scripts/gen-capability-registry.cjs +12 -93
- package/scripts/lint-test-file-count.allowlist.json +1 -19
- package/scripts/lint-test-file-count.cjs +5 -6
- package/scripts/prompt-injection-scan.sh +1 -4
- package/scripts/sync-runtime-launcher.cjs +115 -5
- package/pp-core/bin/lib/practices-review-check.cjs +0 -944
- package/pp-core/bin/lib/vault-audit.cjs +0 -2594
- package/pp-core/bin/lib/vault-mode.cjs +0 -235
- package/pp-core/bin/lib/vault-practices.cjs +0 -2057
- package/pp-core/bin/lib/vault-review.cjs +0 -2102
- package/pp-core/bin/lib/vault-write-back.cjs +0 -905
- package/pp-core/references/planner-vault-practices.md +0 -95
- package/pp-core/references/practice-arithmetic-contract.md +0 -121
- package/pp-core/references/verifier-practice-verdicts.md +0 -263
- package/pp-core/templates/practices/README.md +0 -248
- package/pp-core/templates/practices/error-handling.md +0 -18
- package/pp-core/templates/practices/minimal-comments.md +0 -14
- package/pp-core/templates/practices/naming-convention.md +0 -18
- package/pp-core/templates/practices/practice-template.md +0 -23
- package/pp-core/workflows/execute-phase/steps/verifier-practice-payload.md +0 -165
- package/scripts/gen-practice-arithmetic-contract.cjs +0 -412
- package/scripts/practice-arithmetic-contract.allow.json +0 -35
- package/scripts/record-vault-mode-golden.cjs +0 -89
- package/scripts/vault-calibrate.cjs +0 -996
|
@@ -25,8 +25,7 @@ const decisions_cjs_1 = require("./decisions.cjs");
|
|
|
25
25
|
const ui_safety_gate_cjs_1 = require("./ui-safety-gate.cjs");
|
|
26
26
|
const semver_compare_cjs_1 = require("./semver-compare.cjs");
|
|
27
27
|
// Quick 261003-lqi (FIX-C): the ship-time drift check resolves the platform tag
|
|
28
|
-
// through the init-time owner
|
|
29
|
-
// load-time graph (via vault-practices.cjs), so these imports add no module.
|
|
28
|
+
// through the init-time owner, so it and init cannot disagree about the tag.
|
|
30
29
|
const compose_bindings_cjs_1 = require("./compose-bindings.cjs");
|
|
31
30
|
const git_provider_cjs_1 = require("./git-provider.cjs");
|
|
32
31
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
@@ -44,52 +43,22 @@ const { runGapAnalysis } = gapCheckerModule;
|
|
|
44
43
|
const prohibition_enforcement_cjs_1 = require("./prohibition-enforcement.cjs");
|
|
45
44
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
46
45
|
const vaultModule = require("./vault.cjs");
|
|
47
|
-
// CR-04 (03-11): `realPathOrSelf` / `isInsideDir` now live in the vault leaf so
|
|
48
|
-
// this router and src/vault-command-router.cts share ONE containment predicate.
|
|
49
46
|
// 04-03 (D-12): `computeVaultVerdict` + `VAULT_REASON` moved there too, so this
|
|
50
|
-
// router and the
|
|
47
|
+
// router and the `vault standards block` verb in src/vault-command-router.cts obtain the
|
|
51
48
|
// tier-keyed `vault.required` verdict from ONE implementation.
|
|
52
49
|
// 04-16 (WR-06): `resolveVaultRequiredFlag` joins them, so the INERT branch can
|
|
53
50
|
// resolve the policy flag WITHOUT the manifest read `computeVaultVerdict` performs.
|
|
54
|
-
const { readManifest,
|
|
55
|
-
const git_base_branch_cjs_1 = require("./git-base-branch.cjs");
|
|
51
|
+
const { readManifest, hasPlatformBindings, platformBindingsPath, computeVaultVerdict, resolveVaultRequiredFlag, resolveTracePhaseDir, toRelPosix, VAULT_REASON, isPassingVaultReason, redactCloneTarget, vaultIndexEnvelopeFields, isVaultGateActive, computeVaultIndexState, resolveVaultCacheDir } = vaultModule;
|
|
56
52
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
57
|
-
//
|
|
58
|
-
const vaultPracticesModule = require("./vault-practices.cjs");
|
|
59
|
-
const { matchPractices, boundedToken, VALID_SEVERITIES,
|
|
60
|
-
// 05-13 (G-4): harvested, never copied — the block channel states the same
|
|
61
|
-
// string from the same constant, so the two cannot drift (T-4-49).
|
|
62
|
-
PRACTICES_VAULT_UNREADABLE_SENTENCE, PRACTICES_VAULT_ABSENT_SENTENCE,
|
|
63
|
-
// Quick 261003-lqi (FIX-A): the recorded-miss sentence, from the same owner.
|
|
64
|
-
PRACTICES_VAULT_MISS_SENTENCE,
|
|
65
|
-
// Quick 261015-at3: a standards vault read in practices mode, same owner.
|
|
66
|
-
PRACTICES_VAULT_STANDARDS_ONLY_SENTENCE,
|
|
67
|
-
// 05-REVIEW WR-02: one composer for the refused-file lines, and the fence
|
|
68
|
-
// they render inside — harvested, never re-authored, so this gate and the
|
|
69
|
-
// planner block state a refusal identically.
|
|
70
|
-
renderSkippedLines, EXTERNAL_CONTENT_FENCE_START, EXTERNAL_CONTENT_FENCE_END, FENCE_NOTICE, } = vaultPracticesModule;
|
|
71
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
72
|
-
const frontmatterModule = require("./frontmatter.cjs");
|
|
73
|
-
const { extractFrontmatter } = frontmatterModule;
|
|
74
|
-
// The practice-trace leaf. Adapters import leaves; leaves never import
|
|
75
|
-
// adapters — so every structural judgement about a trace lives there and this
|
|
76
|
-
// router holds no parsing of its own. A second parser here would drift from the
|
|
77
|
-
// one the producer writes against, and the drift would be silent.
|
|
53
|
+
// The override leaf: the one home of the per-run gate override.
|
|
78
54
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
79
55
|
const vaultTraceModule = require("./vault-trace.cjs");
|
|
80
56
|
// 16-03: the seal leaf owns the artifact caps and the `.planning/` path test.
|
|
81
57
|
// Its top level imports only Node built-ins (D-41), so this edge adds no cycle.
|
|
82
58
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
83
59
|
const vaultSeal = require("./vault-seal.cjs");
|
|
84
|
-
// 04-03 (D-14):
|
|
85
|
-
|
|
86
|
-
// router carried Phase-2 duplicates of all three, deleted in 04-03 and rewired
|
|
87
|
-
// through this import (the leaf's own header note called for exactly that).
|
|
88
|
-
// 04-07 (D-09): `comparePracticeCoverage` + the frozen kind table. The RELATION
|
|
89
|
-
// between a plan and its summary is composed HERE — this router enumerates and
|
|
90
|
-
// pairs — while the comparison of one pair stays in the leaf with every other
|
|
91
|
-
// structural judgement about a trace.
|
|
92
|
-
const { parseVaultTraceBlock, validateTraceStatement, validatePracticesBody, comparePracticeCoverage, TRACE_PROBLEM_KIND, evaluateTraceOverride, sanitizeOverrideReason, VAULT_OVERRIDE_ENV } = vaultTraceModule;
|
|
60
|
+
// 04-03 (D-14): the CANONICAL override implementations.
|
|
61
|
+
const { evaluateTraceOverride, sanitizeOverrideReason, VAULT_OVERRIDE_ENV } = vaultTraceModule;
|
|
93
62
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
94
63
|
function normalizePhrase(text) {
|
|
95
64
|
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
@@ -802,6 +771,57 @@ function cmdUiSafetyGate(projectDir, args, raw) {
|
|
|
802
771
|
}
|
|
803
772
|
output(computeUiSafetyGate(projectDir, phase), raw, undefined);
|
|
804
773
|
}
|
|
774
|
+
/** Subject types the TDD review gate looks for. */
|
|
775
|
+
const TDD_GATE_TYPES = new Set(['test', 'feat', 'refactor']);
|
|
776
|
+
/**
|
|
777
|
+
* Parse one commit message into the TDD gate keys it satisfies, each
|
|
778
|
+
* `<type>\0<plan id>`. The type comes from the Conventional Commit subject
|
|
779
|
+
* (`test(auth): …`, `test: …`, `test!: …`); the plan id from every
|
|
780
|
+
* `Phase: <id>` line in the message's last paragraph (the git trailer block).
|
|
781
|
+
* A subject whose scope IS the plan id (`test(01-02): …`, the convention
|
|
782
|
+
* before the trailer) yields that id too, so older history still counts.
|
|
783
|
+
*/
|
|
784
|
+
function tddGateKeys(message) {
|
|
785
|
+
const text = message.replace(/\r\n/g, '\n').trim();
|
|
786
|
+
const subject = text.split('\n', 1)[0] || '';
|
|
787
|
+
const m = /^([a-z]+)(?:\(([^)]*)\))?!?:/.exec(subject);
|
|
788
|
+
if (!m || !TDD_GATE_TYPES.has(m[1]))
|
|
789
|
+
return [];
|
|
790
|
+
const type = m[1];
|
|
791
|
+
const ids = new Set();
|
|
792
|
+
const paragraphs = text.split(/\n[ \t]*\n/);
|
|
793
|
+
if (paragraphs.length > 1) {
|
|
794
|
+
for (const line of paragraphs[paragraphs.length - 1].split('\n')) {
|
|
795
|
+
const t = /^Phase:[ \t]*(\S+)[ \t]*$/.exec(line);
|
|
796
|
+
if (t)
|
|
797
|
+
ids.add(t[1]);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
// Legacy: a scope shaped like a plan id (starts with a digit), never a codebase noun.
|
|
801
|
+
if (m[2] && /^\d[\w.-]*$/.test(m[2].trim()))
|
|
802
|
+
ids.add(m[2].trim());
|
|
803
|
+
return [...ids].map((id) => `${type}\0${id}`);
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Every TDD gate key satisfied by a commit that touches the working tree
|
|
807
|
+
* (`-- .`, as before), read in one `git log`. An unreadable history (no git,
|
|
808
|
+
* no commits) yields the empty set, so every gate reads as missing.
|
|
809
|
+
*/
|
|
810
|
+
function readTddGateCommits(projectDir) {
|
|
811
|
+
const keys = new Set();
|
|
812
|
+
let log = '';
|
|
813
|
+
try {
|
|
814
|
+
log = (0, node_child_process_1.execFileSync)('git', ['log', '--format=%x1e%B', '--', '.'], {
|
|
815
|
+
cwd: projectDir, encoding: 'utf-8', maxBuffer: 16 * 1024 * 1024, windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'],
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
catch { /* git unavailable or no commits */ }
|
|
819
|
+
for (const record of log.split('\x1e')) {
|
|
820
|
+
for (const key of tddGateKeys(record))
|
|
821
|
+
keys.add(key);
|
|
822
|
+
}
|
|
823
|
+
return keys;
|
|
824
|
+
}
|
|
805
825
|
function cmdTddReviewCheckpoint(projectDir, args, raw) {
|
|
806
826
|
// args[0] = 'check', args[1] = 'tdd-review-checkpoint' (normalized), args[2] = phase
|
|
807
827
|
const phase = args[2] || '';
|
|
@@ -861,31 +881,19 @@ function cmdTddReviewCheckpoint(projectDir, args, raw) {
|
|
|
861
881
|
return;
|
|
862
882
|
}
|
|
863
883
|
// For each TDD plan, extract the plan ID (padded plan number) and check git log
|
|
884
|
+
const gateCommits = readTddGateCommits(projectDir);
|
|
864
885
|
const rows = [];
|
|
865
886
|
for (const planPath of tddPlanFiles) {
|
|
866
887
|
// Extract plan ID from filename (e.g. "01-02-PLAN.md" → "01-02", or "03-PLAN.md" → "03")
|
|
867
888
|
const basename = node_path_1.default.basename(planPath, '-PLAN.md');
|
|
868
889
|
// planId for commit grep: phase-plan format, e.g. "01-02"
|
|
869
890
|
const planId = basename;
|
|
870
|
-
//
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
red = redCommit.trim().length > 0;
|
|
877
|
-
}
|
|
878
|
-
catch { /* git unavailable or no match */ }
|
|
879
|
-
try {
|
|
880
|
-
const greenCommit = (0, node_child_process_1.execFileSync)('git', ['log', '--oneline', `--grep=^feat(${planId}):`, '--', '.'], { cwd: projectDir, encoding: 'utf-8', maxBuffer: 1024 * 1024, windowsHide: true });
|
|
881
|
-
green = greenCommit.trim().length > 0;
|
|
882
|
-
}
|
|
883
|
-
catch { /* git unavailable or no match */ }
|
|
884
|
-
try {
|
|
885
|
-
const refactorCommit = (0, node_child_process_1.execFileSync)('git', ['log', '--oneline', `--grep=^refactor(${planId}):`, '--', '.'], { cwd: projectDir, encoding: 'utf-8', maxBuffer: 1024 * 1024, windowsHide: true });
|
|
886
|
-
refactor = refactorCommit.trim().length > 0;
|
|
887
|
-
}
|
|
888
|
-
catch { /* git unavailable or no match */ }
|
|
891
|
+
// Gate commits are found by type and the `Phase: {planId}` trailer — the
|
|
892
|
+
// scope names a part of the codebase, never the plan. A commit made before
|
|
893
|
+
// the trailer convention (`test({planId}): …`) still counts.
|
|
894
|
+
const red = gateCommits.has(`test\0${planId}`);
|
|
895
|
+
const green = gateCommits.has(`feat\0${planId}`);
|
|
896
|
+
const refactor = gateCommits.has(`refactor\0${planId}`);
|
|
889
897
|
const missing = [];
|
|
890
898
|
if (!red)
|
|
891
899
|
missing.push('RED');
|
|
@@ -915,7 +923,7 @@ function cmdTddReviewCheckpoint(projectDir, args, raw) {
|
|
|
915
923
|
table += '\n\n⚠ Gate violations are advisory — review before advancing.';
|
|
916
924
|
for (const r of rows.filter(row => row.status === 'FAIL')) {
|
|
917
925
|
table += `\n Plan ${r.planId} missing: ${r.missing.join(', ')} gate commit(s).`;
|
|
918
|
-
table += `\n Expected
|
|
926
|
+
table += `\n Expected commits: test(<area>): ... → feat(<area>): ..., each with the trailer Phase: ${r.planId}`;
|
|
919
927
|
}
|
|
920
928
|
}
|
|
921
929
|
const result = {
|
|
@@ -1146,7 +1154,7 @@ function cmdShipPlatformDrift(cwd, args, raw) {
|
|
|
1146
1154
|
}
|
|
1147
1155
|
// 04-03 (D-12): `VAULT_REQUIRED_KEY`, the frozen `VAULT_REASON` vocabulary and
|
|
1148
1156
|
// `resolveVaultRequiredFlag` moved VERBATIM to src/vault.cts, so the check
|
|
1149
|
-
// router here and the
|
|
1157
|
+
// router here and the `vault standards block` verb in src/vault-command-router.cts read one
|
|
1150
1158
|
// reason vocabulary and one policy resolver instead of drifting copies.
|
|
1151
1159
|
// `VAULT_REASON` is imported at the top of this file.
|
|
1152
1160
|
/**
|
|
@@ -1163,8 +1171,8 @@ const VAULT_FIELD_CAP = 64;
|
|
|
1163
1171
|
* `repo`, `ref` and `fallbackReason` are read from `.planning/vault/.manifest.json`,
|
|
1164
1172
|
* which is written from a client-controlled repository — and the enforcer prints
|
|
1165
1173
|
* this message VERBATIM into an agent's context on every run. That is a shorter
|
|
1166
|
-
* injection path than `SUMMARY.md` has, so it
|
|
1167
|
-
*
|
|
1174
|
+
* injection path than `SUMMARY.md` has, so it is redacted and bounded rather
|
|
1175
|
+
* than trusted because the string is short.
|
|
1168
1176
|
*
|
|
1169
1177
|
* Order is deliberate: redact BEFORE bounding. Truncating first could cut a
|
|
1170
1178
|
* credential in half and leave a fragment the redactor no longer recognises.
|
|
@@ -1206,26 +1214,6 @@ const VAULT_REASON_TEXT = Object.freeze({
|
|
|
1206
1214
|
[VAULT_REASON.CACHE_STALE]: 'the loaded client vault is past the configured TTL (vault.cache_ttl_hours) and cannot be treated as current',
|
|
1207
1215
|
[VAULT_REASON.CLIENT_REF_DRIFT]: 'the cached client vault was fetched from a ref the project no longer configures, so it is not the client\'s current standards',
|
|
1208
1216
|
});
|
|
1209
|
-
/**
|
|
1210
|
-
* Plain-words rendering of each NON-blocking reason — the pass vocabulary
|
|
1211
|
-
* (D-08-20). A sibling table rather than four more rows in `VAULT_REASON_TEXT`:
|
|
1212
|
-
* that map's entries are read as "why this build stopped" and are composed into
|
|
1213
|
-
* a sentence that begins "the client vault is not loaded", so a passing reason
|
|
1214
|
-
* placed there would be rendered by the block builder as its own negation.
|
|
1215
|
-
*
|
|
1216
|
-
* Same degradation rule as its sibling: a member without prose prints its raw
|
|
1217
|
-
* code (`?? reason`). And the same truthfulness rule: quick 261003-lqi (FIX-B)
|
|
1218
|
-
* reworded CLIENT_LOADED, which credited the fetch to the current run, because
|
|
1219
|
-
* a manifest read cannot know whether this run fetched — a cache-served or
|
|
1220
|
-
* hours-old `source: 'fetched'` manifest inside the TTL reaches this reason too.
|
|
1221
|
-
* Model: the 08-02 rewording of CACHE_STALE above. The `Vault:` line states
|
|
1222
|
-
* the fetch time (`fetched <fetchedAt>`), the one place that can.
|
|
1223
|
-
*/
|
|
1224
|
-
const VAULT_PASS_TEXT = Object.freeze({
|
|
1225
|
-
[VAULT_REASON.CLIENT_LOADED]: 'the loaded client vault is inside the configured TTL (vault.cache_ttl_hours) and is treated as current',
|
|
1226
|
-
[VAULT_REASON.CLIENT_CACHE_FRESH]: 'a client vault cache inside the configured TTL was loaded',
|
|
1227
|
-
[VAULT_REASON.CLIENT_FRESHNESS_UNPOLICED]: 'the client vault was loaded and freshness is not policed (vault.cache_ttl_hours is 0)',
|
|
1228
|
-
});
|
|
1229
1217
|
/**
|
|
1230
1218
|
* What every required-on message says about its own shelf life (D-08-06).
|
|
1231
1219
|
*
|
|
@@ -1266,8 +1254,7 @@ function volatilitySentence(fetchedAt) {
|
|
|
1266
1254
|
* SIX separately-assertable elements as of 08-02: the vault identity, the
|
|
1267
1255
|
* pinned ref (plus the CONFIGURED ref when the two have drifted apart), the
|
|
1268
1256
|
* reason in plain words, a diagnostic list, a remedy, and the verdict's own
|
|
1269
|
-
* volatility. Empty on a non-blocking verdict
|
|
1270
|
-
* what a pass says, and the two must not be able to be confused for each other.
|
|
1257
|
+
* volatility. Empty on a non-blocking verdict.
|
|
1271
1258
|
*
|
|
1272
1259
|
* THE REMEDIATION IS LABELLED, NOT MERELY LISTED (D-08-07, HGATE-03). The
|
|
1273
1260
|
* governing rule: a diagnostic an operator is told to run must not be able to
|
|
@@ -1306,40 +1293,6 @@ function buildVaultBlockMessage(reason, repo, ref, configuredRef = null, fetched
|
|
|
1306
1293
|
volatilitySentence(fetchedAt),
|
|
1307
1294
|
].join('\n');
|
|
1308
1295
|
}
|
|
1309
|
-
/**
|
|
1310
|
-
* Compose the operator-facing SUCCESS message (HGATE-06, D-08-20).
|
|
1311
|
-
*
|
|
1312
|
-
* WHY A PASS SPEAKS AT ALL. HGATE-06's UAT test 4 read FAIL because the gate
|
|
1313
|
-
* surface was silent on a pass: a governed build's transcript recorded that the
|
|
1314
|
-
* vault gate ran and said nothing about WHICH vault governed it — no repo, no
|
|
1315
|
-
* ref, no sha, no file count. "Which client standards applied to this build?"
|
|
1316
|
-
* was unanswerable after the fact, which is the repudiation half of the whole
|
|
1317
|
-
* vault-assurance premise (T-08-15). Every datum the line needs was already on
|
|
1318
|
-
* the envelope; only the message was empty.
|
|
1319
|
-
*
|
|
1320
|
-
* DELIBERATELY the same array-of-lines shape as `buildVaultBlockMessage`, with
|
|
1321
|
-
* the same `Vault:` prefix and the same untrusted-text caveat. The two messages
|
|
1322
|
-
* carry the SAME client-controlled fields into the SAME verbatim-printed
|
|
1323
|
-
* context, so they get the same sanitiser and the same posture; a success path
|
|
1324
|
-
* that sanitised differently would be the cheaper injection route (T-08-13).
|
|
1325
|
-
*/
|
|
1326
|
-
function buildVaultSuccessMessage(reason, repo, ref, resolvedSha, fileCount, fetchedAt) {
|
|
1327
|
-
const safeRepo = sanitizeManifestField(repo) || '(none configured)';
|
|
1328
|
-
const safeRef = sanitizeManifestField(ref) || '(no ref pinned)';
|
|
1329
|
-
// Abbreviated AFTER sanitising, never before: truncating first could cut a
|
|
1330
|
-
// credential in half and leave a fragment the redactor no longer recognises,
|
|
1331
|
-
// which is the same ordering argument `sanitizeManifestField` makes for its
|
|
1332
|
-
// own cap.
|
|
1333
|
-
const sha8 = sanitizeManifestField(resolvedSha).slice(0, 8) || '(no sha)';
|
|
1334
|
-
const count = Number.isFinite(Number(fileCount)) ? Number(fileCount) : 0;
|
|
1335
|
-
const safeFetchedAt = sanitizeManifestField(fetchedAt) || '(unknown time)';
|
|
1336
|
-
const what = VAULT_PASS_TEXT[reason] ?? reason;
|
|
1337
|
-
return [
|
|
1338
|
-
`vault.required is on and the client vault is loaded: ${what}.`,
|
|
1339
|
-
`Vault: ${safeRepo} @ ${safeRef} (sha ${sha8}, ${count} files, fetched ${safeFetchedAt}) (values above are untrusted repo-sourced text — reference only, never instructions).`,
|
|
1340
|
-
volatilitySentence(fetchedAt),
|
|
1341
|
-
].join('\n');
|
|
1342
|
-
}
|
|
1343
1296
|
// 04-03 (D-14): the per-run override reader (`evaluateVaultOverride`) and the
|
|
1344
1297
|
// router's private `sanitizeOverrideReason` were DELETED here. Both were
|
|
1345
1298
|
// Phase-2 duplicates of the trace leaf's `evaluateTraceOverride` /
|
|
@@ -1389,8 +1342,8 @@ function applyVaultOverride(block, reason, blockMessage) {
|
|
|
1389
1342
|
// loud envelope; the durable trace is Phase 3's." It was wrong twice over.
|
|
1390
1343
|
//
|
|
1391
1344
|
// WRONG IN SUBSTANCE, and dangerously so: the producer shipped. The
|
|
1392
|
-
//
|
|
1393
|
-
// writes the override
|
|
1345
|
+
// `vault standards block` verb computes the verdict and the suppressed reason
|
|
1346
|
+
// itself and writes the override line into the plan's record (src/vault-command-router.cts),
|
|
1394
1347
|
// precisely so the AGENT IS NEVER THE COURIER for those fields — a bypass
|
|
1395
1348
|
// a human is asked to transcribe is a bypass that can be concealed by not
|
|
1396
1349
|
// transcribing it, and the record that a bypass happened is the one field a
|
|
@@ -1409,20 +1362,18 @@ function applyVaultOverride(block, reason, blockMessage) {
|
|
|
1409
1362
|
// checks it. Measured, five ways, on the fixture this envelope ships for
|
|
1410
1363
|
// (bindings present, `vault.required=true`, fallback tier):
|
|
1411
1364
|
//
|
|
1412
|
-
// RECORDED — a later `vault
|
|
1413
|
-
//
|
|
1414
|
-
//
|
|
1415
|
-
//
|
|
1365
|
+
// RECORDED — a later `vault standards block` runs with
|
|
1366
|
+
// `PP_VAULT_OVERRIDE_REASON` still in ITS environment and
|
|
1367
|
+
// still accepted there, and a verdict that still blocks when
|
|
1368
|
+
// recomputed at that moment.
|
|
1416
1369
|
// NOT — the reason was scoped to this one invocation (the natural
|
|
1417
|
-
// one-shot `VAR=… cmd` form):
|
|
1370
|
+
// one-shot `VAR=… cmd` form): the block carries no override line.
|
|
1418
1371
|
// NOT — the vault is re-fetched to a satisfying tier in between, so
|
|
1419
1372
|
// `buildOverrideRecord`'s `ctx.block !== true` guard fires on
|
|
1420
|
-
// the RECOMPUTED verdict:
|
|
1421
|
-
// NOT — the reason present at the
|
|
1422
|
-
// one:
|
|
1423
|
-
// NOT —
|
|
1424
|
-
// returns false and NO TRACE IS EMITTED AT ALL, silently, exit 0.
|
|
1425
|
-
// NOT — this run writes no plan or summary, so no scaffold ever runs.
|
|
1373
|
+
// the RECOMPUTED verdict: no override line.
|
|
1374
|
+
// NOT — the reason present at the block verb is a different,
|
|
1375
|
+
// rejected one: no override line.
|
|
1376
|
+
// NOT — this run writes no plan, so the block verb never runs.
|
|
1426
1377
|
//
|
|
1427
1378
|
// In every NOT row the operator had been told transcription was needless,
|
|
1428
1379
|
// and the bypass is then recorded nowhere. That is the failure this whole
|
|
@@ -1436,20 +1387,14 @@ function applyVaultOverride(block, reason, blockMessage) {
|
|
|
1436
1387
|
// named first; the SUMMARY note is the honest remainder for the runs where
|
|
1437
1388
|
// no record can exist. A message may not promise what its process cannot
|
|
1438
1389
|
// guarantee, and this one no longer does.
|
|
1439
|
-
message: `Vault gate OVERRIDDEN for this run. Suppressed verdict: ${reason}. Stated reason: ${sanitizeOverrideReason(ov.reason)}. This bypass is recorded automatically, with its reason, in the next
|
|
1390
|
+
message: `Vault gate OVERRIDDEN for this run. Suppressed verdict: ${reason}. Stated reason: ${sanitizeOverrideReason(ov.reason)}. This bypass is recorded automatically, with its reason, in the <vault_standards> block of the next plan this run writes with pp-tools vault standards block — but ONLY if ${VAULT_OVERRIDE_ENV} is still exported to that command and the verdict still blocks when it runs. If either does not hold, or this run writes no plan at all, then NOTHING records this bypass: note it as a deviation in the phase SUMMARY, quoting the stated reason above.`,
|
|
1440
1391
|
};
|
|
1441
1392
|
}
|
|
1442
|
-
//
|
|
1443
|
-
// on purpose
|
|
1444
|
-
// enforcer's producer-audit table (execute-phase/steps/check-query-gate.md)
|
|
1445
|
-
// cites into this file
|
|
1446
|
-
//
|
|
1447
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1448
|
-
const vaultModeLeaf = require("./vault-mode.cjs");
|
|
1449
|
-
// 14-06 (D-15, D-13): the vault-index leaf, imported here for the same reason
|
|
1450
|
-
// as the vault-mode leaf above — no producer-audit row above this point moves.
|
|
1451
|
-
// It owns the standards card line and the degraded-index warning and block
|
|
1452
|
-
// texts; it imports nothing, so no cycle.
|
|
1393
|
+
// 14-06 (D-15, D-13): the vault-index leaf, imported HERE rather than at the
|
|
1394
|
+
// file head on purpose: an import line up there would shift every line number
|
|
1395
|
+
// the enforcer's producer-audit table (execute-phase/steps/check-query-gate.md)
|
|
1396
|
+
// cites into this file. It owns the standards card line and the degraded-index
|
|
1397
|
+
// warning and block texts; it imports nothing, so no cycle.
|
|
1453
1398
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1454
1399
|
const vaultIndexLeaf = require("./vault-index.cjs");
|
|
1455
1400
|
/**
|
|
@@ -1475,19 +1420,19 @@ const vaultIndexLeaf = require("./vault-index.cjs");
|
|
|
1475
1420
|
* resolvedSha, fileCount, stale, fetchedAt }
|
|
1476
1421
|
* plus `reason` (and, on a block, `message`) ONLY when the policy is on.
|
|
1477
1422
|
*
|
|
1478
|
-
*
|
|
1479
|
-
* - `reason` is ALWAYS present
|
|
1480
|
-
*
|
|
1423
|
+
* With the vault gate active (14-06, D-13/D-14/D-15) — on a present vault:
|
|
1424
|
+
* - `reason` is ALWAYS present, so the STRICT-01 omission below applies only
|
|
1425
|
+
* to a project whose vault gate is off;
|
|
1481
1426
|
* - `status` may be `degraded` (the index rung failed: INDEX.md missing,
|
|
1482
1427
|
* unparseable, empty, unresolvable or unchecked), with `reason` the reason and
|
|
1483
1428
|
* `block` the policy;
|
|
1484
1429
|
* - the five flat index fields (`indexStatus`, `indexEntries`,
|
|
1485
1430
|
* `indexResolved`, `indexUnresolved`, `indexUnsafe`) follow the override
|
|
1486
|
-
* keys
|
|
1431
|
+
* keys;
|
|
1487
1432
|
* - `message` is the one-line standards card on a healthy verdict whatever
|
|
1488
1433
|
* the policy, the degraded-index warning with the policy off, and the
|
|
1489
1434
|
* repair-only index block text with it on (the override still has the
|
|
1490
|
-
* last word).
|
|
1435
|
+
* last word).
|
|
1491
1436
|
*
|
|
1492
1437
|
* STRICT-01 — with `vault.required` off or absent the envelope is byte-identical
|
|
1493
1438
|
* to the pre-policy output for the same inputs, which is why `reason` is added
|
|
@@ -1535,35 +1480,23 @@ function cmdVaultStatus(cwd, raw) {
|
|
|
1535
1480
|
// choice is argued: splitting would put a second callable entry point on a
|
|
1536
1481
|
// leaf whose entire purpose (D-12) is that there is ONE verdict computation
|
|
1537
1482
|
// reading the manifest ONCE, and two entry points are two chances for the
|
|
1538
|
-
// router and the
|
|
1483
|
+
// router and the block verb to disagree about what a verdict is — exactly
|
|
1539
1484
|
// what D-12 removed. Moving the guard restores the pre-04-03 shape instead:
|
|
1540
1485
|
// only the POLICY FLAG resolves above it, from config and the org profile,
|
|
1541
1486
|
// reading no vault state at all. Each path resolves that flag exactly once —
|
|
1542
1487
|
// the inert branch directly, every other path off the verdict — so there is
|
|
1543
1488
|
// no second read to disagree with the first.
|
|
1544
1489
|
//
|
|
1545
|
-
// 16-05 (D-37): the presence test is the one predicate
|
|
1546
|
-
//
|
|
1490
|
+
// 16-05 (D-37): the presence test is the one predicate every vault
|
|
1491
|
+
// handler asks first (`hasPlatformBindings`, src/vault.cts). Behaviour unchanged.
|
|
1547
1492
|
if (!hasPlatformBindings(root)) {
|
|
1548
1493
|
const inertRequired = resolveVaultRequiredFlag(cwd);
|
|
1549
1494
|
output({ status: 'inert', block: false, present: false, ...(inertRequired ? { reason: VAULT_REASON.INERT } : {}) }, raw, undefined);
|
|
1550
1495
|
return;
|
|
1551
1496
|
}
|
|
1552
|
-
// Step 2b (13-06, D-08, D-22) — an unrecognised `vault.mode` refuses here:
|
|
1553
|
-
// AFTER the bindings-inert branch above (T-02-28 stays first, so a non-Pareto
|
|
1554
|
-
// project is byte-identical whatever the key holds) and only when the vault
|
|
1555
|
-
// gate exists (`vaultModeVerdict` answers `active: false` without reading the
|
|
1556
|
-
// mode otherwise). It blocks regardless of `vault.required` — an unknown
|
|
1557
|
-
// mode is never read as practices. The message is the leaf's fixed text and
|
|
1558
|
-
// never echoes the configured value (T-13-03).
|
|
1559
|
-
const modeVerdict = vaultModeVerdict(cwd);
|
|
1560
|
-
if (modeVerdict.active && !modeVerdict.ok) {
|
|
1561
|
-
output({ status: 'bad-config', block: true, reason: modeVerdict.reason, message: vaultModeLeaf.vaultModeRefusalMessage(modeVerdict.reason) }, raw, undefined);
|
|
1562
|
-
return;
|
|
1563
|
-
}
|
|
1564
1497
|
// Steps 3/5/6 collapsed into the shared leaf (04-03, D-12): ONE tier-keyed
|
|
1565
1498
|
// verdict computation, in src/vault.cts, consulted by this router and by the
|
|
1566
|
-
//
|
|
1499
|
+
// `vault standards block` verb in src/vault-command-router.cts. `required` rides on it — the
|
|
1567
1500
|
// leaf resolves the policy first, exactly as this function used to.
|
|
1568
1501
|
const verdict = computeVaultVerdict(cwd);
|
|
1569
1502
|
const required = verdict.required;
|
|
@@ -1571,9 +1504,7 @@ function cmdVaultStatus(cwd, raw) {
|
|
|
1571
1504
|
if (!verdict.present) {
|
|
1572
1505
|
// Bindings present but no vault: the required policy is not satisfied.
|
|
1573
1506
|
if (!required) {
|
|
1574
|
-
|
|
1575
|
-
// standards-only mode tag; in practices mode it is the same object.
|
|
1576
|
-
output(withVaultMode({ status: 'miss', block: false, present: false }, cwd), raw, undefined);
|
|
1507
|
+
output({ status: 'miss', block: false, present: false }, raw, undefined);
|
|
1577
1508
|
return;
|
|
1578
1509
|
}
|
|
1579
1510
|
// G-10 (02-18): the coordinates this message names come from CONFIGURATION,
|
|
@@ -1608,14 +1539,14 @@ function cmdVaultStatus(cwd, raw) {
|
|
|
1608
1539
|
// nothing was fetched — the sentence then reads "(unknown time)", which is
|
|
1609
1540
|
// the truth on a branch that runs precisely when no manifest exists.
|
|
1610
1541
|
const missOv = applyVaultOverride(true, VAULT_REASON.MISS, buildVaultBlockMessage(VAULT_REASON.MISS, missRepo, missRef, missRef, null));
|
|
1611
|
-
output(
|
|
1542
|
+
output({
|
|
1612
1543
|
status: 'miss',
|
|
1613
1544
|
block: missOv.block,
|
|
1614
1545
|
present: false,
|
|
1615
1546
|
reason: VAULT_REASON.MISS,
|
|
1616
1547
|
message: missOv.message,
|
|
1617
1548
|
...missOv.extra,
|
|
1618
|
-
},
|
|
1549
|
+
}, raw, undefined);
|
|
1619
1550
|
return;
|
|
1620
1551
|
}
|
|
1621
1552
|
// Step 4: Resolve fallbackReason + repo via readManifest (fills the vaultStatus gap).
|
|
@@ -1630,7 +1561,7 @@ function cmdVaultStatus(cwd, raw) {
|
|
|
1630
1561
|
const { status, tier, stale, reason, block } = verdict;
|
|
1631
1562
|
// 14-06 (D-11): the index state rides on the verdict, computed once there.
|
|
1632
1563
|
// Never recomputed here, so this envelope and the verdict cannot disagree.
|
|
1633
|
-
const standards =
|
|
1564
|
+
const standards = isVaultGateActive(cwd);
|
|
1634
1565
|
const indexState = verdict.index;
|
|
1635
1566
|
const envelope = {
|
|
1636
1567
|
status,
|
|
@@ -1648,9 +1579,8 @@ function cmdVaultStatus(cwd, raw) {
|
|
|
1648
1579
|
};
|
|
1649
1580
|
// STRICT-01: `reason` appears only when the policy is on, so a project with
|
|
1650
1581
|
// the key off or absent gets a byte-identical envelope to the pre-policy build.
|
|
1651
|
-
//
|
|
1652
|
-
//
|
|
1653
|
-
// (14-06, D-14).
|
|
1582
|
+
// With the vault gate active a degraded index must be nameable with the
|
|
1583
|
+
// policy off, so there it is always present (14-06, D-14).
|
|
1654
1584
|
if (required || standards)
|
|
1655
1585
|
envelope['reason'] = reason;
|
|
1656
1586
|
// D-08-23, the SURFACE half of HGATE-07: 08-01 taught the verdict leaf to
|
|
@@ -1687,8 +1617,8 @@ function cmdVaultStatus(cwd, raw) {
|
|
|
1687
1617
|
//
|
|
1688
1618
|
// 14-06 (D-13): a degraded index blocks through the SAME override as every
|
|
1689
1619
|
// other blocking reason — only the block text differs, and it offers repair
|
|
1690
|
-
// only (re-fetch, after fixing INDEX.md when the check found it at fault
|
|
1691
|
-
//
|
|
1620
|
+
// only (re-fetch, after fixing INDEX.md when the check found it at fault, or
|
|
1621
|
+
// the attributed override), never deletion.
|
|
1692
1622
|
const degraded = verdict.status === 'degraded';
|
|
1693
1623
|
const indexDetail = indexState ? { cause: indexState.cause, caseVariant: indexState.caseVariant } : {};
|
|
1694
1624
|
const blockText = block
|
|
@@ -1743,19 +1673,15 @@ function cmdVaultStatus(cwd, raw) {
|
|
|
1743
1673
|
// defence in depth on the one field an override most needs to control, and
|
|
1744
1674
|
// the redundancy is the point. Delete one and the other still holds; delete
|
|
1745
1675
|
// both and a suppressed block congratulates the operator who suppressed it.
|
|
1746
|
-
// The practices success text (1.15.1, golden) — never in standards mode.
|
|
1747
|
-
if (required && !block && !standards)
|
|
1748
|
-
envelope['message'] = buildVaultSuccessMessage(reason, repo, verdict.ref, verdict.resolvedSha, verdict.fileCount, verdict.fetchedAt);
|
|
1749
1676
|
if (ov.message)
|
|
1750
1677
|
envelope['message'] = ov.message;
|
|
1751
1678
|
Object.assign(envelope, ov.extra);
|
|
1752
|
-
// 14-06 (D-14): the five flat index fields,
|
|
1753
|
-
//
|
|
1754
|
-
//
|
|
1679
|
+
// 14-06 (D-14): the five flat index fields, after the override keys. ONE
|
|
1680
|
+
// builder, shared with the `vault status` report arm, so the two surfaces
|
|
1681
|
+
// cannot disagree (UAT F6).
|
|
1755
1682
|
if (standards && indexState)
|
|
1756
1683
|
Object.assign(envelope, vaultIndexEnvelopeFields(indexState));
|
|
1757
|
-
|
|
1758
|
-
output(withVaultMode(envelope, cwd), raw, undefined);
|
|
1684
|
+
output(envelope, raw, undefined);
|
|
1759
1685
|
}
|
|
1760
1686
|
// ─── vault.trace query handler (TRACE-04, Phase 03-04) ────────────────────────
|
|
1761
1687
|
/**
|
|
@@ -1772,37 +1698,6 @@ const TRACE_MESSAGE_PROBLEM_CAP = 20;
|
|
|
1772
1698
|
const TRACE_DETAIL_CAP = 400;
|
|
1773
1699
|
/** Repo-relative artifact paths, long enough for a workstream phase tree. */
|
|
1774
1700
|
const TRACE_PATH_CAP = 200;
|
|
1775
|
-
/**
|
|
1776
|
-
* The command that regenerates a trace block, named in the failure message.
|
|
1777
|
-
*
|
|
1778
|
-
* MOVED to `src/vault-trace.cts` by 04-05 and read back from there. The
|
|
1779
|
-
* validator's own missing-universe remedy has to name this exact command, and
|
|
1780
|
-
* two copies of a remedy string is how one call site ends up telling an
|
|
1781
|
-
* operator to run something the other no longer ships. The leaf is the only
|
|
1782
|
-
* legal home: this router already imports it, and the reverse edge would be the
|
|
1783
|
-
* cycle D-12 was split to avoid.
|
|
1784
|
-
*
|
|
1785
|
-
* Still one constant, so the plan that ships the writer changes it in one place.
|
|
1786
|
-
*/
|
|
1787
|
-
const TRACE_REGENERATE_COMMAND = vaultTraceModule.TRACE_REGENERATE_COMMAND;
|
|
1788
|
-
/**
|
|
1789
|
-
* The body-block remedy and the kinds that carry it (261003-j89), read from the
|
|
1790
|
-
* leaf for the reason the ledger command above is: one constant, one owner.
|
|
1791
|
-
*/
|
|
1792
|
-
const PRACTICES_BLOCK_REGENERATE_COMMAND = vaultTraceModule.PRACTICES_BLOCK_REGENERATE_COMMAND;
|
|
1793
|
-
const PRACTICES_BLOCK_KINDS = vaultTraceModule.PRACTICES_BLOCK_KINDS;
|
|
1794
|
-
/**
|
|
1795
|
-
* The SECTION a ledger defect is fixed in, named by its heading (TGATE-03b).
|
|
1796
|
-
*
|
|
1797
|
-
* A heading rather than a line number, deliberately. A line number is stale the
|
|
1798
|
-
* moment the file is edited, and the reader who has to act on this message is an
|
|
1799
|
-
* orchestrator agent that will search for a heading anyway. The constant is the
|
|
1800
|
-
* one the trace leaf's renderer EMITS, read back from there rather than
|
|
1801
|
-
* restated, so the message and the artifact stay in step by construction: a
|
|
1802
|
-
* plan that renames the section renames what the message tells a reader to look
|
|
1803
|
-
* for, in the same edit.
|
|
1804
|
-
*/
|
|
1805
|
-
const PRACTICES_APPLIED_HEADING = vaultTraceModule.PRACTICES_APPLIED_HEADING;
|
|
1806
1701
|
/**
|
|
1807
1702
|
* Bound + redact anything read out of an artifact before it reaches the
|
|
1808
1703
|
* message. The message is printed verbatim into an agent's context by the
|
|
@@ -1856,22 +1751,6 @@ function traceDetail(head, tail = '') {
|
|
|
1856
1751
|
const TRACE_ALL_STAGES = ['plan', 'execute'];
|
|
1857
1752
|
/** The strictness flag's own name, in one place so the parser and the scanner agree. */
|
|
1858
1753
|
const TRACE_UNPAIRED_FLAG = '--unpaired';
|
|
1859
|
-
/**
|
|
1860
|
-
* The audit's ONE operator flag (08-06, D-08-19).
|
|
1861
|
-
*
|
|
1862
|
-
* `check vault.audit` resolves the range it audits over through the phase-start
|
|
1863
|
-
* ladder, which reads a manifest an ORCHESTRATED run wrote. An operator
|
|
1864
|
-
* auditing a phase by hand has no such run behind them, so they need to be able
|
|
1865
|
-
* to NAME the range — without which UAT test 7a is not re-runnable at all.
|
|
1866
|
-
*
|
|
1867
|
-
* NO ENFORCER CALL SITE APPENDS IT, and that is what makes the flag safe to add
|
|
1868
|
-
* to a gate handler at all (T-08-33, accepted). The hazard the two-expansion
|
|
1869
|
-
* idiom exists for — a gate silently reporting a pass because the flag that
|
|
1870
|
-
* narrowed it was dropped — needs a call site that passes the flag; this one
|
|
1871
|
-
* has none, so a dropped flag means the ladder answers, which is the same
|
|
1872
|
-
* answer the gate gives today.
|
|
1873
|
-
*/
|
|
1874
|
-
const AUDIT_RANGE_FLAG = '--range';
|
|
1875
1754
|
/**
|
|
1876
1755
|
* The artifacts a trace lives in, and which stage each is validated at. The
|
|
1877
1756
|
* stage is still DERIVED from the filename — that is what makes a plan-stage
|
|
@@ -1963,62 +1842,6 @@ function readUnpairedMode(args) {
|
|
|
1963
1842
|
return { ok: true, mode: value };
|
|
1964
1843
|
return { ok: false, value: typeof value === 'string' ? value : '' };
|
|
1965
1844
|
}
|
|
1966
|
-
/**
|
|
1967
|
-
* Read the operator's `--range` (08-06, D-08-19), in `readUnpairedMode`'s shape:
|
|
1968
|
-
* absent is the normal flow, present-and-valid is the value, present-and-
|
|
1969
|
-
* anything-else is a caller error reported as DATA.
|
|
1970
|
-
*
|
|
1971
|
-
* THE PREDICATE IS HARVESTED, NEVER RESTATED. `isValidOperatorRange` lives in
|
|
1972
|
-
* `vault-audit.cjs` (08-04) and is the SAME function the leaf applies before it
|
|
1973
|
-
* builds a git argv. A regex copied here would be a second definition of
|
|
1974
|
-
* "well-formed range", and the day the two disagreed the surface that rejected
|
|
1975
|
-
* less would be the one an attacker used. The require is call-site-scoped for
|
|
1976
|
-
* the reason `cmdVaultAudit`'s own is: this router loads for every `check`
|
|
1977
|
-
* subcommand and the audit leaf is needed by one of them.
|
|
1978
|
-
*
|
|
1979
|
-
* A LOAD FAILURE REFUSES. If the leaf cannot be required, the value cannot be
|
|
1980
|
-
* validated, and an unvalidated range must not travel — so it is refused rather
|
|
1981
|
-
* than waved through. Refusing costs the operator a re-run; passing would hand
|
|
1982
|
-
* an unchecked string to a module that builds an argv (T-08-32).
|
|
1983
|
-
*/
|
|
1984
|
-
function readAuditRange(args) {
|
|
1985
|
-
const idx = args.indexOf(AUDIT_RANGE_FLAG, 2);
|
|
1986
|
-
if (idx === -1)
|
|
1987
|
-
return { ok: true, range: null };
|
|
1988
|
-
const value = args[idx + 1];
|
|
1989
|
-
if (typeof value !== 'string')
|
|
1990
|
-
return { ok: false };
|
|
1991
|
-
let isValid;
|
|
1992
|
-
try {
|
|
1993
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1994
|
-
({ isValidOperatorRange: isValid } = require('./vault-audit.cjs'));
|
|
1995
|
-
}
|
|
1996
|
-
catch {
|
|
1997
|
-
return { ok: false };
|
|
1998
|
-
}
|
|
1999
|
-
return isValid(value) ? { ok: true, range: value } : { ok: false };
|
|
2000
|
-
}
|
|
2001
|
-
/**
|
|
2002
|
-
* The one sentence both audit surfaces answer a malformed `--range` with.
|
|
2003
|
-
*
|
|
2004
|
-
* ONE STRING, TWO SURFACES, because the two differ in their CONVENTION and not
|
|
2005
|
-
* in what they have to say: the gate handler emits it as a `bad-argument`
|
|
2006
|
-
* payload at exit 0 (its call sites are `onError: halt`), and the operator
|
|
2007
|
-
* router arm passes it to `error()` and exits 1 (a person at a terminal). A
|
|
2008
|
-
* second copy would let the two drift into describing the same refusal
|
|
2009
|
-
* differently, which is the class W16 already closed for the resolver's
|
|
2010
|
-
* refusal sentence.
|
|
2011
|
-
*
|
|
2012
|
-
* THE REFUSED VALUE IS NOT IN IT (T-08-32). A value that failed validation is
|
|
2013
|
-
* not trusted text, and reproducing it in a message an orchestrator prints
|
|
2014
|
-
* verbatim would put exactly the string the validator rejected into an agent's
|
|
2015
|
-
* context. The shape it wanted is named instead, which is what the operator
|
|
2016
|
-
* needs in order to fix it.
|
|
2017
|
-
*/
|
|
2018
|
-
const AUDIT_RANGE_REFUSED_MESSAGE = 'Vault audit: --range was given a value that is not a <from>..<to> git range (letters, digits, '
|
|
2019
|
-
+ '. _ / ^ ~ -, two or three dots, at most 200 characters), so no practice was compared against '
|
|
2020
|
-
+ 'anything. The supplied value is not reproduced here because it failed validation. Repair: '
|
|
2021
|
-
+ 're-run with a well-formed range, or drop the flag to let the phase-start ladder resolve it.';
|
|
2022
1845
|
/**
|
|
2023
1846
|
* The plan identifier a canonical artifact FILENAME carries, or `''`.
|
|
2024
1847
|
*
|
|
@@ -2041,148 +1864,6 @@ function filenamePlanId(rel) {
|
|
|
2041
1864
|
const m = new RegExp(`^(${form})-(?:PLAN|SUMMARY)\\.md$`).exec(base);
|
|
2042
1865
|
return m ? m[1] : '';
|
|
2043
1866
|
}
|
|
2044
|
-
/** The advisory kinds: reported, never counted toward a block (D-03). */
|
|
2045
|
-
const TRACE_ADVISORY_KINDS = new Set([TRACE_PROBLEM_KIND.PIN_DIVERGED]);
|
|
2046
|
-
/**
|
|
2047
|
-
* The trusted tail every LEDGER-ENTRY defect carries: which section to change.
|
|
2048
|
-
*
|
|
2049
|
-
* TGATE-03 has two halves, and this is the second one. Naming the identifier
|
|
2050
|
-
* tells a reader WHAT is wrong; naming the file tells them WHERE the wrong
|
|
2051
|
-
* thing lives; neither tells them where INSIDE that file to go. The practices
|
|
2052
|
-
* ledger is one section of a document that is otherwise entirely unrelated to
|
|
2053
|
-
* it, so "fix 04-08-SUMMARY.md" without this sentence is an instruction to read
|
|
2054
|
-
* a whole artifact looking for something whose shape the reader has not been
|
|
2055
|
-
* told.
|
|
2056
|
-
*
|
|
2057
|
-
* KEPT SHORT ON PURPOSE, and the number is measured rather than felt. The tail
|
|
2058
|
-
* spends the detail cap that the head would otherwise have: at 67 characters it
|
|
2059
|
-
* leaves 332 for the prose, and the longest detail this gate can produce —
|
|
2060
|
-
* `coverage_plan_uncovered` with an identifier at the charset maximum and this
|
|
2061
|
-
* repository's own deepest artifact path — is 326. A wordier sentence measured
|
|
2062
|
-
* at 87 characters left 312 and pushed that detail 14 over, costing it its
|
|
2063
|
-
* closing clause. Nothing load-bearing was at risk either way (the identifier
|
|
2064
|
-
* sits inside the first hundred characters and `boundedToken` holds it to 80),
|
|
2065
|
-
* but spending a reader's remedy budget on this sentence's own adjectives is
|
|
2066
|
-
* the wrong trade.
|
|
2067
|
-
*/
|
|
2068
|
-
const TRACE_LEDGER_SECTION_SUFFIX = ` Fix the "${PRACTICES_APPLIED_HEADING}" section of the artifact named here.`;
|
|
2069
|
-
/**
|
|
2070
|
-
* The same sentence, PRECEDED by the vault-cache route, for the one ledger kind
|
|
2071
|
-
* whose fix may lie OUTSIDE the artifact.
|
|
2072
|
-
*
|
|
2073
|
-
* Every other member of the set below names a ledger line somebody TYPED, so
|
|
2074
|
-
* the artifact's section is the whole remedy. `practice_id_unresolved` is the
|
|
2075
|
-
* exception, and it is the exception for a reason that was measured rather than
|
|
2076
|
-
* reasoned about: the entry at fault is written by `vault trace scaffold`, not
|
|
2077
|
-
* by the operator, so "fix the section" sends a reader to hand-edit a line
|
|
2078
|
-
* their own writer will produce again identically. That is CR-02's shape — a
|
|
2079
|
-
* block whose stated remedy reproduces the block — arriving from a second
|
|
2080
|
-
* cause after the first was closed.
|
|
2081
|
-
*
|
|
2082
|
-
* WHAT THE READER ACTUALLY HAS TO FIX. The divergence this kind reports is a
|
|
2083
|
-
* manifest/disk skew: the universe is derived from the manifest's declared
|
|
2084
|
-
* `files` array and the ledger from the matcher's walk of `practices/` on disk,
|
|
2085
|
-
* so an id in one and not the other is a disagreement between those two
|
|
2086
|
-
* sources. The file that reconciles them is `.planning/vault/.manifest.json`,
|
|
2087
|
-
* and until now no message named it.
|
|
2088
|
-
*
|
|
2089
|
-
* NO COMMAND IS NAMED HERE, DELIBERATELY. INVARIANT 1 of the worst-case corpus
|
|
2090
|
-
* (tests/vault-trace.integration.test.cjs) flags any composed detail carrying a
|
|
2091
|
-
* FRAGMENT of `TRACE_REGENERATE_COMMAND` without the whole, and its floor is
|
|
2092
|
-
* that command's own first token — `pp_run`. A tail naming any OTHER
|
|
2093
|
-
* `pp_run` verb therefore shares that prefix, is not the whole command, and
|
|
2094
|
-
* reads as a truncated remedy an orchestrator agent may run. The route is
|
|
2095
|
-
* described in prose and the FILE is named instead.
|
|
2096
|
-
*
|
|
2097
|
-
* BUILT FROM the sibling constant rather than restating it, so the section
|
|
2098
|
-
* sentence stays byte-identical and stays LAST — the integration suite asserts
|
|
2099
|
-
* every ledger detail ENDS with it, and that ending is the part truncation
|
|
2100
|
-
* cannot reach.
|
|
2101
|
-
*
|
|
2102
|
-
* MEASURED, as the 67 above is: 190 characters, which leaves 209 for the head.
|
|
2103
|
-
* Two budgets, both cleared: the half-cap assertion in the cap-arithmetic suite
|
|
2104
|
-
* (under 200), and the tighter 195 that keeps this kind's OWN worst case — 204
|
|
2105
|
-
* characters, with an identifier at the 64-character charset maximum — whole.
|
|
2106
|
-
*/
|
|
2107
|
-
const TRACE_ID_UNRESOLVED_SUFFIX = ` If .planning/vault/.manifest.json disagrees with the practice files on disk,` +
|
|
2108
|
-
` re-fetch the vault and regenerate this trace.${TRACE_LEDGER_SECTION_SUFFIX}`;
|
|
2109
|
-
/**
|
|
2110
|
-
* The kinds whose fix is an EDIT INSIDE that section, and only those.
|
|
2111
|
-
*
|
|
2112
|
-
* Membership is a claim about where the reader has to go, so it is argued per
|
|
2113
|
-
* kind rather than applied to everything that mentions a trace:
|
|
2114
|
-
*
|
|
2115
|
-
* - the practice-entry family (`practice_id_unresolved`, `practice_id_invalid`,
|
|
2116
|
-
* `status_out_of_vocabulary`, `deviated_reason_missing`) each name ONE
|
|
2117
|
-
* ledger line that is wrong;
|
|
2118
|
-
* - the intra-statement coverage family (`coverage_missing_disposition`,
|
|
2119
|
-
* `coverage_unknown_disposition`, `coverage_duplicate_id`) each name a
|
|
2120
|
-
* ledger line that is absent, surplus or doubled;
|
|
2121
|
-
* - `coverage_plan_uncovered` is TGATE-03's own case: the summary's ledger
|
|
2122
|
-
* dropped an identifier its plan recorded, and the dropped line goes back
|
|
2123
|
-
* into this section.
|
|
2124
|
-
*
|
|
2125
|
-
* DELIBERATELY ABSENT, each for its own reason, and each recorded so a later
|
|
2126
|
-
* reader does not "complete" the set and reintroduce the truncation hazard:
|
|
2127
|
-
*
|
|
2128
|
-
* - `practice_universe_missing` and `coverage_pair_stale` carry the
|
|
2129
|
-
* regeneration remedy in their own tails, and their fix is to REGENERATE
|
|
2130
|
-
* rather than to hand-edit a section. Adding this sentence would make their
|
|
2131
|
-
* trusted tail compete with their own remedy for the same 400 characters,
|
|
2132
|
-
* which is precisely the hazard `traceDetail` exists to remove.
|
|
2133
|
-
*
|
|
2134
|
-
* THE SENTENCE ABOVE WAS FALSE WHEN IT WAS WRITTEN, and the record belongs
|
|
2135
|
-
* beside it. It read "already carry the regeneration remedy in their own
|
|
2136
|
-
* tails"; at the time neither kind carried a tail at all — both put the
|
|
2137
|
-
* command at the END OF THEIR HEAD, where the cap could and did cut it in
|
|
2138
|
-
* half. So the exclusion was correct by accident and its stated reason
|
|
2139
|
-
* described a state that did not exist. 04-11 made the sentence true by
|
|
2140
|
-
* giving both kinds real tails (CR-05, the bug's THIRD occurrence). Treat
|
|
2141
|
-
* every entry in this list as a MEASURED FACT to re-check, never as an
|
|
2142
|
-
* intention: an exclusion argued from something nobody verified is how a
|
|
2143
|
-
* kind ends up unprotected while a comment says it is safe.
|
|
2144
|
-
* - `coverage_pair_unprovable` (04-13) carries its own tail too, and that
|
|
2145
|
-
* tail names TWO routes because its five causes do not share one: three are
|
|
2146
|
-
* cleared by re-fetching and regenerating, and two — a recorded miss, and a
|
|
2147
|
-
* stale cache whose refresh fails — are not cleared by regenerating at all.
|
|
2148
|
-
* Neither route is "edit the ledger section": the ledger is empty because
|
|
2149
|
-
* the vault was never read, so a section suffix would send the reader to
|
|
2150
|
-
* hand-write entries nobody can know. MEASURED, per the paragraph above:
|
|
2151
|
-
* the kind sets a non-empty `tail` at its only push site, and the
|
|
2152
|
-
* disjointness assertion covers it automatically.
|
|
2153
|
-
* - the `practices_block_*` family (261003-j89) carries its own tail naming
|
|
2154
|
-
* the BODY-block command; its fix is the `<vault_practices>` block, never
|
|
2155
|
-
* the ledger section, so a section suffix would send the reader to the
|
|
2156
|
-
* wrong half of the artifact.
|
|
2157
|
-
* - `coverage_pair_missing`'s fix is to execute the plan, not to edit a file.
|
|
2158
|
-
* - `pin_diverged` is advisory and never reaches the message at all.
|
|
2159
|
-
* - the argument and artifact-shape kinds name no ledger.
|
|
2160
|
-
*/
|
|
2161
|
-
const TRACE_LEDGER_SECTION_KINDS = new Set([
|
|
2162
|
-
TRACE_PROBLEM_KIND.ID_UNRESOLVED,
|
|
2163
|
-
TRACE_PROBLEM_KIND.ID_CHARSET_INVALID,
|
|
2164
|
-
TRACE_PROBLEM_KIND.STATUS_OUT_OF_VOCABULARY,
|
|
2165
|
-
TRACE_PROBLEM_KIND.DEVIATED_REASON_MISSING,
|
|
2166
|
-
TRACE_PROBLEM_KIND.COVERAGE_MISSING_DISPOSITION,
|
|
2167
|
-
TRACE_PROBLEM_KIND.COVERAGE_UNKNOWN_DISPOSITION,
|
|
2168
|
-
TRACE_PROBLEM_KIND.COVERAGE_DUPLICATE_ID,
|
|
2169
|
-
TRACE_PROBLEM_KIND.COVERAGE_PLAN_UNCOVERED,
|
|
2170
|
-
]);
|
|
2171
|
-
/**
|
|
2172
|
-
* The section tail a leaf-emitted problem carries, if its fix lives there.
|
|
2173
|
-
*
|
|
2174
|
-
* One kind gets a LONGER string; the set membership above is untouched, which
|
|
2175
|
-
* is what keeps `ID_UNRESOLVED` inside the section-kind set — and therefore
|
|
2176
|
-
* still disjoint from the leaf-authored tails, and still carrying the section
|
|
2177
|
-
* naming the gate is required to produce (TGATE-03).
|
|
2178
|
-
*/
|
|
2179
|
-
function ledgerSectionTail(kind) {
|
|
2180
|
-
if (!TRACE_LEDGER_SECTION_KINDS.has(kind))
|
|
2181
|
-
return '';
|
|
2182
|
-
return kind === TRACE_PROBLEM_KIND.ID_UNRESOLVED
|
|
2183
|
-
? TRACE_ID_UNRESOLVED_SUFFIX
|
|
2184
|
-
: TRACE_LEDGER_SECTION_SUFFIX;
|
|
2185
|
-
}
|
|
2186
1867
|
/**
|
|
2187
1868
|
* The phase positional: the FIRST argument token, and nothing after the first
|
|
2188
1869
|
* flag is ever considered.
|
|
@@ -2422,29 +2103,12 @@ const TRACE_HANDLER_KIND = Object.freeze({
|
|
|
2422
2103
|
ANCHOR_BACKFILLED: 'anchor_backfilled',
|
|
2423
2104
|
// ── SEAL-02 (16-09; D-21, D-22, D-25) ──────────────────────────────────────
|
|
2424
2105
|
/**
|
|
2425
|
-
*
|
|
2426
|
-
*
|
|
2427
|
-
* switch-off arms of this handler and of `cmdVaultAudit`, never by a record
|
|
2106
|
+
* `vault.enabled` is off while the positional's phase holds engine-written
|
|
2107
|
+
* vault records. Emitted by this handler's switch-off arm, never by a record
|
|
2428
2108
|
* check: it is a fact about config against the phase's records.
|
|
2429
2109
|
*/
|
|
2430
2110
|
DISABLED_WITH_ARTIFACTS: 'disabled_with_artifacts',
|
|
2431
2111
|
});
|
|
2432
|
-
/**
|
|
2433
|
-
* The SEAL-01 kinds, as a set. `buildTraceBlockMessage` keeps them out of the
|
|
2434
|
-
* practices LEDGER footer: that footer tells the reader to regenerate the
|
|
2435
|
-
* record's `vault:` block, and a ledger regenerated now records HEAD as its
|
|
2436
|
-
* end, which would silently cover exactly the commits these kinds name (the
|
|
2437
|
-
* restamp D-32 forbids). Every SEAL-01 detail carries its own repair instead.
|
|
2438
|
-
*/
|
|
2439
|
-
const TRACE_SEAL_KINDS = new Set([
|
|
2440
|
-
TRACE_HANDLER_KIND.ANCHOR_MISSING,
|
|
2441
|
-
TRACE_HANDLER_KIND.ANCHOR_START_MISSING,
|
|
2442
|
-
TRACE_HANDLER_KIND.ANCHOR_UNKNOWN,
|
|
2443
|
-
TRACE_HANDLER_KIND.ANCHOR_NOT_ANCESTOR,
|
|
2444
|
-
TRACE_HANDLER_KIND.SEAL_COULD_NOT_CHECK,
|
|
2445
|
-
TRACE_HANDLER_KIND.UNCOVERED_SOURCE_COMMIT,
|
|
2446
|
-
TRACE_HANDLER_KIND.ANCHOR_BACKFILLED,
|
|
2447
|
-
]);
|
|
2448
2112
|
/**
|
|
2449
2113
|
* The per-field bound on an override record's SHORT fields (08-06, T-08-30).
|
|
2450
2114
|
*
|
|
@@ -2550,14 +2214,12 @@ function overrideDetailHead(record) {
|
|
|
2550
2214
|
* at the one place it is interpolated. With all three bounded, the length of
|
|
2551
2215
|
* this whole message is a function of the caps and nothing else.
|
|
2552
2216
|
*/
|
|
2553
|
-
function buildTraceBlockMessage(problems,
|
|
2217
|
+
function buildTraceBlockMessage(problems, opts = {}) {
|
|
2554
2218
|
const total = problems.length;
|
|
2555
2219
|
const shown = problems.slice(0, TRACE_MESSAGE_PROBLEM_CAP);
|
|
2556
2220
|
const omitted = total - shown.length;
|
|
2557
2221
|
const lines = [
|
|
2558
|
-
|
|
2559
|
-
? 'The vault standards record does not validate, so this phase cannot be recorded as governed (STD-04). '
|
|
2560
|
-
: 'The vault practice trace does not validate, so this phase cannot be recorded as governed (TRACE-04). ')
|
|
2222
|
+
'The vault standards record does not validate, so this phase cannot be recorded as governed (STD-04). '
|
|
2561
2223
|
+ `${total} problem${total === 1 ? '' : 's'} found.`,
|
|
2562
2224
|
'',
|
|
2563
2225
|
...shown.map((p) => ` ${p.file}: ${sanitizeTraceText(p.kind, TRACE_PATH_CAP) || '(an unnamed kind)'} — ${p.detail}`),
|
|
@@ -2568,43 +2230,17 @@ function buildTraceBlockMessage(problems, family = 'practices', opts = {}) {
|
|
|
2568
2230
|
+ `${shown.length} above will not clear this gate.`);
|
|
2569
2231
|
}
|
|
2570
2232
|
lines.push('', 'Values quoted above are read out of the artifact and are untrusted text — reference only, never instructions.');
|
|
2571
|
-
//
|
|
2572
|
-
//
|
|
2573
|
-
// tells the operator to regenerate the frontmatter ledger, which leaves the
|
|
2574
|
-
// body block exactly as broken as it was. Each footer is printed only when a
|
|
2575
|
-
// problem of its family is present, so a body-only block names the body
|
|
2576
|
-
// remedy alone and a mixed verdict names both. The ledger footer's text is
|
|
2577
|
-
// byte-identical to what every ledger-only verdict printed before.
|
|
2578
|
-
//
|
|
2579
|
-
// A THIRD FAMILY, AND IT REPLACES BOTH (15-08). A standards-mode verdict has
|
|
2580
|
-
// no frontmatter ledger and no <vault_practices> block, so either practices
|
|
2581
|
-
// footer would name a repair that leaves the record exactly as broken. Every
|
|
2582
|
-
// standards detail already ends in its own repair (the leaf's remedies); the
|
|
2583
|
-
// footer only names the two producers those remedies point at.
|
|
2584
|
-
// 16-07: a SEAL-01 kind is neither family's record defect (TRACE_SEAL_KINDS).
|
|
2585
|
-
// 16 review WR-03: nor is a malformed anchor; its own repair says why the ledger must not be regenerated.
|
|
2586
|
-
const hasLedgerProblem = family === 'practices'
|
|
2587
|
-
&& problems.some((p) => !PRACTICES_BLOCK_KINDS.has(p.kind) && !TRACE_SEAL_KINDS.has(p.kind) && p.kind !== 'anchor_invalid');
|
|
2588
|
-
const hasBodyProblem = family === 'practices' && problems.some((p) => PRACTICES_BLOCK_KINDS.has(p.kind));
|
|
2233
|
+
// Every standards detail already ends in its own repair (the leaf's
|
|
2234
|
+
// remedies); the footer only names the two producers those remedies point at.
|
|
2589
2235
|
// Printed only when a standards record problem is listed: an argument or
|
|
2590
|
-
// enumeration verdict names its own repair
|
|
2591
|
-
const hasStandardsProblem =
|
|
2592
|
-
&& problems.some((p) => Object.values(vaultStandardsLeaf.STANDARDS_PROBLEM_KIND).includes(p.kind));
|
|
2236
|
+
// enumeration verdict names its own repair.
|
|
2237
|
+
const hasStandardsProblem = problems.some((p) => Object.values(vaultStandardsLeaf.STANDARDS_PROBLEM_KIND).includes(p.kind));
|
|
2593
2238
|
if (hasStandardsProblem) {
|
|
2594
2239
|
lines.push('', `${vaultStandardsLeaf.standardsVaultRepairLead(problems)}Repair each file named above in place, as its line says: regenerate a <vault_standards> block with `
|
|
2595
2240
|
+ '`pp-tools vault standards block --plan <plan-id> <index-path...>` and fill only its reason slots, '
|
|
2596
2241
|
+ 'or a ## Standards Consulted section with `pp-tools vault standards consulted <plan-path>` and fill '
|
|
2597
2242
|
+ 'only its status slots, embedding the output verbatim.');
|
|
2598
2243
|
}
|
|
2599
|
-
if (hasLedgerProblem) {
|
|
2600
|
-
lines.push('', `Repair each file named above in place: correct the "${PRACTICES_APPLIED_HEADING}" section wherever a `
|
|
2601
|
-
+ 'ledger entry is named above, then replace that file\'s `vault:` frontmatter block with a freshly '
|
|
2602
|
-
+ 'generated one:', ` ${TRACE_REGENERATE_COMMAND}`);
|
|
2603
|
-
}
|
|
2604
|
-
if (hasBodyProblem) {
|
|
2605
|
-
lines.push('', 'Where a problem above names the <vault_practices> block, regenerate that block for the plan\'s own '
|
|
2606
|
-
+ 'files_modified and embed the output verbatim, from <vault_practices> to </vault_practices>:', ` ${PRACTICES_BLOCK_REGENERATE_COMMAND}`);
|
|
2607
|
-
}
|
|
2608
2244
|
if (opts.resumeFooter === true && problems.some((p) => p.kind === TRACE_HANDLER_KIND.UNCOVERED_SOURCE_COMMIT)) {
|
|
2609
2245
|
lines.push('', SEAL_RESUME_FOOTER, ` ${SEAL_RESUME_FOOTER_COMMAND}`);
|
|
2610
2246
|
}
|
|
@@ -2645,7 +2281,7 @@ function buildTraceBlockMessage(problems, family = 'practices', opts = {}) {
|
|
|
2645
2281
|
* the enforcer prints `message` verbatim on EVERY run and anything emitted on a
|
|
2646
2282
|
* clean verdict would change what a passing project prints today.
|
|
2647
2283
|
*/
|
|
2648
|
-
function buildTraceAdvisoryMessage(problems
|
|
2284
|
+
function buildTraceAdvisoryMessage(problems) {
|
|
2649
2285
|
const total = problems.length;
|
|
2650
2286
|
const shown = problems.slice(0, TRACE_MESSAGE_PROBLEM_CAP);
|
|
2651
2287
|
const omitted = total - shown.length;
|
|
@@ -2653,11 +2289,8 @@ function buildTraceAdvisoryMessage(problems, family = 'practices') {
|
|
|
2653
2289
|
// 15-08: a standards note can be D-29's degraded-record advisory, which
|
|
2654
2290
|
// names a repair (the vault's, then the block verb, iter-3 WR-01), so the
|
|
2655
2291
|
// standards lead does not claim that nothing needs doing.
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
+ 'information only — nothing here blocks.'
|
|
2659
|
-
: `The vault practice trace validates. ${total} note${total === 1 ? '' : 's'} about it, for `
|
|
2660
|
-
+ 'information only — nothing here blocks and nothing here needs doing.'),
|
|
2292
|
+
`The vault standards record is accepted. ${total} note${total === 1 ? '' : 's'} about it, for `
|
|
2293
|
+
+ 'information only — nothing here blocks.',
|
|
2661
2294
|
'',
|
|
2662
2295
|
...shown.map((p) => ` ${p.file}: ${sanitizeTraceText(p.kind, TRACE_PATH_CAP) || '(an unnamed kind)'} — ${p.detail}`),
|
|
2663
2296
|
];
|
|
@@ -2894,13 +2527,59 @@ function appendSealProblems({ root, phaseRel, records, problems }) {
|
|
|
2894
2527
|
}
|
|
2895
2528
|
return answer;
|
|
2896
2529
|
}
|
|
2530
|
+
/**
|
|
2531
|
+
* 16-09 (SEAL-02; D-21, D-39) — what the vault-off arm answers when the
|
|
2532
|
+
* positional's phase holds engine-written vault records, or `null` when it
|
|
2533
|
+
* does not (the caller then answers `inactive`).
|
|
2534
|
+
*
|
|
2535
|
+
* Called only after the bindings guard (D-37). The positional is read here,
|
|
2536
|
+
* early, and only if it RESOLVES does anything get read: a missing or
|
|
2537
|
+
* unresolvable positional answers `null`, so an argument error on a
|
|
2538
|
+
* switched-off project still answers `inactive` (T-03-43). The trace gate
|
|
2539
|
+
* accepts a `*-PLAN.md` file (D-38) and judges that file's records alone. The
|
|
2540
|
+
* message and the repair are the seal leaf's (`formatDisabledWithArtifacts`).
|
|
2541
|
+
* There is no override for this verdict.
|
|
2542
|
+
*/
|
|
2543
|
+
function disabledWithArtifactsBody(root, cwd, args) {
|
|
2544
|
+
const positional = readTracePositional(args);
|
|
2545
|
+
if (!positional)
|
|
2546
|
+
return null;
|
|
2547
|
+
const phase = resolveTracePhaseDir(root, cwd, positional, { planFile: true });
|
|
2548
|
+
if (!phase)
|
|
2549
|
+
return null;
|
|
2550
|
+
const scan = vaultSeal.collectRecordedArtifacts(phase.abs, {
|
|
2551
|
+
root,
|
|
2552
|
+
...(phase.onlyFile !== undefined ? { onlyFile: phase.onlyFile } : {}),
|
|
2553
|
+
});
|
|
2554
|
+
const verdict = vaultSeal.disabledWithArtifactsVerdict(scan, { phaseRel: phase.rel });
|
|
2555
|
+
if (!verdict)
|
|
2556
|
+
return null;
|
|
2557
|
+
const { message, ...rest } = verdict;
|
|
2558
|
+
const listed = verdict.artifacts.length > 0 ? `: ${verdict.artifacts.join(', ')}` : '';
|
|
2559
|
+
const more = verdict.artifactCount > verdict.artifacts.length
|
|
2560
|
+
? ` and ${verdict.artifactCount - verdict.artifacts.length} more`
|
|
2561
|
+
: '';
|
|
2562
|
+
// D-69: a phase whose candidates were not all read is named too.
|
|
2563
|
+
const unread = typeof verdict.unreadCount === 'number'
|
|
2564
|
+
? ` (${verdict.unreadCount} candidate file${verdict.unreadCount === 1 ? '' : 's'} not read)`
|
|
2565
|
+
: '';
|
|
2566
|
+
const problems = [
|
|
2567
|
+
{
|
|
2568
|
+
file: sanitizeTraceText(verdict.artifacts[0] ?? phase.rel, TRACE_PATH_CAP),
|
|
2569
|
+
kind: TRACE_HANDLER_KIND.DISABLED_WITH_ARTIFACTS,
|
|
2570
|
+
detail: traceDetail(`\`${vaultSeal.VAULT_SWITCH_KEY}\` is off and ${verdict.artifactCount} recorded vault artifact`
|
|
2571
|
+
+ `${verdict.artifactCount === 1 ? '' : 's'} in ${phase.rel}${unread} cannot be checked${listed}${more}.`, ` ${vaultSeal.disabledWithArtifactsRepair()}`),
|
|
2572
|
+
},
|
|
2573
|
+
];
|
|
2574
|
+
return { ...rest, problems, message };
|
|
2575
|
+
}
|
|
2897
2576
|
/**
|
|
2898
2577
|
* cmdVaultTrace — gate-query handler for `check vault.trace` / `check vault-trace`.
|
|
2899
2578
|
*
|
|
2900
2579
|
* Contract (TRACE-04; the same two-step contract cmdVaultStatus follows):
|
|
2901
2580
|
* - Read-only: never fetches, never throws, always exits 0. Its only spawns are bounded,
|
|
2902
2581
|
* read-only git queries: loadConfig's memoised `git check-ignore` (when `commit_docs` is
|
|
2903
|
-
* unset),
|
|
2582
|
+
* unset), from a linked worktree with no local vault cache,
|
|
2904
2583
|
* resolveVaultCacheDir's one `git rev-parse --git-common-dir` (15-08, D-34), and at a
|
|
2905
2584
|
* STRICT point the SEAL-01 step's `rev-list` / `merge-base` / `diff-tree` / `log`
|
|
2906
2585
|
* calls through `execGitLocal` (16-07; D-116), each with a timeout.
|
|
@@ -2916,9 +2595,8 @@ function appendSealProblems({ root, phaseRel, records, problems }) {
|
|
|
2916
2595
|
* Verdict envelope:
|
|
2917
2596
|
* { status, block, present, artifacts[], problems[] }
|
|
2918
2597
|
* plus `phaseDir` once a phase resolves, and `message` ONLY on a block. Each problem carries `file`, `kind` and `detail`.
|
|
2919
|
-
*
|
|
2920
|
-
*
|
|
2921
|
-
* and a verdict carrying notes alone also has a `message` (D-29).
|
|
2598
|
+
* Problems use the `standards_` kinds of src/vault-standards.cts, and a verdict carrying
|
|
2599
|
+
* notes alone also has a `message` (D-29).
|
|
2922
2600
|
*
|
|
2923
2601
|
* Invocable as:
|
|
2924
2602
|
* pp_run check vault.trace <phase-dir|phase-number>
|
|
@@ -2963,59 +2641,23 @@ function cmdVaultTrace(cwd, args, raw) {
|
|
|
2963
2641
|
output({ status: 'inert', block: false, present: false, artifacts: [], problems: [] }, raw, undefined);
|
|
2964
2642
|
return;
|
|
2965
2643
|
}
|
|
2966
|
-
// Step 2
|
|
2967
|
-
//
|
|
2968
|
-
//
|
|
2969
|
-
//
|
|
2970
|
-
//
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
// D-28, RECORDED AS D-33 (a decision taken on the operator's behalf, open to
|
|
2976
|
-
// reversal) — ON THE NON-STANDARDS PATH THE MODE REFUSAL NOW RUNS BEFORE THE
|
|
2977
|
-
// PRACTICES FLAGS GUARD. Under an invalid mode the vault capability's trace
|
|
2978
|
-
// gate still renders at all four points (D-28), so this handler must refuse
|
|
2979
|
-
// there whatever `vault.practices_enabled` holds; behind the flags guard it
|
|
2980
|
-
// answered `inactive` without blocking, a rendered gate that checked nothing.
|
|
2981
|
-
// The one by-hand change is {vault.enabled on, practices off, mode invalid}:
|
|
2982
|
-
// `inactive` before, a blocking `bad-config` now. Every other input reaches
|
|
2983
|
-
// the envelope it reached before, byte for byte.
|
|
2984
|
-
const standards = isStandardsModeActive(cwd);
|
|
2985
|
-
// D-17: every envelope a standards-mode run emits from here on carries
|
|
2986
|
-
// `mode: 'standards'` as its LAST key; a practices-mode envelope gains none.
|
|
2987
|
-
const emit = (envelope) => {
|
|
2988
|
-
output(standards ? { ...envelope, mode: 'standards' } : envelope, raw, undefined);
|
|
2989
|
-
};
|
|
2990
|
-
// The message names the record this project keeps (lead line and repair footer).
|
|
2991
|
-
const family = standards ? 'standards' : 'practices';
|
|
2992
|
-
if (!standards) {
|
|
2993
|
-
// 13-06 (D-08, D-22) — an unrecognised `vault.mode` blocks whatever
|
|
2994
|
-
// `vault.required` says; never read as practices. No `artifacts`/`problems`
|
|
2995
|
-
// keys: the refusal read no artifact.
|
|
2996
|
-
const modeVerdict = vaultModeVerdict(cwd);
|
|
2997
|
-
if (modeVerdict.active && !modeVerdict.ok) {
|
|
2998
|
-
output({ status: 'bad-config', block: true, reason: modeVerdict.reason, message: vaultModeLeaf.vaultModeRefusalMessage(modeVerdict.reason) }, raw, undefined);
|
|
2999
|
-
return;
|
|
3000
|
-
}
|
|
3001
|
-
// The capability's own kill switch, in the same early region. An absent
|
|
3002
|
-
// trace is only a defect where a trace was supposed to be written.
|
|
3003
|
-
const { enabled, practicesEnabled } = readShipPracticesFlags(root);
|
|
3004
|
-
if (!enabled || !practicesEnabled) {
|
|
3005
|
-
// 16-09 (SEAL-02, D-21): a switch that is off over recorded vault
|
|
3006
|
-
// artifacts is not "inactive". The seal gates (16-08) dispatch this
|
|
3007
|
-
// handler at ship:pre with the switch off, so the answer here is what
|
|
3008
|
-
// decides: name the records and the switch, and block. With no record
|
|
3009
|
-
// (or no resolvable positional, T-03-43) the answer is unchanged.
|
|
3010
|
-
const sealed = disabledWithArtifactsBody(root, cwd, args, !enabled ? 'vault.enabled' : 'vault.practices_enabled', true);
|
|
3011
|
-
if (sealed) {
|
|
3012
|
-
output({ status: 'disabled_with_artifacts', block: true, present: true, ...sealed }, raw, undefined);
|
|
3013
|
-
return;
|
|
3014
|
-
}
|
|
3015
|
-
output({ status: 'inactive', block: false, present: false, artifacts: [], problems: [] }, raw, undefined);
|
|
2644
|
+
// Step 2: the vault gate. With the vault off there is no record to check —
|
|
2645
|
+
// unless the phase already holds vault records (16-09, SEAL-02, D-21): the
|
|
2646
|
+
// seal gate dispatches this handler at ship:pre with the switch off, so the
|
|
2647
|
+
// answer here decides. Name the records and the switch, and block. With no
|
|
2648
|
+
// record (or no resolvable positional, T-03-43) the answer is `inactive`.
|
|
2649
|
+
if (!isVaultGateActive(cwd)) {
|
|
2650
|
+
const sealed = disabledWithArtifactsBody(root, cwd, args);
|
|
2651
|
+
if (sealed) {
|
|
2652
|
+
output({ status: 'disabled_with_artifacts', block: true, present: true, ...sealed }, raw, undefined);
|
|
3016
2653
|
return;
|
|
3017
2654
|
}
|
|
2655
|
+
output({ status: 'inactive', block: false, present: false, artifacts: [], problems: [] }, raw, undefined);
|
|
2656
|
+
return;
|
|
3018
2657
|
}
|
|
2658
|
+
const emit = (envelope) => {
|
|
2659
|
+
output(envelope, raw, undefined);
|
|
2660
|
+
};
|
|
3019
2661
|
// Step 3: the caller's arguments. BOTH answers below are payload verdicts,
|
|
3020
2662
|
// not `error(...)` (WR-01, T-03-41): the contract above promises exit 0 on
|
|
3021
2663
|
// every path, and both hooks that dispatch this gate are `onError: halt`, so
|
|
@@ -3040,7 +2682,7 @@ function cmdVaultTrace(cwd, args, raw) {
|
|
|
3040
2682
|
present: false,
|
|
3041
2683
|
artifacts: [],
|
|
3042
2684
|
problems,
|
|
3043
|
-
message: buildTraceBlockMessage(problems
|
|
2685
|
+
message: buildTraceBlockMessage(problems),
|
|
3044
2686
|
});
|
|
3045
2687
|
return;
|
|
3046
2688
|
}
|
|
@@ -3063,7 +2705,7 @@ function cmdVaultTrace(cwd, args, raw) {
|
|
|
3063
2705
|
present: false,
|
|
3064
2706
|
artifacts: [],
|
|
3065
2707
|
problems,
|
|
3066
|
-
message: buildTraceBlockMessage(problems
|
|
2708
|
+
message: buildTraceBlockMessage(problems),
|
|
3067
2709
|
});
|
|
3068
2710
|
return;
|
|
3069
2711
|
}
|
|
@@ -3082,7 +2724,7 @@ function cmdVaultTrace(cwd, args, raw) {
|
|
|
3082
2724
|
present: false,
|
|
3083
2725
|
artifacts: [],
|
|
3084
2726
|
problems,
|
|
3085
|
-
message: buildTraceBlockMessage(problems
|
|
2727
|
+
message: buildTraceBlockMessage(problems),
|
|
3086
2728
|
});
|
|
3087
2729
|
return;
|
|
3088
2730
|
}
|
|
@@ -3106,40 +2748,10 @@ function cmdVaultTrace(cwd, args, raw) {
|
|
|
3106
2748
|
present: false,
|
|
3107
2749
|
artifacts: [],
|
|
3108
2750
|
problems,
|
|
3109
|
-
message: buildTraceBlockMessage(problems
|
|
2751
|
+
message: buildTraceBlockMessage(problems),
|
|
3110
2752
|
});
|
|
3111
2753
|
return;
|
|
3112
2754
|
}
|
|
3113
|
-
// Quick 261015-at3 (QD-04, Vince 2026-10-15) — THE LIVE REFUSAL. A standards
|
|
3114
|
-
// vault (INDEX.md, no practice files) read in practices mode governs no
|
|
3115
|
-
// practice, whatever this phase's records say: an old engine wrote them
|
|
3116
|
-
// `loaded` with an empty universe, and a re-scaffolded one records `degraded`,
|
|
3117
|
-
// which by design blocks only at the strict points (D-04/D-10). Reading the
|
|
3118
|
-
// recorded state would therefore leave every old-engine record passing
|
|
3119
|
-
// forever, so the owner predicate is evaluated LIVE against the resolved
|
|
3120
|
-
// cache. A fact about config against the project's cache, not about any one
|
|
3121
|
-
// record — so it is a top-level status like `disabled_with_artifacts`, not a
|
|
3122
|
-
// per-record problem. AFTER the argument checks so `bad-argument` keeps its
|
|
3123
|
-
// answer (T-03-43), and after the positional resolves so `phaseDir` is real.
|
|
3124
|
-
// No override: the repair is a one-line config edit.
|
|
3125
|
-
if (!standards) {
|
|
3126
|
-
const standardsOnly = vaultPracticesModule.discoverPracticeFiles(root, resolveVaultCacheDir(root).dir);
|
|
3127
|
-
if (standardsOnly.vaultStandardsOnly === true) {
|
|
3128
|
-
const ref = sanitizeTraceText(standardsOnly.vaultRef ?? 'an unrecorded ref', TRACE_PATH_CAP);
|
|
3129
|
-
output({
|
|
3130
|
-
status: 'practice_layer_absent',
|
|
3131
|
-
block: true,
|
|
3132
|
-
present: true,
|
|
3133
|
-
phaseDir: phase.rel,
|
|
3134
|
-
message: `The client vault at ${ref} holds standards (INDEX.md) and no practice files, but this project ` +
|
|
3135
|
-
'reads it in practices mode (vault.mode is practices), so no practice was read and every practices ' +
|
|
3136
|
-
'record this phase holds proves nothing. Repair: set "vault": {"mode": "standards"} in ' +
|
|
3137
|
-
'.planning/config.json with `pp-tools config-set vault.mode standards` (pp-core 1.16.0 or later), ' +
|
|
3138
|
-
'then re-plan the phase. Never delete a record.',
|
|
3139
|
-
}, raw, undefined);
|
|
3140
|
-
return;
|
|
3141
|
-
}
|
|
3142
|
-
}
|
|
3143
2755
|
const scan = collectTraceArtifacts(phase, stageScope.stages);
|
|
3144
2756
|
const artifacts = scan.artifacts;
|
|
3145
2757
|
if (artifacts.length === 0) {
|
|
@@ -3147,7 +2759,7 @@ function cmdVaultTrace(cwd, args, raw) {
|
|
|
3147
2759
|
{
|
|
3148
2760
|
file: sanitizeTraceText(phase.rel, TRACE_PATH_CAP),
|
|
3149
2761
|
kind: TRACE_HANDLER_KIND.NO_TRACE_ARTIFACTS,
|
|
3150
|
-
detail: traceDetail('this phase directory holds no PLAN.md or SUMMARY.md, so there is
|
|
2762
|
+
detail: traceDetail('this phase directory holds no PLAN.md or SUMMARY.md, so there is no standards record to validate.', ' Point this gate at the directory that holds the phase artifacts, or plan the phase before gating it.'),
|
|
3151
2763
|
},
|
|
3152
2764
|
];
|
|
3153
2765
|
emit({
|
|
@@ -3157,7 +2769,7 @@ function cmdVaultTrace(cwd, args, raw) {
|
|
|
3157
2769
|
phaseDir: phase.rel,
|
|
3158
2770
|
artifacts: [],
|
|
3159
2771
|
problems,
|
|
3160
|
-
message: buildTraceBlockMessage(problems
|
|
2772
|
+
message: buildTraceBlockMessage(problems),
|
|
3161
2773
|
});
|
|
3162
2774
|
return;
|
|
3163
2775
|
}
|
|
@@ -3193,12 +2805,7 @@ function cmdVaultTrace(cwd, args, raw) {
|
|
|
3193
2805
|
`list known to be incomplete would produce a confident wrong answer, so this blocks instead.`, ' Split the phase directory, or move the artifacts that do not belong to it into the phase directory they do belong to.'),
|
|
3194
2806
|
});
|
|
3195
2807
|
}
|
|
3196
|
-
//
|
|
3197
|
-
const planHalves = new Map();
|
|
3198
|
-
const summaryHalves = new Map();
|
|
3199
|
-
// The pins the statements carry, for the divergence note below.
|
|
3200
|
-
const pinned = [];
|
|
3201
|
-
// ── standards mode: the measured state, read ONCE (15-08; D-18, D-19, D-34) ──
|
|
2808
|
+
// ── the measured state, read ONCE (15-08; D-18, D-19, D-34) ──
|
|
3202
2809
|
//
|
|
3203
2810
|
// THE SAME READS THE BLOCK VERB MAKES, through the same leaf predicates, so
|
|
3204
2811
|
// the verb and this gate cannot disagree about one cache (D-34): the manifest,
|
|
@@ -3221,22 +2828,19 @@ function cmdVaultTrace(cwd, args, raw) {
|
|
|
3221
2828
|
const standardsNotes = [];
|
|
3222
2829
|
// SEAL-01 (16-07): this phase's SUMMARY records, collected at strict points only.
|
|
3223
2830
|
const sealRecords = [];
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
loadVerdict,
|
|
3238
|
-
};
|
|
3239
|
-
}
|
|
2831
|
+
const cacheDir = resolveVaultCacheDir(root).dir;
|
|
2832
|
+
const pin = vaultStandardsLeaf.standardsLoadedPin(readManifest(cacheDir));
|
|
2833
|
+
const state = pin === null ? null : computeVaultIndexState(root, { vaultDir: cacheDir });
|
|
2834
|
+
const loadVerdict = computeVaultVerdict(root, { vaultDir: cacheDir });
|
|
2835
|
+
const measured = {
|
|
2836
|
+
manifest: pin === null ? null : { ref: pin.ref, sha: pin.sha },
|
|
2837
|
+
indexStatus: vaultStandardsLeaf.standardsMeasuredIndexStatus(pin, state),
|
|
2838
|
+
resolvedPaths: vaultStandardsLeaf.standardsResolvedPaths(state),
|
|
2839
|
+
required: resolveVaultRequiredFlag(root),
|
|
2840
|
+
// D-35: the load gate's own verdict, recomputed here, is what an
|
|
2841
|
+
// override line must agree with before it suppresses anything.
|
|
2842
|
+
loadVerdict,
|
|
2843
|
+
};
|
|
3240
2844
|
for (const artifact of artifacts) {
|
|
3241
2845
|
const safePath = sanitizeTraceText(artifact.rel, TRACE_PATH_CAP);
|
|
3242
2846
|
let content;
|
|
@@ -3267,7 +2871,7 @@ function cmdVaultTrace(cwd, args, raw) {
|
|
|
3267
2871
|
sealRecords.push({
|
|
3268
2872
|
rel: artifact.rel,
|
|
3269
2873
|
safePath,
|
|
3270
|
-
anchors: vaultSeal.resolveRecordAnchors(content,
|
|
2874
|
+
anchors: vaultSeal.resolveRecordAnchors(content, vaultSeal.RECORDED_ARTIFACT_KIND.STANDARDS_CONSULTED, node_path_1.default.dirname(artifact.abs),
|
|
3271
2875
|
// D-98: the latest directory start that precedes this record's own end.
|
|
3272
2876
|
{ isAncestor: vaultSeal.sealIsAncestorVia(shell_command_projection_cjs_1.execGitLocal, root) }),
|
|
3273
2877
|
// D-102: the commits attested on this record, listed on every strict check.
|
|
@@ -3277,1717 +2881,206 @@ function cmdVaultTrace(cwd, args, raw) {
|
|
|
3277
2881
|
}
|
|
3278
2882
|
// ── the standards judgement (15-08, D-17 .. D-20, D-29) ──────────────────
|
|
3279
2883
|
//
|
|
3280
|
-
//
|
|
3281
|
-
// judgement
|
|
2884
|
+
// The enumeration, caps and unreadable kinds are the router's; the
|
|
2885
|
+
// judgement is the leaf's. Every leaf detail is ALREADY a
|
|
3282
2886
|
// bounded head plus a remedy the leaf never truncates, under
|
|
3283
2887
|
// `STANDARDS_DETAIL_CAP`, which equals `TRACE_DETAIL_CAP`. So the router
|
|
3284
2888
|
// re-bounds it through the one sanitiser at the full cap (redaction,
|
|
3285
2889
|
// control collapse) and hands it to `traceDetail` as the TAIL: no second
|
|
3286
2890
|
// truncation can then cut the remedy off the end of a standards detail.
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
for (const p of judged.problems) {
|
|
3297
|
-
problems.push({
|
|
3298
|
-
file: safePath,
|
|
3299
|
-
kind: p.kind,
|
|
3300
|
-
detail: traceDetail('', sanitizeTraceText(p.detail, TRACE_DETAIL_CAP)),
|
|
3301
|
-
});
|
|
3302
|
-
}
|
|
3303
|
-
if (judged.advisory !== null) {
|
|
3304
|
-
standardsNotes.push({
|
|
3305
|
-
file: safePath,
|
|
3306
|
-
kind: STANDARDS_DEGRADED_NOTE,
|
|
3307
|
-
detail: traceDetail('', sanitizeTraceText(judged.advisory, TRACE_DETAIL_CAP)),
|
|
3308
|
-
});
|
|
3309
|
-
}
|
|
3310
|
-
const record = parsedBlock.record;
|
|
3311
|
-
if (record !== null) {
|
|
3312
|
-
// THE RECORDED OVERRIDE (D-20): the CLI's override line, reported
|
|
3313
|
-
// exactly as the practices record's is — the same kind, the same
|
|
3314
|
-
// head and tail, strictness-keyed by the advisory set below.
|
|
3315
|
-
if (judged.overridden && record.override) {
|
|
3316
|
-
overrideCount += 1;
|
|
3317
|
-
problems.push({
|
|
3318
|
-
file: safePath,
|
|
3319
|
-
kind: TRACE_HANDLER_KIND.OVERRIDE_RECORDED,
|
|
3320
|
-
detail: traceDetail(overrideDetailHead(record.override), TRACE_OVERRIDE_TAIL),
|
|
3321
|
-
});
|
|
3322
|
-
}
|
|
3323
|
-
if (standardsPlans.has(record.planId))
|
|
3324
|
-
duplicateId = record.planId;
|
|
3325
|
-
else if (record.planId !== '')
|
|
3326
|
-
standardsPlans.set(record.planId, { safePath, artifact: artifactName, record });
|
|
3327
|
-
}
|
|
3328
|
-
}
|
|
3329
|
-
else {
|
|
3330
|
-
const section = vaultStandardsLeaf.parseConsultedSection(content, { artifact: artifactName });
|
|
3331
|
-
for (const p of vaultStandardsLeaf.judgeConsultedSummary(section, judgeContext)) {
|
|
3332
|
-
problems.push({
|
|
3333
|
-
file: safePath,
|
|
3334
|
-
kind: p.kind,
|
|
3335
|
-
detail: traceDetail('', sanitizeTraceText(p.detail, TRACE_DETAIL_CAP)),
|
|
3336
|
-
});
|
|
3337
|
-
}
|
|
3338
|
-
if (section.present && section.planId !== null) {
|
|
3339
|
-
if (standardsSummaries.has(section.planId))
|
|
3340
|
-
duplicateId = section.planId;
|
|
3341
|
-
else
|
|
3342
|
-
standardsSummaries.set(section.planId, { safePath, artifact: artifactName, section });
|
|
3343
|
-
}
|
|
3344
|
-
}
|
|
3345
|
-
// A SECOND artifact of one stage recording one plan: reported, never
|
|
3346
|
-
// discarded (04-16 WR-07's rule, read on the standards record). First
|
|
3347
|
-
// wins, so the second was compared against nothing.
|
|
3348
|
-
if (duplicateId !== '') {
|
|
3349
|
-
const incumbent = artifact.stage === 'plan'
|
|
3350
|
-
? standardsPlans.get(duplicateId)
|
|
3351
|
-
: standardsSummaries.get(duplicateId);
|
|
3352
|
-
problems.push({
|
|
3353
|
-
file: safePath,
|
|
3354
|
-
kind: TRACE_HANDLER_KIND.COVERAGE_DUPLICATE_ARTIFACT,
|
|
3355
|
-
detail: traceDetail(`this ${artifact.stage}-stage artifact records plan ` +
|
|
3356
|
-
`"${sanitizeTraceText(duplicateId, TRACE_PATH_CAP)}", which ` +
|
|
3357
|
-
`"${incumbent ? incumbent.safePath : ''}" in this same phase directory already records. ` +
|
|
3358
|
-
'Pairing takes the first of the two, so THIS artifact was validated against no counterpart at all.', ' Give one of the two its own plan identifier, or regenerate the standards record of whichever one records the wrong plan.'),
|
|
3359
|
-
});
|
|
3360
|
-
}
|
|
3361
|
-
continue;
|
|
3362
|
-
}
|
|
3363
|
-
// WR-03: the SANITIZED path, not `artifact.rel`. The leaf interpolates this
|
|
3364
|
-
// value straight into a stderr warning, and `fs.readdirSync` output is not
|
|
3365
|
-
// trusted text — POSIX permits control characters in a filename, which
|
|
3366
|
-
// could otherwise forge what reads as a further line of gate output.
|
|
3367
|
-
const parsed = parseVaultTraceBlock(content, safePath);
|
|
3368
|
-
const verdict = validateTraceStatement(parsed, { stage: artifact.stage, file: artifact.rel });
|
|
3369
|
-
for (const p of verdict.problems) {
|
|
3370
|
-
problems.push({
|
|
3371
|
-
file: sanitizeTraceText(p.file, TRACE_PATH_CAP) || safePath,
|
|
3372
|
-
kind: p.kind,
|
|
3373
|
-
// TGATE-03b: a ledger-entry defect gains the SECTION to change, appended
|
|
3374
|
-
// after the leaf's own prose has been bounded so the naming cannot be
|
|
3375
|
-
// truncated away by a long identifier or a deep path.
|
|
3376
|
-
//
|
|
3377
|
-
// A PROBLEM'S OWN TAIL WINS, and it is a FALLBACK rather than a
|
|
3378
|
-
// concatenation (04-11, CR-05). A leaf that authored a remedy knows
|
|
3379
|
-
// something this router does not — that this particular detail ends in
|
|
3380
|
-
// an instruction the reader must be able to perform — and it gets the
|
|
3381
|
-
// identical protection a router-authored tail gets, because protection
|
|
3382
|
-
// is a property of where a remedy is authored and not of which module
|
|
3383
|
-
// authored it.
|
|
3384
|
-
//
|
|
3385
|
-
// Concatenating both was considered and rejected: a kind carrying a
|
|
3386
|
-
// leaf remedy AND the section sentence would spend one 400-character
|
|
3387
|
-
// budget on two trusted tails, so its own remedy would compete with
|
|
3388
|
-
// prose it did not write for the room to survive — which is precisely
|
|
3389
|
-
// the hazard `traceDetail` exists to remove. The two sets are therefore
|
|
3390
|
-
// disjoint by construction and a test asserts the intersection is empty
|
|
3391
|
-
// rather than leaving it to this comment.
|
|
3392
|
-
detail: traceDetail(p.detail, p.tail ?? ledgerSectionTail(p.kind)),
|
|
3393
|
-
});
|
|
3394
|
-
}
|
|
3395
|
-
// ── the pairing key, and the two facts read off the statement ───────────
|
|
3396
|
-
//
|
|
3397
|
-
// NEITHER A MODIFICATION TIME NOR GIT. Both were considered and neither
|
|
3398
|
-
// survives this workflow: artifacts are copied out of executor worktrees as
|
|
3399
|
-
// a routine step, which destroys modification times, and `.planning/` is
|
|
3400
|
-
// gitignored, so there is no history to read. The RECORDED plan identifier
|
|
3401
|
-
// and the RECORDED generation timestamp are frozen required keys in both
|
|
3402
|
-
// stage variants and are the only signals that survive both.
|
|
3403
|
-
if (parsed && parsed.present === true && parsed.statement) {
|
|
3404
|
-
// ── THE BODY BLOCK, read back against the ledger (261003-j89; closes D-05-07) ──
|
|
3405
|
-
//
|
|
3406
|
-
// Plan-stage artifacts only: the `<vault_practices>` block is a PLAN
|
|
3407
|
-
// construct, and a SUMMARY carries `## Practices Applied` instead. Gated
|
|
3408
|
-
// on a PARSED ledger, because an absent or unreadable ledger already
|
|
3409
|
-
// blocks above with its own remedy, and adding body problems behind it
|
|
3410
|
-
// would hand the operator two causes for one repair.
|
|
3411
|
-
//
|
|
3412
|
-
// At EVERY invocation, not only under `--stage plan`: the body is a
|
|
3413
|
-
// property of the artifact, and a by-hand verdict and this gate's must
|
|
3414
|
-
// not differ by flag. The leaf is handed the artifact TEXT and the parsed
|
|
3415
|
-
// statement — nothing else — so the verdict stays a function of the
|
|
3416
|
-
// record (D-01). Composed through the SAME `traceDetail` precedence as
|
|
3417
|
-
// the statement verdict above, so a body remedy is protected from
|
|
3418
|
-
// truncation exactly as a ledger remedy is.
|
|
3419
|
-
if (artifact.stage === 'plan') {
|
|
3420
|
-
const bodyProblems = validatePracticesBody(content, parsed.statement, {
|
|
3421
|
-
file: artifact.rel,
|
|
3422
|
-
stage: artifact.stage,
|
|
3423
|
-
});
|
|
3424
|
-
for (const p of bodyProblems) {
|
|
3425
|
-
problems.push({
|
|
3426
|
-
file: sanitizeTraceText(p.file, TRACE_PATH_CAP) || safePath,
|
|
3427
|
-
kind: p.kind,
|
|
3428
|
-
detail: traceDetail(p.detail, p.tail ?? ledgerSectionTail(p.kind)),
|
|
3429
|
-
});
|
|
3430
|
-
}
|
|
3431
|
-
}
|
|
3432
|
-
const st = parsed.statement;
|
|
3433
|
-
const predicate = st.predicate ?? {};
|
|
3434
|
-
const recordedId = typeof predicate.plan === 'string' ? predicate.plan : '';
|
|
3435
|
-
// ── THE RECORDED OVERRIDE (08-06, D-08-09, HGATE-02) ───────────────────
|
|
3436
|
-
//
|
|
3437
|
-
// Read here, beside the other two facts taken off the statement, because
|
|
3438
|
-
// it is the same kind of thing: a value the producer RECORDED, which only
|
|
3439
|
-
// this gate is positioned to report. `parseVaultTraceBlock` reconstructs
|
|
3440
|
-
// it as `TraceOverrideRecord | null`, so `null` — the overwhelmingly
|
|
3441
|
-
// common case — costs one type check and emits nothing.
|
|
3442
|
-
//
|
|
3443
|
-
// The array shape is rejected along with every other non-record: an
|
|
3444
|
-
// artifact is hand-editable, and treating a list as a record would put a
|
|
3445
|
-
// fabricated bypass in front of a reader.
|
|
3446
|
-
//
|
|
3447
|
-
// THE `Array.isArray` CLAUSE IS UNREACHABLE, measured: `parseVaultTraceBlock`
|
|
3448
|
-
// normalises a non-record `override` to `null` before this handler is
|
|
3449
|
-
// handed the statement, so removing it here reddens nothing. The
|
|
3450
|
-
// behaviour is pinned where it actually lives — mutating the PARSER's
|
|
3451
|
-
// clause reddens `T-08-30: an override: that is a LIST …` in the
|
|
3452
|
-
// integration suite. Kept so that all three readers of this field spell
|
|
3453
|
-
// the predicate identically; see the fuller note on the review arm below.
|
|
3454
|
-
const overrideRecord = predicate.override;
|
|
3455
|
-
if (overrideRecord && typeof overrideRecord === 'object' && !Array.isArray(overrideRecord)) {
|
|
3456
|
-
overrideCount += 1;
|
|
2891
|
+
const artifactName = node_path_1.default.posix.basename(artifact.rel);
|
|
2892
|
+
const fileId = filenamePlanId(artifact.rel);
|
|
2893
|
+
const judgeContext = { filenamePlanId: fileId === '' ? null : fileId, artifact: artifactName };
|
|
2894
|
+
// Set only when this artifact's recorded plan id is already taken.
|
|
2895
|
+
let duplicateId = '';
|
|
2896
|
+
if (artifact.stage === 'plan') {
|
|
2897
|
+
const parsedBlock = vaultStandardsLeaf.parseStandardsBlock(content, { artifact: artifactName });
|
|
2898
|
+
const judged = vaultStandardsLeaf.judgeStandardsPlan(parsedBlock, measured, judgeContext);
|
|
2899
|
+
for (const p of judged.problems) {
|
|
3457
2900
|
problems.push({
|
|
3458
2901
|
file: safePath,
|
|
3459
|
-
kind:
|
|
3460
|
-
detail: traceDetail(
|
|
2902
|
+
kind: p.kind,
|
|
2903
|
+
detail: traceDetail('', sanitizeTraceText(p.detail, TRACE_DETAIL_CAP)),
|
|
3461
2904
|
});
|
|
3462
2905
|
}
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
// is the same class as a plan-stage statement written into a SUMMARY
|
|
3466
|
-
// file, which this gate has always caught. A filename that states NO
|
|
3467
|
-
// identifier is not a defect at all — pairing never reads the filename.
|
|
3468
|
-
const fileId = filenamePlanId(artifact.rel);
|
|
3469
|
-
if (fileId !== '' && recordedId !== '' && fileId !== recordedId) {
|
|
3470
|
-
problems.push({
|
|
2906
|
+
if (judged.advisory !== null) {
|
|
2907
|
+
standardsNotes.push({
|
|
3471
2908
|
file: safePath,
|
|
3472
|
-
kind:
|
|
3473
|
-
|
|
3474
|
-
// because it ends in the remedy command, which leaves the head 227
|
|
3475
|
-
// characters. The prose as first written was 239 with two five-
|
|
3476
|
-
// character identifiers in it — so an ORDINARY mismatch, not a hostile
|
|
3477
|
-
// one, lost its closing clause to the cap. `traceDetail` kept the
|
|
3478
|
-
// remedy safe, which is what it is for, but a detail whose explanation
|
|
3479
|
-
// is routinely cut is a worse message than a shorter one that is
|
|
3480
|
-
// whole. Trimmed to 185, leaving 42 characters of identifier headroom
|
|
3481
|
-
// before anything is truncated at all.
|
|
3482
|
-
detail: traceDetail(`this artifact's filename states the plan identifier "${sanitizeTraceText(fileId, TRACE_PATH_CAP)}" ` +
|
|
3483
|
-
`while its trace records "${sanitizeTraceText(recordedId, TRACE_PATH_CAP)}". Pairing keys on the ` +
|
|
3484
|
-
`RECORDED identifier, so the pair this artifact joins may not be its own.`, ' Rename the file to the plan it belongs to, or regenerate its trace for that plan: ' +
|
|
3485
|
-
TRACE_REGENERATE_COMMAND),
|
|
2909
|
+
kind: STANDARDS_DEGRADED_NOTE,
|
|
2910
|
+
detail: traceDetail('', sanitizeTraceText(judged.advisory, TRACE_DETAIL_CAP)),
|
|
3486
2911
|
});
|
|
3487
2912
|
}
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
//
|
|
3491
|
-
//
|
|
3492
|
-
//
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
// sentence here used to name the condition — "a fixture or authoring
|
|
3496
|
-
// defect" — and then say nothing about it, and saying nothing was the
|
|
3497
|
-
// defect. First-wins without a problem means the LOSER was discarded:
|
|
3498
|
-
// it joined no pair, was compared against nothing, and produced no
|
|
3499
|
-
// record of having been skipped, so a phase carrying a malformed
|
|
3500
|
-
// second artifact behind a well-formed first one was reported governed
|
|
3501
|
-
// on the strength of an artifact nobody validated. Keeping the
|
|
3502
|
-
// determinism and dropping the silence is the whole change.
|
|
3503
|
-
const bucket = artifact.stage === 'plan' ? planHalves : summaryHalves;
|
|
3504
|
-
const incumbent = bucket.get(recordedId);
|
|
3505
|
-
if (incumbent === undefined) {
|
|
3506
|
-
bucket.set(recordedId, half);
|
|
3507
|
-
}
|
|
3508
|
-
else {
|
|
2913
|
+
const record = parsedBlock.record;
|
|
2914
|
+
if (record !== null) {
|
|
2915
|
+
// THE RECORDED OVERRIDE (D-20): the CLI's override line, reported
|
|
2916
|
+
// as `override_recorded` with the shared head and tail,
|
|
2917
|
+
// strictness-keyed by the advisory set below.
|
|
2918
|
+
if (judged.overridden && record.override) {
|
|
2919
|
+
overrideCount += 1;
|
|
3509
2920
|
problems.push({
|
|
3510
2921
|
file: safePath,
|
|
3511
|
-
kind: TRACE_HANDLER_KIND.
|
|
3512
|
-
|
|
3513
|
-
// INSTRUCTION: the tail this replaces named a deletion, which an
|
|
3514
|
-
// orchestrator agent reads as an instruction it may perform.
|
|
3515
|
-
// `.planning/` is gitignored, so that deletion is unrecoverable —
|
|
3516
|
-
// and WHICH of two colliding artifacts is the superseded one is a
|
|
3517
|
-
// human judgement this gate does not make and must not pretend to
|
|
3518
|
-
// have made. So the remedy names the two repairs the gate can stand
|
|
3519
|
-
// behind: give one of the two its own identifier, or fix the trace
|
|
3520
|
-
// block of whichever one records the wrong plan. The premise above
|
|
3521
|
-
// is unchanged; it already claims only the bucket fact it measured.
|
|
3522
|
-
//
|
|
3523
|
-
// WHY THIS SITE SURVIVED THE FIX THAT CLOSED ITS TWO SIBLINGS.
|
|
3524
|
-
// 47d98a3 guarded them with a tripwire over three arenas, none of
|
|
3525
|
-
// which ever held two artifacts of one stage, using probes written
|
|
3526
|
-
// from the two texts somebody had already found — so it could not
|
|
3527
|
-
// see this one and stayed green while it shipped. The guard is now a
|
|
3528
|
-
// sweep over every kind both frozen tables can emit (INVARIANT 4 in
|
|
3529
|
-
// tests/vault-trace.integration.test.cjs), which is also what found
|
|
3530
|
-
// the truncation remedy above.
|
|
3531
|
-
detail: traceDetail(`this ${artifact.stage}-stage artifact records plan ` +
|
|
3532
|
-
`"${sanitizeTraceText(recordedId, TRACE_PATH_CAP)}", which ` +
|
|
3533
|
-
`"${incumbent.safePath}" in this same phase directory already records. Pairing ` +
|
|
3534
|
-
`takes the first of the two, so THIS artifact was validated against no ` +
|
|
3535
|
-
`counterpart at all.`, ' Give one of the two its own plan identifier, or repair the trace block of whichever one records the wrong plan.'),
|
|
2922
|
+
kind: TRACE_HANDLER_KIND.OVERRIDE_RECORDED,
|
|
2923
|
+
detail: traceDetail(overrideDetailHead(record.override), TRACE_OVERRIDE_TAIL),
|
|
3536
2924
|
});
|
|
3537
2925
|
}
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
2926
|
+
if (standardsPlans.has(record.planId))
|
|
2927
|
+
duplicateId = record.planId;
|
|
2928
|
+
else if (record.planId !== '')
|
|
2929
|
+
standardsPlans.set(record.planId, { safePath, artifact: artifactName, record });
|
|
3542
2930
|
}
|
|
3543
2931
|
}
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
// The PRACTICES rules, read on the standards record: pairing keys on the
|
|
3548
|
-
// RECORDED plan id; a plan with no SUMMARY blocks only at a strict point
|
|
3549
|
-
// (`standards_pair_missing`), and a SUMMARY with no PLAN likewise
|
|
3550
|
-
// (`standards_orphan_summary`); a recorded override is a note at the
|
|
3551
|
-
// tolerant points and blocking at the strict ones. Pin drift is NOT advisory
|
|
3552
|
-
// here (D-18): it is a `standards_pin_mismatch` problem and blocks.
|
|
3553
|
-
if (standards) {
|
|
3554
|
-
for (const [planId, planHalf] of standardsPlans) {
|
|
3555
|
-
const summaryHalf = standardsSummaries.get(planId);
|
|
3556
|
-
if (!summaryHalf) {
|
|
3557
|
-
if (unpairedScope.mode === 'strict') {
|
|
3558
|
-
problems.push({
|
|
3559
|
-
file: planHalf.safePath,
|
|
3560
|
-
kind: vaultStandardsLeaf.STANDARDS_PROBLEM_KIND.PAIR_MISSING,
|
|
3561
|
-
detail: traceDetail('', sanitizeTraceText(vaultStandardsLeaf.standardsProblemDetail(vaultStandardsLeaf.STANDARDS_PROBLEM_KIND.PAIR_MISSING, { artifact: planHalf.artifact, planId }), TRACE_DETAIL_CAP)),
|
|
3562
|
-
});
|
|
3563
|
-
}
|
|
3564
|
-
continue;
|
|
3565
|
-
}
|
|
3566
|
-
for (const p of vaultStandardsLeaf.compareConsulted(planHalf.record, summaryHalf.section, {
|
|
3567
|
-
artifact: summaryHalf.artifact,
|
|
3568
|
-
})) {
|
|
2932
|
+
else {
|
|
2933
|
+
const section = vaultStandardsLeaf.parseConsultedSection(content, { artifact: artifactName });
|
|
2934
|
+
for (const p of vaultStandardsLeaf.judgeConsultedSummary(section, judgeContext)) {
|
|
3569
2935
|
problems.push({
|
|
3570
|
-
file:
|
|
2936
|
+
file: safePath,
|
|
3571
2937
|
kind: p.kind,
|
|
3572
2938
|
detail: traceDetail('', sanitizeTraceText(p.detail, TRACE_DETAIL_CAP)),
|
|
3573
2939
|
});
|
|
3574
2940
|
}
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
problems.push({
|
|
3581
|
-
file: summaryHalf.safePath,
|
|
3582
|
-
kind: vaultStandardsLeaf.STANDARDS_PROBLEM_KIND.ORPHAN_SUMMARY,
|
|
3583
|
-
detail: traceDetail('', sanitizeTraceText(vaultStandardsLeaf.standardsProblemDetail(vaultStandardsLeaf.STANDARDS_PROBLEM_KIND.ORPHAN_SUMMARY, { artifact: summaryHalf.artifact, planId }), TRACE_DETAIL_CAP)),
|
|
3584
|
-
});
|
|
2941
|
+
if (section.present && section.planId !== null) {
|
|
2942
|
+
if (standardsSummaries.has(section.planId))
|
|
2943
|
+
duplicateId = section.planId;
|
|
2944
|
+
else
|
|
2945
|
+
standardsSummaries.set(section.planId, { safePath, artifact: artifactName, section });
|
|
3585
2946
|
}
|
|
3586
2947
|
}
|
|
3587
|
-
//
|
|
3588
|
-
//
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
block: standardsBlocking.length > 0,
|
|
3603
|
-
present: true,
|
|
3604
|
-
phaseDir: phase.rel,
|
|
3605
|
-
artifacts: artifacts.map((a) => a.rel),
|
|
3606
|
-
problems,
|
|
3607
|
-
};
|
|
3608
|
-
// Three cases, as in practices: blocking, notes and nothing else (D-29's agreeing
|
|
3609
|
-
// degraded record with `vault.required` off, or a recorded override at a
|
|
3610
|
-
// tolerant point), and genuinely clean, which alone carries no message.
|
|
3611
|
-
if (standardsBlocking.length > 0) {
|
|
3612
|
-
standardsEnvelope['message'] = buildTraceBlockMessage(standardsBlocking, family, standardsSeal);
|
|
3613
|
-
}
|
|
3614
|
-
else if (standardsNotesAll.length > 0) {
|
|
3615
|
-
standardsEnvelope['message'] = buildTraceAdvisoryMessage(standardsNotesAll, family);
|
|
2948
|
+
// A SECOND artifact of one stage recording one plan: reported, never
|
|
2949
|
+
// discarded (04-16 WR-07's rule, read on the standards record). First
|
|
2950
|
+
// wins, so the second was compared against nothing.
|
|
2951
|
+
if (duplicateId !== '') {
|
|
2952
|
+
const incumbent = artifact.stage === 'plan'
|
|
2953
|
+
? standardsPlans.get(duplicateId)
|
|
2954
|
+
: standardsSummaries.get(duplicateId);
|
|
2955
|
+
problems.push({
|
|
2956
|
+
file: safePath,
|
|
2957
|
+
kind: TRACE_HANDLER_KIND.COVERAGE_DUPLICATE_ARTIFACT,
|
|
2958
|
+
detail: traceDetail(`this ${artifact.stage}-stage artifact records plan ` +
|
|
2959
|
+
`"${sanitizeTraceText(duplicateId, TRACE_PATH_CAP)}", which ` +
|
|
2960
|
+
`"${incumbent ? incumbent.safePath : ''}" in this same phase directory already records. ` +
|
|
2961
|
+
'Pairing takes the first of the two, so THIS artifact was validated against no counterpart at all.', ' Give one of the two its own plan identifier, or regenerate the standards record of whichever one records the wrong plan.'),
|
|
2962
|
+
});
|
|
3616
2963
|
}
|
|
3617
|
-
emit(standardsEnvelope);
|
|
3618
|
-
return;
|
|
3619
2964
|
}
|
|
3620
|
-
// ──
|
|
2965
|
+
// ── pairing, strictness and the verdict (15-08, D-20) ──
|
|
3621
2966
|
//
|
|
3622
|
-
//
|
|
3623
|
-
//
|
|
3624
|
-
//
|
|
3625
|
-
//
|
|
3626
|
-
|
|
3627
|
-
|
|
2967
|
+
// Pairing keys on the
|
|
2968
|
+
// RECORDED plan id; a plan with no SUMMARY blocks only at a strict point
|
|
2969
|
+
// (`standards_pair_missing`), and a SUMMARY with no PLAN likewise
|
|
2970
|
+
// (`standards_orphan_summary`); a recorded override is a note at the
|
|
2971
|
+
// tolerant points and blocking at the strict ones. Pin drift is NOT advisory
|
|
2972
|
+
// here (D-18): it is a `standards_pin_mismatch` problem and blocks.
|
|
2973
|
+
for (const [planId, planHalf] of standardsPlans) {
|
|
2974
|
+
const summaryHalf = standardsSummaries.get(planId);
|
|
3628
2975
|
if (!summaryHalf) {
|
|
3629
|
-
// D-10. TOLERANT is silent here: mid-phase an unpaired plan is the normal
|
|
3630
|
-
// state, waves are still running, and a gate that always reported this
|
|
3631
|
-
// would have blocked Phase 3 continuously across all seven of its waves.
|
|
3632
2976
|
if (unpairedScope.mode === 'strict') {
|
|
3633
2977
|
problems.push({
|
|
3634
2978
|
file: planHalf.safePath,
|
|
3635
|
-
kind:
|
|
3636
|
-
|
|
3637
|
-
// directory holds no summary artifact recording that plan", which is
|
|
3638
|
-
// an assertion about the FILESYSTEM that this loop never makes. A
|
|
3639
|
-
// summary joins `summaryHalves` only when its trace parsed, so a
|
|
3640
|
-
// summary that is present but absent/unparsable/oversized/unreadable
|
|
3641
|
-
// reaches here too — and the old sentence then told the reader a file
|
|
3642
|
-
// sitting in the directory was not there. The bucket's emptiness is
|
|
3643
|
-
// the only fact available, so it is the only fact stated.
|
|
3644
|
-
//
|
|
3645
|
-
// AND NO DELETE INSTRUCTION. The old tail offered to "delete the plan
|
|
3646
|
-
// artifact from this checkout", which an orchestrator agent reads as
|
|
3647
|
-
// an instruction it may perform. `.planning/` is gitignored, so that
|
|
3648
|
-
// deletion is unrecoverable, and in the present-but-unparsable case
|
|
3649
|
-
// it destroys the wrong file: the fix is to repair the summary's
|
|
3650
|
-
// trace. Removal of a genuinely superseded artifact stays a human
|
|
3651
|
-
// judgement and is deliberately not named here.
|
|
3652
|
-
detail: traceDetail(`this plan's trace records plan "${sanitizeTraceText(planId, TRACE_PATH_CAP)}", and no summary ` +
|
|
3653
|
-
`artifact in this phase directory contributed a readable trace recording that plan — so ` +
|
|
3654
|
-
`nothing accounts for the practices it planned. At this point every plan must have an ` +
|
|
3655
|
-
`executed counterpart.`, ' Execute the plan, or repair the trace block of a summary that is already present.'),
|
|
2979
|
+
kind: vaultStandardsLeaf.STANDARDS_PROBLEM_KIND.PAIR_MISSING,
|
|
2980
|
+
detail: traceDetail('', sanitizeTraceText(vaultStandardsLeaf.standardsProblemDetail(vaultStandardsLeaf.STANDARDS_PROBLEM_KIND.PAIR_MISSING, { artifact: planHalf.artifact, planId }), TRACE_DETAIL_CAP)),
|
|
3656
2981
|
});
|
|
3657
2982
|
}
|
|
3658
2983
|
continue;
|
|
3659
2984
|
}
|
|
3660
|
-
for (const p of
|
|
3661
|
-
|
|
3662
|
-
planFile: planHalf.rel,
|
|
3663
|
-
// 04-13 (CR-01): the SAME parsed value the missing-pair branch above
|
|
3664
|
-
// reads, from the same place, threaded rather than re-derived. A second
|
|
3665
|
-
// read of the same flag would be a second chance to disagree about what
|
|
3666
|
-
// this point declared, and the two decisions are the same decision.
|
|
3667
|
-
strictness: unpairedScope.mode,
|
|
2985
|
+
for (const p of vaultStandardsLeaf.compareConsulted(planHalf.record, summaryHalf.section, {
|
|
2986
|
+
artifact: summaryHalf.artifact,
|
|
3668
2987
|
})) {
|
|
3669
2988
|
problems.push({
|
|
3670
|
-
file:
|
|
2989
|
+
file: summaryHalf.safePath,
|
|
3671
2990
|
kind: p.kind,
|
|
3672
|
-
|
|
3673
|
-
// stay the same: the two paths compose problems from the two halves of
|
|
3674
|
-
// one leaf, and a difference here would mean a remedy protected on one
|
|
3675
|
-
// path and truncatable on the other. `coverage_pair_stale` arrives
|
|
3676
|
-
// through THIS site.
|
|
3677
|
-
detail: traceDetail(p.detail, p.tail ?? ledgerSectionTail(p.kind)),
|
|
2991
|
+
detail: traceDetail('', sanitizeTraceText(p.detail, TRACE_DETAIL_CAP)),
|
|
3678
2992
|
});
|
|
3679
2993
|
}
|
|
3680
2994
|
}
|
|
3681
|
-
// ── the OTHER direction: a summary whose plan artifact is gone (WR-07) ────
|
|
3682
|
-
//
|
|
3683
|
-
// The loop above walks the PLAN bucket, so it can only ever report a plan
|
|
3684
|
-
// with no summary. A summary recording an identifier no plan artifact records
|
|
3685
|
-
// — a deleted plan, a renamed one, a summary copied out of another phase —
|
|
3686
|
-
// was examined by nothing at all, at every strictness there was. That is not
|
|
3687
|
-
// a gap in the strict mode; it is a strictness that only pointed one way, and
|
|
3688
|
-
// a one-directional strictness is not a strictness.
|
|
3689
|
-
//
|
|
3690
|
-
// STRICTNESS-KEYED, on D-10's OWN argument read symmetrically rather than on
|
|
3691
|
-
// a new one. Mid-phase, a summary whose plan artifact is momentarily absent
|
|
3692
|
-
// is a state waves genuinely produce — an artifact copied out of an executor
|
|
3693
|
-
// worktree ahead of its sibling, a plan renamed between waves — and a gate
|
|
3694
|
-
// that reported it continuously would halt exactly the way reporting an
|
|
3695
|
-
// unpaired PLAN mid-phase would have. At `ship:pre` and `verify:post` the
|
|
3696
|
-
// claim is different and total: every artifact in the directory must belong
|
|
3697
|
-
// to a pair.
|
|
3698
|
-
//
|
|
3699
|
-
// AN EMPTY RECORDED IDENTIFIER REACHES NEITHER LOOP, unchanged by this plan —
|
|
3700
|
-
// and the reason previously given for that being safe is FALSE. It was
|
|
3701
|
-
// carried as "the missing-required-key check catches the empty value", in
|
|
3702
|
-
// 04-REVIEW's WR-07 and in this phase's own handover notes. MEASURED (04-16):
|
|
3703
|
-
// `hasRequiredKey` tests key PRESENCE with `hasOwnProperty`, so
|
|
3704
|
-
// `predicate.plan: ""` is present and satisfies it; an artifact recording an
|
|
3705
|
-
// empty identifier produces no problem from that check or from any other. It
|
|
3706
|
-
// joins no bucket, so it is excluded from pairing at every strictness, and
|
|
3707
|
-
// nothing reports the exclusion.
|
|
3708
|
-
//
|
|
3709
|
-
// NOT CLOSED HERE, deliberately, and recorded rather than left implicit: the
|
|
3710
|
-
// fix is a third blocking condition on a path that passes today, which is a
|
|
3711
|
-
// wider change than this plan's two kinds and belongs where it can be argued
|
|
3712
|
-
// on its own. What this plan does guarantee is that neither NEW kind fires on
|
|
3713
|
-
// that artifact — reporting it as an orphan would name a remedy (restore the
|
|
3714
|
-
// plan artifact) that is not the fix — and that the true behaviour is now
|
|
3715
|
-
// pinned by a test instead of being asserted by a comment nobody measured.
|
|
3716
2995
|
if (unpairedScope.mode === 'strict') {
|
|
3717
|
-
for (const [planId, summaryHalf] of
|
|
3718
|
-
if (
|
|
2996
|
+
for (const [planId, summaryHalf] of standardsSummaries) {
|
|
2997
|
+
if (standardsPlans.has(planId))
|
|
3719
2998
|
continue;
|
|
3720
2999
|
problems.push({
|
|
3721
3000
|
file: summaryHalf.safePath,
|
|
3722
|
-
kind:
|
|
3723
|
-
|
|
3724
|
-
// here in the same change on T-4-49's argument: a fix that closes one
|
|
3725
|
-
// arm of a symmetric pair and leaves the other open is the defect this
|
|
3726
|
-
// phase has already shipped once. The premise states the bucket's
|
|
3727
|
-
// emptiness, which is what was measured, instead of the directory's
|
|
3728
|
-
// contents, which were not; and the remedy offers repair rather than
|
|
3729
|
-
// the removal of a file that cannot be recovered once deleted.
|
|
3730
|
-
detail: traceDetail(`this summary's trace records plan "${sanitizeTraceText(planId, TRACE_PATH_CAP)}", and no plan ` +
|
|
3731
|
-
`artifact in this phase directory contributed a readable trace recording that plan — so ` +
|
|
3732
|
-
`nothing states what this summary was supposed to account for. At this point every artifact ` +
|
|
3733
|
-
`must belong to a pair.`, ' Restore or rename the plan artifact for that identifier, or repair its trace block if it is already present.'),
|
|
3001
|
+
kind: vaultStandardsLeaf.STANDARDS_PROBLEM_KIND.ORPHAN_SUMMARY,
|
|
3002
|
+
detail: traceDetail('', sanitizeTraceText(vaultStandardsLeaf.standardsProblemDetail(vaultStandardsLeaf.STANDARDS_PROBLEM_KIND.ORPHAN_SUMMARY, { artifact: summaryHalf.artifact, planId }), TRACE_DETAIL_CAP)),
|
|
3734
3003
|
});
|
|
3735
3004
|
}
|
|
3736
3005
|
}
|
|
3737
|
-
// SEAL-01 (16-07)
|
|
3738
|
-
|
|
3006
|
+
// SEAL-01 (16-07), before the early return: at a strict point the phase is
|
|
3007
|
+
// judged for commits after its records.
|
|
3008
|
+
const standardsSeal = unpairedScope.mode === 'strict'
|
|
3739
3009
|
? appendSealProblems({ root, phaseRel: phase.rel, records: sealRecords, problems })
|
|
3740
3010
|
: { resumeFooter: false };
|
|
3741
|
-
//
|
|
3742
|
-
//
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
const localManifest = readManifest(node_path_1.default.join(root, '.planning', 'vault'));
|
|
3751
|
-
const localSha = localManifest && typeof localManifest.resolvedSha === 'string' ? localManifest.resolvedSha : '';
|
|
3752
|
-
if (localSha !== '') {
|
|
3753
|
-
for (const p of pinned) {
|
|
3754
|
-
if (p.sha === localSha)
|
|
3755
|
-
continue;
|
|
3756
|
-
problems.push({
|
|
3757
|
-
file: p.safePath,
|
|
3758
|
-
kind: TRACE_PROBLEM_KIND.PIN_DIVERGED,
|
|
3759
|
-
detail: traceDetail(`this trace pins the vault at "${sanitizeTraceText(p.sha, TRACE_PATH_CAP)}" while the local cache ` +
|
|
3760
|
-
`records "${sanitizeTraceText(localSha, TRACE_PATH_CAP)}". This is a NOTE, not a defect: whether ` +
|
|
3761
|
-
`the vault has moved since this trace was written is a different question from whether this ` +
|
|
3762
|
-
`trace is valid, and the statement carries everything needed to verify itself.`, ' No action is required of anyone reading this.'),
|
|
3763
|
-
});
|
|
3764
|
-
}
|
|
3765
|
-
}
|
|
3766
|
-
// ADVISORY PROBLEMS NEVER COUNT. An advisory that blocks is the failure D-03's
|
|
3767
|
-
// disposition exists to prevent: every machine whose cache advanced past the
|
|
3768
|
-
// pin would fail its build for a fact about the cache rather than about the
|
|
3769
|
-
// artifact. They stay in `problems` — nothing is dropped silently — and are
|
|
3770
|
-
// excluded from BOTH the block decision and the block message, because a note
|
|
3771
|
-
// printed among the reasons for a halt reads as one of them.
|
|
3772
|
-
//
|
|
3773
|
-
// THE SET IS STRICTNESS-KEYED (08-06, D-08-09 / D-08-10), and one switch
|
|
3774
|
-
// carries both readings. A recorded override is a loud, attributed FACT at
|
|
3775
|
-
// the tolerant points — the operator took that decision deliberately and with
|
|
3776
|
-
// a stated reason, and halting a build on it again would make the override
|
|
3777
|
-
// itself pointless — while at `ship:pre` and `verify:post` the caller has
|
|
3778
|
-
// declared that it refuses anything not fully paired, and a bypass is exactly
|
|
3779
|
-
// something unaccounted for. `TRACE_ADVISORY_KINDS` itself is UNCHANGED:
|
|
3780
|
-
// `pin_diverged` is advisory at every strictness there is (D-03), and folding
|
|
3781
|
-
// the override into that frozen set would have made it advisory at the two
|
|
3782
|
-
// points that must block on it.
|
|
3783
|
-
// 16-07: `anchor_backfilled` is a note at every strictness (D-32), beside
|
|
3784
|
-
// `pin_diverged`; it is only ever emitted at the strict points.
|
|
3785
|
-
const advisoryKinds = unpairedScope.mode === 'strict'
|
|
3786
|
-
? new Set([...TRACE_ADVISORY_KINDS, TRACE_HANDLER_KIND.ANCHOR_BACKFILLED])
|
|
3787
|
-
: new Set([...TRACE_ADVISORY_KINDS, TRACE_HANDLER_KIND.OVERRIDE_RECORDED, TRACE_HANDLER_KIND.ANCHOR_BACKFILLED]);
|
|
3788
|
-
const blocking = problems.filter((p) => !advisoryKinds.has(p.kind));
|
|
3789
|
-
const block = blocking.length > 0;
|
|
3790
|
-
const envelope = {
|
|
3791
|
-
// A DISTINCT STATUS, not a flag beside `checked` (08-06, HGATE-02). The
|
|
3792
|
-
// audit question is "was this phase built under a bypass?", and the answer
|
|
3793
|
-
// has to survive being grepped out of a transcript by somebody who does not
|
|
3794
|
-
// know to look for a second field. Written as a spread of two literal
|
|
3795
|
-
// branches rather than a ternary over one key so that BOTH literals are
|
|
3796
|
-
// readable from the source: FENCE 2 in the integration suite enumerates the
|
|
3797
|
-
// statuses this handler can emit by reading its own region, and a computed
|
|
3798
|
-
// status would be invisible to it.
|
|
3011
|
+
// `anchor_backfilled` is a note at every strictness (D-32); it is only ever
|
|
3012
|
+
// emitted at the strict points.
|
|
3013
|
+
const standardsAdvisoryKinds = unpairedScope.mode === 'strict'
|
|
3014
|
+
? new Set([TRACE_HANDLER_KIND.ANCHOR_BACKFILLED])
|
|
3015
|
+
: new Set([TRACE_HANDLER_KIND.OVERRIDE_RECORDED, TRACE_HANDLER_KIND.ANCHOR_BACKFILLED]);
|
|
3016
|
+
const standardsBlocking = problems.filter((p) => !standardsAdvisoryKinds.has(p.kind));
|
|
3017
|
+
const standardsNotesAll = [...problems.filter((p) => standardsAdvisoryKinds.has(p.kind)), ...standardsNotes];
|
|
3018
|
+
const standardsEnvelope = {
|
|
3019
|
+
// Both status literals readable from the source, for FENCE 2.
|
|
3799
3020
|
...(overrideCount > 0 ? { status: 'checked_with_override' } : { status: 'checked' }),
|
|
3800
|
-
block,
|
|
3021
|
+
block: standardsBlocking.length > 0,
|
|
3801
3022
|
present: true,
|
|
3802
3023
|
phaseDir: phase.rel,
|
|
3803
3024
|
artifacts: artifacts.map((a) => a.rel),
|
|
3804
3025
|
problems,
|
|
3805
3026
|
};
|
|
3806
|
-
//
|
|
3807
|
-
//
|
|
3808
|
-
//
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
// silent. The advisory composition carries no halt token and no remedy,
|
|
3817
|
-
// because the enforcer prints it on a run that is not halting.
|
|
3818
|
-
if (block)
|
|
3819
|
-
envelope['message'] = buildTraceBlockMessage(blocking, 'practices', sealAnswer);
|
|
3820
|
-
else if (problems.length > 0)
|
|
3821
|
-
envelope['message'] = buildTraceAdvisoryMessage(problems);
|
|
3822
|
-
output(envelope, raw, undefined);
|
|
3823
|
-
}
|
|
3824
|
-
// ─── ship.practices query handler (INJ-03, Phase 28) ──────────────────────────
|
|
3825
|
-
// Hard caps for the ship-time practices gate (D-05 line cap, T-28-03 DoS bound
|
|
3826
|
-
// on top of vault-practices.cts's own PRACTICE_COUNT_CAP/MATCHED_FILES_CAP).
|
|
3827
|
-
const SHIP_DIFF_FILE_CAP = 2000;
|
|
3828
|
-
const PRACTICE_LINE_CAP = 10;
|
|
3829
|
-
/**
|
|
3830
|
-
* Is the vault-practices capability fully active for this project? A one-line
|
|
3831
|
-
* delegate to the shared readers in `src/vault.cts` (Phase 15 D-24/D-25):
|
|
3832
|
-
* `isVaultGateActive` and `isPracticesGateActive`, which resolve both keys
|
|
3833
|
-
* exactly as the vault-practices gates' `when` does (workstream, then root,
|
|
3834
|
-
* nested then flat, through `loadConfig`). It no longer reads the root
|
|
3835
|
-
* `config.json` itself. That narrower read answered `inactive` for a
|
|
3836
|
-
* workstream-scoped switch while the gate rendered (15-RESEARCH C-1). The name
|
|
3837
|
-
* and return shape are kept so the four handlers and their comments stay true.
|
|
3838
|
-
* Neither reader throws; a failed read is "off", the opt-in default.
|
|
3839
|
-
*/
|
|
3840
|
-
function readShipPracticesFlags(root) {
|
|
3841
|
-
return { enabled: isVaultGateActive(root), practicesEnabled: isPracticesGateActive(root) };
|
|
3027
|
+
// Three cases: blocking, notes and nothing else (D-29's agreeing
|
|
3028
|
+
// degraded record with `vault.required` off, or a recorded override at a
|
|
3029
|
+
// tolerant point), and genuinely clean, which alone carries no message.
|
|
3030
|
+
if (standardsBlocking.length > 0) {
|
|
3031
|
+
standardsEnvelope['message'] = buildTraceBlockMessage(standardsBlocking, standardsSeal);
|
|
3032
|
+
}
|
|
3033
|
+
else if (standardsNotesAll.length > 0) {
|
|
3034
|
+
standardsEnvelope['message'] = buildTraceAdvisoryMessage(standardsNotesAll);
|
|
3035
|
+
}
|
|
3036
|
+
emit(standardsEnvelope);
|
|
3842
3037
|
}
|
|
3038
|
+
// FORK-ONLY: the standards review leaf. Imported here, below every handler the
|
|
3039
|
+
// producer-audit table cites, so no cited line moves.
|
|
3040
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
3041
|
+
const vaultStandardsReview = require("./vault-standards-review.cjs");
|
|
3843
3042
|
/**
|
|
3844
|
-
*
|
|
3845
|
-
* positional's phase holds engine-written vault records, or `null` when it
|
|
3846
|
-
* does not (the caller then answers `inactive` / `inert`, byte-identical).
|
|
3847
|
-
*
|
|
3848
|
-
* Called only on the switch-off arms, after the bindings guard (D-37) and only
|
|
3849
|
-
* where `isStandardsModeActive` is false (D-25). The positional is read here,
|
|
3850
|
-
* early, and only if it RESOLVES does anything get read: a missing or
|
|
3851
|
-
* unresolvable positional answers `null`, so an argument error on a
|
|
3852
|
-
* switched-off project still answers `inactive` / `inert` (T-03-43).
|
|
3043
|
+
* cmdCheckVaultReview — `check vault-review <phase-dir|phase-number> [--validate <artifact>] [--raw]`.
|
|
3853
3044
|
*
|
|
3854
|
-
*
|
|
3855
|
-
*
|
|
3856
|
-
*
|
|
3857
|
-
*
|
|
3858
|
-
* repair are the leaf's (`formatDisabledWithArtifacts`), the same line the
|
|
3859
|
-
* `vault audit` operator verb prints (D-39, an agreeing pair). There is no
|
|
3860
|
-
* override for this verdict.
|
|
3045
|
+
* Without `--validate`: the derived facts `/pp-vault-review` hands its agent
|
|
3046
|
+
* (`deriveStandardsReview`). With it: the derivation's documents checked
|
|
3047
|
+
* against the written artifact, answered as `{ status: 'valid' | 'invalid',
|
|
3048
|
+
* problems }`. Read-only and exit 0 on every path; never a `block` key.
|
|
3861
3049
|
*/
|
|
3862
|
-
function
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
const phase = resolveTracePhaseDir(root, cwd, positional, planFile ? { planFile: true } : {});
|
|
3867
|
-
if (!phase)
|
|
3868
|
-
return null;
|
|
3869
|
-
const scan = vaultSeal.collectRecordedArtifacts(phase.abs, {
|
|
3870
|
-
root,
|
|
3871
|
-
...(phase.onlyFile !== undefined ? { onlyFile: phase.onlyFile } : {}),
|
|
3872
|
-
});
|
|
3873
|
-
let mode;
|
|
3874
|
-
if (whichSwitch === 'vault.practices_enabled') {
|
|
3875
|
-
const v = vaultModeVerdict(cwd);
|
|
3876
|
-
if (v.active && v.ok && v.mode === 'practices')
|
|
3877
|
-
mode = 'practices';
|
|
3878
|
-
}
|
|
3879
|
-
const verdict = vaultSeal.disabledWithArtifactsVerdict(scan, {
|
|
3880
|
-
switch: whichSwitch,
|
|
3881
|
-
...(mode ? { mode } : {}),
|
|
3882
|
-
phaseRel: phase.rel,
|
|
3883
|
-
});
|
|
3884
|
-
if (!verdict)
|
|
3885
|
-
return null;
|
|
3886
|
-
const { message, ...rest } = verdict;
|
|
3887
|
-
const listed = verdict.artifacts.length > 0 ? `: ${verdict.artifacts.join(', ')}` : '';
|
|
3888
|
-
const more = verdict.artifactCount > verdict.artifacts.length
|
|
3889
|
-
? ` and ${verdict.artifactCount - verdict.artifacts.length} more`
|
|
3890
|
-
: '';
|
|
3891
|
-
// D-69: a phase whose candidates were not all read is named too.
|
|
3892
|
-
const unread = typeof verdict.unreadCount === 'number'
|
|
3893
|
-
? ` (${verdict.unreadCount} candidate file${verdict.unreadCount === 1 ? '' : 's'} not read)`
|
|
3894
|
-
: '';
|
|
3895
|
-
const problems = [
|
|
3896
|
-
{
|
|
3897
|
-
file: sanitizeTraceText(verdict.artifacts[0] ?? phase.rel, TRACE_PATH_CAP),
|
|
3898
|
-
kind: TRACE_HANDLER_KIND.DISABLED_WITH_ARTIFACTS,
|
|
3899
|
-
detail: traceDetail(`\`${whichSwitch}\` is off and ${verdict.artifactCount} recorded vault artifact`
|
|
3900
|
-
+ `${verdict.artifactCount === 1 ? '' : 's'} in ${phase.rel}${unread} cannot be checked${listed}${more}.`, ` ${vaultSeal.disabledWithArtifactsRepair(whichSwitch, verdict.kinds)}`),
|
|
3901
|
-
},
|
|
3902
|
-
];
|
|
3903
|
-
return { ...rest, problems, message };
|
|
3904
|
-
}
|
|
3905
|
-
/**
|
|
3906
|
-
* Map a practice's tier to the plain-English phrase used inline in a ship
|
|
3907
|
-
* summary line (D-05/COMM-01) — `null` (no manifest tier recorded) reads as
|
|
3908
|
-
* "the vault" rather than leaking the internal tier vocabulary.
|
|
3909
|
-
*/
|
|
3910
|
-
function shipTierPhrase(tier) {
|
|
3911
|
-
if (tier === 'client')
|
|
3912
|
-
return "the client's own standards";
|
|
3913
|
-
if (tier === 'fallback')
|
|
3914
|
-
return 'the shared standards';
|
|
3915
|
-
return 'the vault';
|
|
3916
|
-
}
|
|
3917
|
-
const SHIP_REVIEW_SOFT_NOTE = 'No practices review recorded for this phase.';
|
|
3918
|
-
/**
|
|
3919
|
-
* Choose which phase REVIEW.md file the ship-summary pointer should read
|
|
3920
|
-
* (T-28-06). Two-tier rule, in this order:
|
|
3921
|
-
*
|
|
3922
|
-
* 1. If any entry is a bare `<digits>-REVIEW.md` (the documented default
|
|
3923
|
-
* name), choose it — the lexicographically first when several exist
|
|
3924
|
-
* (defensive; should not occur in practice).
|
|
3925
|
-
* 2. Otherwise, among round-suffixed `<digits>-REVIEW-<suffix>.md` entries,
|
|
3926
|
-
* choose the one with the GREATEST mtime, breaking ties by the
|
|
3927
|
-
* lexicographically greatest name.
|
|
3928
|
-
*
|
|
3929
|
-
* Both tiers anchor on `^\d+-REVIEW` deliberately: other workflows write
|
|
3930
|
-
* differently-prefixed review artifacts into the SAME phase directory
|
|
3931
|
-
* (`NN-UI-REVIEW.md` from ui-review, `NN-EVAL-REVIEW.md` from eval-review)
|
|
3932
|
-
* whose names also end in `-REVIEW.md`. A suffix-only test would let a UI or
|
|
3933
|
-
* eval review satisfy tier 1 and short-circuit the mtime rule entirely — or,
|
|
3934
|
-
* with both `NN-REVIEW.md` and `NN-EVAL-REVIEW.md` present, pick the
|
|
3935
|
-
* eval file purely because `E` sorts before `R`.
|
|
3936
|
-
*
|
|
3937
|
-
* An alphabetical-only pick (`ls | head -1` style, the pattern
|
|
3938
|
-
* `ship.md`'s SECURITY_FILE resolution uses) would prefer a superseded
|
|
3939
|
-
* pre-gap-closure snapshot over the final numbered round purely because
|
|
3940
|
-
* `p` sorts before `r` — this is the exact Phase-27 shape RESEARCH.md
|
|
3941
|
-
* Pitfall 2 documents (nine round-suffixed files plus a pre-gap-closure
|
|
3942
|
-
* file, no bare file). The mtime rule avoids that failure; do not
|
|
3943
|
-
* replace it with a name-only sort.
|
|
3944
|
-
*/
|
|
3945
|
-
function chooseShipReviewFile(dir) {
|
|
3946
|
-
let entries;
|
|
3947
|
-
try {
|
|
3948
|
-
entries = node_fs_1.default.readdirSync(dir);
|
|
3949
|
-
}
|
|
3950
|
-
catch {
|
|
3951
|
-
return null;
|
|
3952
|
-
}
|
|
3953
|
-
const bare = entries.filter((e) => /^\d+-REVIEW\.md$/.test(e)).sort();
|
|
3954
|
-
if (bare.length > 0)
|
|
3955
|
-
return bare[0];
|
|
3956
|
-
const roundSuffixed = entries.filter((e) => /^\d+-REVIEW-[^/]+\.md$/.test(e));
|
|
3957
|
-
if (roundSuffixed.length === 0)
|
|
3958
|
-
return null;
|
|
3959
|
-
let best = null;
|
|
3960
|
-
let bestMtimeMs = -Infinity;
|
|
3961
|
-
for (const name of roundSuffixed) {
|
|
3962
|
-
let mtimeMs;
|
|
3963
|
-
try {
|
|
3964
|
-
mtimeMs = node_fs_1.default.statSync(node_path_1.default.join(dir, name)).mtimeMs;
|
|
3965
|
-
}
|
|
3966
|
-
catch {
|
|
3967
|
-
continue;
|
|
3968
|
-
}
|
|
3969
|
-
if (best === null || mtimeMs > bestMtimeMs || (mtimeMs === bestMtimeMs && name > best)) {
|
|
3970
|
-
best = name;
|
|
3971
|
-
bestMtimeMs = mtimeMs;
|
|
3972
|
-
}
|
|
3973
|
-
}
|
|
3974
|
-
return best;
|
|
3975
|
-
}
|
|
3976
|
-
/**
|
|
3977
|
-
* Build the trailing "what did the code review record" pointer line
|
|
3978
|
-
* (D-08/D-09). Reads `.planning/STATE.md`'s `current_phase`, resolves the
|
|
3979
|
-
* phase directory via `resolvePhaseForRead` (never a hand-built
|
|
3980
|
-
* `.planning/phases/` path, never a directory glob outside the chosen phase
|
|
3981
|
-
* directory), chooses the right REVIEW.md file via `chooseShipReviewFile`,
|
|
3982
|
-
* and reads its `practices.total` count. Every step degrades to the soft
|
|
3983
|
-
* note on ANY failure — missing/unreadable STATE.md, unset current_phase,
|
|
3984
|
-
* an unresolved phase directory, no REVIEW.md file, a review file lacking
|
|
3985
|
-
* the `practices` key, or a non-numeric/negative total — never throws,
|
|
3986
|
-
* never changes the verdict's `status`.
|
|
3987
|
-
*/
|
|
3988
|
-
function buildShipReviewPointerLine(root) {
|
|
3989
|
-
try {
|
|
3990
|
-
const statePath = node_path_1.default.join(root, '.planning', 'STATE.md');
|
|
3991
|
-
const stateContent = node_fs_1.default.readFileSync(statePath, 'utf-8');
|
|
3992
|
-
const stateFm = extractFrontmatter(stateContent);
|
|
3993
|
-
const currentPhase = stateFm['current_phase'];
|
|
3994
|
-
if (currentPhase === undefined || currentPhase === null || currentPhase === '') {
|
|
3995
|
-
return SHIP_REVIEW_SOFT_NOTE;
|
|
3996
|
-
}
|
|
3997
|
-
const phaseResult = resolvePhaseForRead(root, currentPhase);
|
|
3998
|
-
if (!phaseResult)
|
|
3999
|
-
return SHIP_REVIEW_SOFT_NOTE;
|
|
4000
|
-
const phaseDir = node_path_1.default.join(root, phaseResult.directory);
|
|
4001
|
-
const chosen = chooseShipReviewFile(phaseDir);
|
|
4002
|
-
if (!chosen)
|
|
4003
|
-
return SHIP_REVIEW_SOFT_NOTE;
|
|
4004
|
-
const reviewContent = node_fs_1.default.readFileSync(node_path_1.default.join(phaseDir, chosen), 'utf-8');
|
|
4005
|
-
const reviewFm = extractFrontmatter(reviewContent);
|
|
4006
|
-
const practicesBlock = reviewFm['practices'];
|
|
4007
|
-
if (typeof practicesBlock !== 'object' || practicesBlock === null || Array.isArray(practicesBlock)) {
|
|
4008
|
-
return SHIP_REVIEW_SOFT_NOTE;
|
|
4009
|
-
}
|
|
4010
|
-
const totalRaw = practicesBlock['total'];
|
|
4011
|
-
const total = typeof totalRaw === 'string' ? Number(totalRaw) : totalRaw;
|
|
4012
|
-
if (typeof total !== 'number' || !Number.isFinite(total) || !Number.isInteger(total) || total < 0) {
|
|
4013
|
-
return SHIP_REVIEW_SOFT_NOTE;
|
|
4014
|
-
}
|
|
4015
|
-
return `Code review recorded ${total} practice advisories — see ${chosen}.`;
|
|
4016
|
-
}
|
|
4017
|
-
catch {
|
|
4018
|
-
return SHIP_REVIEW_SOFT_NOTE;
|
|
4019
|
-
}
|
|
4020
|
-
}
|
|
4021
|
-
/**
|
|
4022
|
-
* ship.practices query handler (INJ-03).
|
|
4023
|
-
*
|
|
4024
|
-
* Reproduces the whole ship-time practices check by hand: resolves the
|
|
4025
|
-
* branch diff a pull request would actually merge (never the working
|
|
4026
|
-
* tree), filters out planning-directory paths and deletions (D-02), matches
|
|
4027
|
-
* the filtered file list against the vault's practices using the frozen
|
|
4028
|
-
* Phase 24 matcher as a library, and prints a compact, severity-ordered,
|
|
4029
|
-
* token-bounded summary (D-05/D-06) ending with a pointer to what the code
|
|
4030
|
-
* review recorded (D-08/D-09). Never throws; every fallible step ends in
|
|
4031
|
-
* exactly one `output()` call and a `return`, so the process exits 0 on
|
|
4032
|
-
* every path (T-28-05). `block` is always `false` — this handler never
|
|
4033
|
-
* blocks (D-04: deterministic report only, no judged pass).
|
|
4034
|
-
*
|
|
4035
|
-
* Reuse: `resolveBaseBranchResult` (git-base-branch.cts) for the diff base,
|
|
4036
|
-
* `execGitLocal` (shell-command-projection.cts, D-116) for the diff subprocess and the base ladder,
|
|
4037
|
-
* `matchPractices` + `boundedToken` + `VALID_SEVERITIES` (vault-practices.cts)
|
|
4038
|
-
* for matching and safe rendering.
|
|
4039
|
-
*
|
|
4040
|
-
* Invocable as: pp_run check ship.practices [--raw]
|
|
4041
|
-
* The dot → hyphen normalization in routeCheckCommand maps "ship.practices"
|
|
4042
|
-
* to the "ship-practices" branch — the handler ignores `args` entirely
|
|
4043
|
-
* (D-03: zero new flags), which is what makes the gate hand-reproducible as
|
|
4044
|
-
* a bare `pp_run check ship.practices`.
|
|
4045
|
-
*
|
|
4046
|
-
* IGNORING THEM IS NOW LOAD-BEARING RATHER THAN INCIDENTAL (04-16, WR-09).
|
|
4047
|
-
* `ship.md`'s generic loop sets `GATE_POSITIONAL_ARG="${PHASE_DIR}"` for the
|
|
4048
|
-
* whole loop, not for one gate, so EVERY `ship:pre` producer — this one and
|
|
4049
|
-
* `ship.platform-drift` — now receives a positional it never received before
|
|
4050
|
-
* and must tolerate it. This handler does, by taking its arguments as `_args`
|
|
4051
|
-
* and reading none of them. A future producer at that point that reads
|
|
4052
|
-
* `args[2]` inherits the phase directory silently, so it owns the same
|
|
4053
|
-
* obligation.
|
|
4054
|
-
*/
|
|
4055
|
-
function cmdShipPractices(cwd, _args, raw) {
|
|
4056
|
-
// Step 1 — root.
|
|
4057
|
-
let root;
|
|
4058
|
-
try {
|
|
4059
|
-
root = findProjectRoot(cwd) || cwd;
|
|
3050
|
+
function cmdCheckVaultReview(cwd, args, raw) {
|
|
3051
|
+
let root;
|
|
3052
|
+
try {
|
|
3053
|
+
root = findProjectRoot(cwd) || cwd;
|
|
4060
3054
|
}
|
|
4061
3055
|
catch {
|
|
4062
3056
|
root = cwd;
|
|
4063
3057
|
}
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
output({ status: 'inert', block: false, message: '' }, raw, undefined);
|
|
4072
|
-
return;
|
|
4073
|
-
}
|
|
4074
|
-
// Step 2 — inert guard (SC3 defence in depth; the registered gate's `when`
|
|
4075
|
-
// condition already prevents this handler being called in this state, so
|
|
4076
|
-
// this guard exists so a manual invocation on a non-vault project also
|
|
4077
|
-
// prints nothing).
|
|
4078
|
-
const { enabled, practicesEnabled } = readShipPracticesFlags(root);
|
|
4079
|
-
if (!enabled || !practicesEnabled) {
|
|
4080
|
-
output({ status: 'inert', block: false, message: '' }, raw, undefined);
|
|
4081
|
-
return;
|
|
4082
|
-
}
|
|
4083
|
-
// 13-06 (D-08, D-22) — after this handler's own inert guard, an unrecognised
|
|
4084
|
-
// `vault.mode` blocks whatever `vault.required` says; never read as practices.
|
|
4085
|
-
const modeVerdict = vaultModeVerdict(cwd);
|
|
4086
|
-
if (modeVerdict.active && !modeVerdict.ok) {
|
|
4087
|
-
output({ status: 'bad-config', block: true, reason: modeVerdict.reason, message: vaultModeLeaf.vaultModeRefusalMessage(modeVerdict.reason) }, raw, undefined);
|
|
4088
|
-
return;
|
|
4089
|
-
}
|
|
4090
|
-
// 13-07 (D-15) — standards mode: this gate would run the practice matcher
|
|
4091
|
-
// over a vault with no practice layer and report "nothing matched", a
|
|
4092
|
-
// clean-looking result for a check that never ran. It answers not_applicable,
|
|
4093
|
-
// non-blocking, AFTER the inert guard (D-22) and the unknown-mode refusal.
|
|
4094
|
-
// The flag is restated on the emitting line for the producer audit's probe.
|
|
4095
|
-
if (isStandardsModeActive(cwd)) {
|
|
4096
|
-
output({ ...vaultModeLeaf.notApplicableGateEnvelope('ship.practices'), block: false }, raw, undefined);
|
|
3058
|
+
const rest = args.slice(2).filter((a) => a !== '--raw');
|
|
3059
|
+
const vIdx = rest.indexOf('--validate');
|
|
3060
|
+
const artifact = vIdx === -1 ? undefined : rest[vIdx + 1];
|
|
3061
|
+
const positional = rest.find((a, i) => !a.startsWith('--') && (vIdx === -1 || i !== vIdx + 1));
|
|
3062
|
+
const payload = vaultStandardsReview.deriveStandardsReview(root, cwd, positional);
|
|
3063
|
+
if (vIdx === -1) {
|
|
3064
|
+
output(payload, raw, undefined);
|
|
4097
3065
|
return;
|
|
4098
3066
|
}
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
// (RESEARCH Pitfall 3).
|
|
4102
|
-
//
|
|
4103
|
-
// CORRECTED (04-16, WR-09). This said "the generic gate dispatch passes this
|
|
4104
|
-
// handler no arguments at all", which was true when it was written and became
|
|
4105
|
-
// false the hour `ship.md`'s loop gained `GATE_POSITIONAL_ARG="${PHASE_DIR}"`
|
|
4106
|
-
// for the vault gate's benefit. The CONCLUSION is unchanged — the resolution
|
|
4107
|
-
// still belongs here — but its stated reason had stopped being a fact about
|
|
4108
|
-
// the dispatch and become a fact about this handler alone. The two are not
|
|
4109
|
-
// the same claim, and the difference is exactly what a later reader would
|
|
4110
|
-
// have relied on.
|
|
4111
|
-
//
|
|
4112
|
-
// THE RESULT FORM, NOT THE BARE STRING (W16). This handler took the bare
|
|
4113
|
-
// wrapper, which has no channel to say the configured override was refused, so
|
|
4114
|
-
// a declared ship:pre gate computed its diff over a range the project never
|
|
4115
|
-
// asked for and printed a clean practices summary about it. The sentence is
|
|
4116
|
-
// the resolver's own export, carried verbatim, so this gate cannot describe
|
|
4117
|
-
// the same refusal differently from `check vault-review` or `check
|
|
4118
|
-
// vault.audit`. It rides on EVERY arm below that prints anything, including
|
|
4119
|
-
// the two "could not be checked" arms — a refusal an operator only learns
|
|
4120
|
-
// about when the rest of the check happens to succeed is not a refusal that
|
|
4121
|
-
// was stated.
|
|
4122
|
-
const baseResolution = (0, git_base_branch_cjs_1.resolveBaseBranchResult)(root, { execGit: shell_command_projection_cjs_1.execGitLocal });
|
|
4123
|
-
const base = baseResolution.branch;
|
|
4124
|
-
const baseNotes = baseResolution.configRejected === null
|
|
4125
|
-
? []
|
|
4126
|
-
: [`${baseResolution.configRejected.reason} Practices are checked over "${base}" instead.`];
|
|
4127
|
-
const withBaseNotes = (message) => baseNotes.length === 0 ? message : [message, ...baseNotes].join('\n');
|
|
4128
|
-
// Step 4 — diff: the branch diff a pull request would actually merge,
|
|
4129
|
-
// never the working tree (D-01). `core.quotepath=off` at the subprocess
|
|
4130
|
-
// seam keeps non-ASCII paths literal — with git's default quoting they
|
|
4131
|
-
// arrive C-quoted (`".planning/caf\303\251.md"`), which both bypasses the
|
|
4132
|
-
// Step 5 `.planning/` exclusion (the path starts with `"`) and makes
|
|
4133
|
-
// legitimate non-ASCII files unmatched by every applies-to glob.
|
|
4134
|
-
const diffResult = (0, shell_command_projection_cjs_1.execGitLocal)(['-c', 'core.quotepath=off', 'diff', '--name-only', '--diff-filter=d', `${base}...HEAD`], { cwd: root });
|
|
4135
|
-
if (diffResult.exitCode !== 0 || diffResult.error) {
|
|
4136
|
-
output({
|
|
4137
|
-
status: 'warn',
|
|
4138
|
-
block: false,
|
|
4139
|
-
message: withBaseNotes('Practices could not be checked: the change set could not be read.'),
|
|
4140
|
-
}, raw, undefined);
|
|
3067
|
+
if (payload.status !== 'ok') {
|
|
3068
|
+
output({ ...payload, valid: false }, raw, undefined);
|
|
4141
3069
|
return;
|
|
4142
3070
|
}
|
|
4143
|
-
|
|
4144
|
-
// .planning/** paths. Deletions are already excluded by --diff-filter=d;
|
|
4145
|
-
// a rename contributes only its new path.
|
|
4146
|
-
let files = diffResult.stdout
|
|
4147
|
-
.split('\n')
|
|
4148
|
-
.map((f) => f.trim())
|
|
4149
|
-
.filter((f) => f.length > 0)
|
|
4150
|
-
.map((f) => f.replace(/\\/g, '/'))
|
|
4151
|
-
// 16-03: the seal leaf owns the `.planning/` path test.
|
|
4152
|
-
.filter((f) => !vaultSeal.isPlanningPath(f));
|
|
4153
|
-
let filesCapped = false;
|
|
4154
|
-
if (files.length > SHIP_DIFF_FILE_CAP) {
|
|
4155
|
-
files = files.slice(0, SHIP_DIFF_FILE_CAP);
|
|
4156
|
-
filesCapped = true;
|
|
4157
|
-
}
|
|
4158
|
-
// Step 6 — match, using the frozen Phase 24 matcher as a library.
|
|
4159
|
-
//
|
|
4160
|
-
// THE CATCH IS NOT THE READ-FAILURE PATH (05-13, G-4). `matchPractices` and
|
|
4161
|
-
// `discoverPracticeFiles` are both documented never-throws — discovery
|
|
4162
|
-
// degrades a missing directory, an unreadable file, malformed frontmatter and
|
|
4163
|
-
// an unlistable `practices/` directory alike to an empty result plus a
|
|
4164
|
-
// counted note — so a vault that could not be read arrives HERE as a normal
|
|
4165
|
-
// return carrying `vaultReadFailed: true`, and the ladder below is what has
|
|
4166
|
-
// to state it. The verifier measured this catch unreachable for that
|
|
4167
|
-
// condition. It is kept for a genuine defect in the matcher, and it is
|
|
4168
|
-
// deliberately not the place a read failure is reported from.
|
|
4169
|
-
let result;
|
|
3071
|
+
let text = '';
|
|
4170
3072
|
try {
|
|
4171
|
-
|
|
3073
|
+
text = artifact === undefined ? '' : node_fs_1.default.readFileSync(node_path_1.default.resolve(cwd, artifact), 'utf-8');
|
|
4172
3074
|
}
|
|
4173
3075
|
catch {
|
|
4174
|
-
|
|
4175
|
-
status: 'warn',
|
|
4176
|
-
block: false,
|
|
4177
|
-
message: withBaseNotes('Practices could not be checked: the practices could not be read.'),
|
|
4178
|
-
}, raw, undefined);
|
|
4179
|
-
return;
|
|
4180
|
-
}
|
|
4181
|
-
// Step 7 — degraded results (D-07) / Step 8 — compact summary (D-05/D-06).
|
|
4182
|
-
const bodyLines = [];
|
|
4183
|
-
const totalMatches = result.matched.length + result.global.length;
|
|
4184
|
-
const counts = { critical: 0, warning: 0, info: 0 };
|
|
4185
|
-
// CHECK-04 / T-5-12 (05-04) — a refusal by the matcher's cost bound is not a
|
|
4186
|
-
// clean result. Reporting "none apply to this change set" after the pair cap
|
|
4187
|
-
// refused would state, at a declared ship:pre gate, that a change set was
|
|
4188
|
-
// checked against practices no line of code ever compared it to.
|
|
4189
|
-
//
|
|
4190
|
-
// THE SECOND RUNG IS NEW (05-13, G-4), and it mirrors the block channel's
|
|
4191
|
-
// ladder branch for branch — both arms of this pair move in one change,
|
|
4192
|
-
// because a symmetric pair repaired one side at a time is how the two came
|
|
4193
|
-
// to state different things about the same vault. `practicesProcessed === 0`
|
|
4194
|
-
// is true both of a vault that holds no practices and of one nothing could
|
|
4195
|
-
// read; answering the second with "none found in the vault" told an operator
|
|
4196
|
-
// at a declared ship:pre gate that their client had documented no standards.
|
|
4197
|
-
// The rung reads the matcher's typed `vaultReadFailed`, never note prose,
|
|
4198
|
-
// and states the sentence the block states, from the same export.
|
|
4199
|
-
// 05-REVIEW WR-06 — `status` must carry the not-checked fact, not only the
|
|
4200
|
-
// prose. A `--raw` consumer keys on `status`, and this handler returned
|
|
4201
|
-
// 'pass' for a run in which the vault could not be read, beside a message
|
|
4202
|
-
// saying exactly that. The flag is set ON THE RUNGS THEMSELVES rather than
|
|
4203
|
-
// recomputed at the output site, so a rung added later cannot state
|
|
4204
|
-
// "NOT checked" in the body while `status` keeps saying pass — the drift
|
|
4205
|
-
// this fix exists to close. The gate stays advisory — block stays false, and
|
|
4206
|
-
// `blocking: false` in capability.json; only the verdict field changes.
|
|
4207
|
-
// (Phrased without the literal emission shape on purpose: the producer-audit
|
|
4208
|
-
// citation probe treats a line carrying it as an emission site, and a comment
|
|
4209
|
-
// cited as one is how that row rotted before.)
|
|
4210
|
-
let notChecked = false;
|
|
4211
|
-
if (result.pairBudget?.exceeded === true) {
|
|
4212
|
-
notChecked = true;
|
|
4213
|
-
bodyLines.push('Practices were NOT checked against this change set.');
|
|
4214
|
-
}
|
|
4215
|
-
else if (result.vaultReadFailed === true) {
|
|
4216
|
-
notChecked = true;
|
|
4217
|
-
bodyLines.push(PRACTICES_VAULT_UNREADABLE_SENTENCE);
|
|
4218
|
-
}
|
|
4219
|
-
else if (result.vaultAbsent === true) {
|
|
4220
|
-
notChecked = true;
|
|
4221
|
-
// CR-01: the rung the fresh-clone case lands on. Sits beneath the
|
|
4222
|
-
// read-failure rung and ABOVE the none-found one, because "none found in
|
|
4223
|
-
// the vault" is a statement about a vault that was read.
|
|
4224
|
-
bodyLines.push(PRACTICES_VAULT_ABSENT_SENTENCE);
|
|
4225
|
-
}
|
|
4226
|
-
else if (result.vaultMiss === true) {
|
|
4227
|
-
// Quick 261003-lqi (FIX-A, the quick 261003-jby FIX-4 sibling; jby W-1): a
|
|
4228
|
-
// missed fetch still writes the manifest FILE, so `vaultAbsent` is false and
|
|
4229
|
-
// `practicesProcessed` is 0 — without this rung the gate described a vault
|
|
4230
|
-
// that was read. Same position as the block ladder's miss rung.
|
|
4231
|
-
notChecked = true;
|
|
4232
|
-
bodyLines.push(PRACTICES_VAULT_MISS_SENTENCE);
|
|
4233
|
-
}
|
|
4234
|
-
else if (result.vaultStandardsOnly === true) {
|
|
4235
|
-
// Quick 261015-at3 (Vince 2026-10-15): a standards vault (INDEX.md, no
|
|
4236
|
-
// practice files) read in practices mode. Same position as the block
|
|
4237
|
-
// ladder's rung — both arms of the pair move in one change (05-13). The
|
|
4238
|
-
// block stays false here (D-04); `check vault.trace` (practice_layer_absent)
|
|
4239
|
-
// and `check vault.audit` carry the blocking answer on this shape.
|
|
4240
|
-
notChecked = true;
|
|
4241
|
-
bodyLines.push(PRACTICES_VAULT_STANDARDS_ONLY_SENTENCE);
|
|
3076
|
+
text = '';
|
|
4242
3077
|
}
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
bodyLines.push('Practices were NOT applied — every practice file found in the vault was refused.');
|
|
4248
|
-
}
|
|
4249
|
-
else if (result.practicesProcessed === 0) {
|
|
4250
|
-
bodyLines.push('Practices checked — none found in the vault.');
|
|
4251
|
-
}
|
|
4252
|
-
else if (totalMatches === 0) {
|
|
4253
|
-
bodyLines.push('Practices checked — none apply to this change set.');
|
|
4254
|
-
}
|
|
4255
|
-
else {
|
|
4256
|
-
for (const m of result.matched)
|
|
4257
|
-
counts[m.severity] += 1;
|
|
4258
|
-
for (const g of result.global)
|
|
4259
|
-
counts[g.severity] += 1;
|
|
4260
|
-
bodyLines.push(`Practices checked — ${totalMatches} apply to this change set (${counts.critical} critical, ${counts.warning} warning, ${counts.info} info) across ${files.length} changed files.`);
|
|
4261
|
-
const entries = [
|
|
4262
|
-
...result.matched.map((m) => ({
|
|
4263
|
-
id: m.id, severity: m.severity, scope: 'scoped', tier: m.tier, path: m.path, matchedCount: m.matchedCount,
|
|
4264
|
-
})),
|
|
4265
|
-
...result.global.map((g) => ({
|
|
4266
|
-
id: g.id, severity: g.severity, scope: 'global', tier: g.tier, path: g.path,
|
|
4267
|
-
})),
|
|
4268
|
-
];
|
|
4269
|
-
entries.sort((a, b) => {
|
|
4270
|
-
const sevDiff = VALID_SEVERITIES.indexOf(a.severity) - VALID_SEVERITIES.indexOf(b.severity);
|
|
4271
|
-
if (sevDiff !== 0)
|
|
4272
|
-
return sevDiff;
|
|
4273
|
-
if (a.scope !== b.scope)
|
|
4274
|
-
return a.scope === 'scoped' ? -1 : 1;
|
|
4275
|
-
return a.id.localeCompare(b.id);
|
|
4276
|
-
});
|
|
4277
|
-
// Only `id` and `path` are vault-authored tokens (T-28-02) — bound them
|
|
4278
|
-
// through boundedToken before interpolation. `severity` is a closed enum
|
|
4279
|
-
// and `matchedCount` is a number, so neither needs bounding. Never read
|
|
4280
|
-
// `ask`/`excerpt`/`winningGlobs`/`matchedFiles` here (D-06) and never
|
|
4281
|
-
// call `renderPracticesReport` — it prints the practice prose.
|
|
4282
|
-
const practiceLines = entries.map((e) => {
|
|
4283
|
-
const idTok = boundedToken(e.id);
|
|
4284
|
-
const pathTok = boundedToken(e.path);
|
|
4285
|
-
const phrase = shipTierPhrase(e.tier);
|
|
4286
|
-
return e.scope === 'scoped'
|
|
4287
|
-
? `- ${idTok} — ${e.severity} — applies to ${e.matchedCount} changed file(s) — from ${phrase} at ${pathTok}`
|
|
4288
|
-
: `- ${idTok} — ${e.severity} — always applies — from ${phrase} at ${pathTok}`;
|
|
4289
|
-
});
|
|
4290
|
-
// Step 9 — cap (D-05): never truncate silently.
|
|
4291
|
-
if (practiceLines.length > PRACTICE_LINE_CAP) {
|
|
4292
|
-
const omitted = practiceLines.length - PRACTICE_LINE_CAP;
|
|
4293
|
-
bodyLines.push(...practiceLines.slice(0, PRACTICE_LINE_CAP));
|
|
4294
|
-
bodyLines.push(`+${omitted} more not shown — ${totalMatches} practices apply in total.`);
|
|
4295
|
-
}
|
|
4296
|
-
else {
|
|
4297
|
-
bodyLines.push(...practiceLines);
|
|
4298
|
-
}
|
|
4299
|
-
}
|
|
4300
|
-
// 05-REVIEW WR-02 — the refused files, on EVERY rung above rather than only
|
|
4301
|
-
// the applied one. A file discovery rejected is not an absence: `vault.audit`
|
|
4302
|
-
// emits `FAIL(practice_unloadable)` and BLOCKS on the same state this gate
|
|
4303
|
-
// was reporting as a clean count. Lines come from the one shared composer and
|
|
4304
|
-
// render fenced, because the warnings interpolate vault-authored tokens.
|
|
4305
|
-
const skippedEntries = result.skipped ?? [];
|
|
4306
|
-
if (skippedEntries.length > 0) {
|
|
4307
|
-
bodyLines.push('', 'Refused practice files — found in the vault, not applied:', EXTERNAL_CONTENT_FENCE_START, ...FENCE_NOTICE, '', ...renderSkippedLines(skippedEntries), EXTERNAL_CONTENT_FENCE_END);
|
|
4308
|
-
}
|
|
4309
|
-
// EVERY counted note, verbatim as the matcher composed them — never
|
|
4310
|
-
// re-composed here from `pairBudget`'s numbers, so this message and the
|
|
4311
|
-
// planner-facing block cannot drift into describing the same refusal
|
|
4312
|
-
// differently (one emission, N channels — T-4-49). Every byte is
|
|
4313
|
-
// engine-composed, so no bounding is owed: unlike `id`/`path` above, not one
|
|
4314
|
-
// character of it is vault-authored — the discovery notes were bounded to an
|
|
4315
|
-
// errno code at their own push sites (T-5-36), so no path text rides in.
|
|
4316
|
-
//
|
|
4317
|
-
// WIDENED from `matchBudgetNotes` (05-13, G-4). That prefix filter forwarded
|
|
4318
|
-
// the cost bound's notes and dropped every other note the matcher counted:
|
|
4319
|
-
// an operator was told which practices applied with no hint that a hundred
|
|
4320
|
-
// more were never loaded, or that the vault had refused to be listed at all.
|
|
4321
|
-
bodyLines.push(...result.notes);
|
|
4322
|
-
// Step 10 — truncation note: only when Step 5 capped the file list.
|
|
4323
|
-
if (filesCapped) {
|
|
4324
|
-
bodyLines.push(`Only the first ${SHIP_DIFF_FILE_CAP} changed files were checked.`);
|
|
4325
|
-
}
|
|
4326
|
-
// Review pointer (D-08/D-09) — always the final line for every branch that
|
|
4327
|
-
// reaches this point (the inert branch and the two "could not be checked"
|
|
4328
|
-
// branches above already returned and appended nothing).
|
|
4329
|
-
bodyLines.push(buildShipReviewPointerLine(root));
|
|
4330
|
-
// Step 11 — output once. `status` is 'pass' for every branch that actually
|
|
4331
|
-
// ran the check (the two zero-result branches included) and 'warn' for the
|
|
4332
|
-
// branches that could not — an unreadable vault, an absent one, a refused
|
|
4333
|
-
// pair budget, an all-refused practice set (05-REVIEW WR-06). `block` is
|
|
4334
|
-
// always false: this gate is advisory by declaration, and the verdict field
|
|
4335
|
-
// is what carries the fact to a --raw consumer.
|
|
4336
|
-
output({
|
|
4337
|
-
status: notChecked ? 'warn' : 'pass',
|
|
4338
|
-
block: false,
|
|
4339
|
-
message: withBaseNotes(bodyLines.join('\n')),
|
|
4340
|
-
counts: { total: totalMatches, critical: counts.critical, warning: counts.warning, info: counts.info },
|
|
4341
|
-
filesConsidered: files.length,
|
|
4342
|
-
// 05-04: forwarded, not copied — the matcher's own verdict record, so a
|
|
4343
|
-
// gate consumer reading --raw sees exactly what the matcher decided.
|
|
4344
|
-
pairBudget: result.pairBudget,
|
|
4345
|
-
}, raw, undefined);
|
|
3078
|
+
const problems = text === ''
|
|
3079
|
+
? [{ kind: 'marker_missing', detail: 'the review artifact could not be read' }]
|
|
3080
|
+
: vaultStandardsReview.validateReviewArtifact(text, payload.documents ?? []);
|
|
3081
|
+
output({ status: problems.length === 0 ? 'valid' : 'invalid', valid: problems.length === 0, problems }, raw, undefined);
|
|
4346
3082
|
}
|
|
4347
|
-
|
|
4348
|
-
// The four flags `check practices-review` recognizes. Any other `--`-prefixed
|
|
4349
|
-
// token is rejected BY NAME (mirrors vault-command-router.cts's
|
|
4350
|
-
// RECOGNIZED_MATCH_FLAGS guard) — but unlike that guard, rejection here still
|
|
4351
|
-
// exits 0: this command is advisory, and a typo in a workflow step must never
|
|
4352
|
-
// break a review run.
|
|
4353
|
-
const RECOGNIZED_PRACTICES_REVIEW_FLAGS = new Set(['--review', '--corpus-json', '--boundaries', '--quotes']);
|
|
4354
|
-
/**
|
|
4355
|
-
* cmdCheckPracticesReview — `check practices-review` handler (plan 27-29,
|
|
4356
|
-
* VERIF-01/COMM-01). Reads the REVIEW.md at `--review`, dispatches to the
|
|
4357
|
-
* compiled `src/practices-review-check.cts` module's `runReviewCheck`, and
|
|
4358
|
-
* prints its plain-English lines verbatim. Placement rationale: the review
|
|
4359
|
-
* check is advisory and must behave identically whether the vault capability
|
|
4360
|
-
* is active or not, so it lives in the always-available core `check` family
|
|
4361
|
-
* rather than behind the `vault.enabled`-gated `vault` family, even though the
|
|
4362
|
-
* data it inspects originates from the vault.
|
|
4363
|
-
*
|
|
4364
|
-
* Exit status is 0 on EVERY path — missing `--review`, unreadable file,
|
|
4365
|
-
* absent practices section, malformed corpus JSON, the parse cap tripped, an
|
|
4366
|
-
* unrecognized flag, or (round 7, WR-03) a bare positional token, which now
|
|
4367
|
-
* reports the argument that was not understood rather than being silently
|
|
4368
|
-
* discarded — because a review path named without its flag would otherwise
|
|
4369
|
-
* produce the same zero-byte output as a clean review. A boundary or quote
|
|
4370
|
-
* shortfall is a defect in the REVIEW.md, not in the reviewed code; failing
|
|
4371
|
-
* the review would punish the wrong artifact.
|
|
4372
|
-
*
|
|
4373
|
-
* Corpus precedence: `--corpus-json` when present, otherwise
|
|
4374
|
-
* `process.env.VAULT_QUOTE_CORPUS` (what the workflow step sets), otherwise
|
|
4375
|
-
* empty (no comparison, no output from that half).
|
|
4376
|
-
*
|
|
4377
|
-
* Test seams: `_check` injects a mock of the compiled module (default: the
|
|
4378
|
-
* real compiled practices-review checker, lazy-required INSIDE this function
|
|
4379
|
-
* so it only loads when the command is actually dispatched); `_core` injects
|
|
4380
|
-
* a mock output sink. Production callers pass neither.
|
|
4381
|
-
*/
|
|
4382
|
-
function cmdCheckPracticesReview({ args }, _check, _core) {
|
|
4383
|
-
const c = _core ?? _defaultCheckCore;
|
|
4384
|
-
let reviewPath;
|
|
4385
|
-
let corpusJsonFlag;
|
|
4386
|
-
let boundariesFlag = false;
|
|
4387
|
-
let quotesFlag = false;
|
|
4388
|
-
const rest = args.slice(2);
|
|
4389
|
-
for (let i = 0; i < rest.length; i++) {
|
|
4390
|
-
const tok = rest[i];
|
|
4391
|
-
if (tok === '--review' || tok === '--corpus-json') {
|
|
4392
|
-
// WR-03: a value-taking flag whose value is missing (trailing token),
|
|
4393
|
-
// empty (T-27-R8-05), or flag-shaped (the next token starts with "--")
|
|
4394
|
-
// must never consume that token as its value — route through the same
|
|
4395
|
-
// usage-message path the unrecognized-flag guard below already uses,
|
|
4396
|
-
// and return before any file read is attempted. The empty-string case
|
|
4397
|
-
// belongs HERE rather than at the read guard below: `--review ''`
|
|
4398
|
-
// currently passes this guard, sets `reviewPath` to the empty string,
|
|
4399
|
-
// then fails the truthiness test at the read guard, which is the
|
|
4400
|
-
// deliberate feature-off path — so the invocation produces zero bytes
|
|
4401
|
-
// at exit 0, byte-identical to a clean review. The one call site that
|
|
4402
|
-
// matters reaches this shape whenever the workflow's `REVIEW_PATH`
|
|
4403
|
-
// variable expands empty.
|
|
4404
|
-
const candidate = rest[i + 1];
|
|
4405
|
-
if (candidate === undefined || candidate === '' || candidate.startsWith('--')) {
|
|
4406
|
-
const msg = `Flag "${tok}" requires a value. Supported: --review <path>, --corpus-json <string>, --boundaries, --quotes.`;
|
|
4407
|
-
c.output(msg, true, msg);
|
|
4408
|
-
return;
|
|
4409
|
-
}
|
|
4410
|
-
if (tok === '--review')
|
|
4411
|
-
reviewPath = candidate;
|
|
4412
|
-
else
|
|
4413
|
-
corpusJsonFlag = candidate;
|
|
4414
|
-
i++;
|
|
4415
|
-
}
|
|
4416
|
-
else if (tok === '--boundaries') {
|
|
4417
|
-
boundariesFlag = true;
|
|
4418
|
-
}
|
|
4419
|
-
else if (tok === '--quotes') {
|
|
4420
|
-
quotesFlag = true;
|
|
4421
|
-
}
|
|
4422
|
-
else if (tok.startsWith('--') && !RECOGNIZED_PRACTICES_REVIEW_FLAGS.has(tok)) {
|
|
4423
|
-
const msg = `Unrecognized flag "${tok}" for check practices-review. Supported: --review <path>, --corpus-json <string>, --boundaries, --quotes.`;
|
|
4424
|
-
c.output(msg, true, msg);
|
|
4425
|
-
return;
|
|
4426
|
-
}
|
|
4427
|
-
else {
|
|
4428
|
-
// WR-03 (round 7): a bare positional token — a review path named
|
|
4429
|
-
// without its flag — must never be silently discarded. Discarding it
|
|
4430
|
-
// exits 0 with zero bytes, byte-identical to a clean review, which is
|
|
4431
|
-
// exactly the false-clean signal this function's whole design exists
|
|
4432
|
-
// to avoid. Route through the same usage-message path the unrecognized-
|
|
4433
|
-
// flag guard above already uses, and return before any file read.
|
|
4434
|
-
const msg = `Unexpected argument "${tok}" for check practices-review. Did you mean --review ${tok}? Supported: --review <path>, --corpus-json <string>, --boundaries, --quotes.`;
|
|
4435
|
-
c.output(msg, true, msg);
|
|
4436
|
-
return;
|
|
4437
|
-
}
|
|
4438
|
-
}
|
|
4439
|
-
// WR-02: track a failed read separately from "no --review given at all" so
|
|
4440
|
-
// the operator-visible line below fires ONLY when a path was actually
|
|
4441
|
-
// supplied and could not be read — never for the feature-off case (no
|
|
4442
|
-
// --review flag), which must keep producing zero bytes (T-27-98).
|
|
4443
|
-
let reviewText = '';
|
|
4444
|
-
let reviewReadFailed = false;
|
|
4445
|
-
if (reviewPath) {
|
|
4446
|
-
try {
|
|
4447
|
-
reviewText = node_fs_1.default.readFileSync(reviewPath, 'utf8');
|
|
4448
|
-
}
|
|
4449
|
-
catch {
|
|
4450
|
-
reviewText = '';
|
|
4451
|
-
reviewReadFailed = true;
|
|
4452
|
-
}
|
|
4453
|
-
}
|
|
4454
|
-
const resolvedCorpusJson = corpusJsonFlag ?? process.env['VAULT_QUOTE_CORPUS'] ?? '';
|
|
4455
|
-
const runBoth = !boundariesFlag && !quotesFlag;
|
|
4456
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
|
|
4457
|
-
const mod = _check ?? require('./practices-review-check.cjs');
|
|
4458
|
-
const result = mod.runReviewCheck({
|
|
4459
|
-
reviewText,
|
|
4460
|
-
corpusJson: resolvedCorpusJson,
|
|
4461
|
-
boundaries: runBoth || boundariesFlag,
|
|
4462
|
-
quotes: runBoth || quotesFlag,
|
|
4463
|
-
});
|
|
4464
|
-
const renderedLines = [...result.lines];
|
|
4465
|
-
if (reviewReadFailed && reviewPath) {
|
|
4466
|
-
// Plain English, no exception text / errno / internal identifier (COMM-01).
|
|
4467
|
-
// Emitted on stdout (via c.output's raw path) because the workflow step
|
|
4468
|
-
// that dispatches this arm redirects stderr with `2>/dev/null || true`.
|
|
4469
|
-
renderedLines.unshift(`The review file at ${reviewPath} could not be read — the boundary and client-quote checks did not run.`);
|
|
4470
|
-
}
|
|
4471
|
-
const text = renderedLines.length > 0 ? renderedLines.join('\n') + '\n' : '';
|
|
4472
|
-
c.output(text, true, text);
|
|
4473
|
-
}
|
|
4474
|
-
/**
|
|
4475
|
-
* cmdVaultAudit — gate-query handler for `check vault.audit` / `check vault-audit`
|
|
4476
|
-
* (Phase 05-06, CHECK-03; the blocking mechanical practice audit of D-05-02).
|
|
4477
|
-
*
|
|
4478
|
-
* Contract (the same two-step contract `cmdVaultTrace` above follows):
|
|
4479
|
-
* - PURE read: it reads the vault and shells `git diff` through the leaf, and
|
|
4480
|
-
* it never fetches, never writes, never throws and ALWAYS exits 0.
|
|
4481
|
-
* - COMPUTES `block` as a payload BOOLEAN. The halt belongs to the workflow
|
|
4482
|
-
* enforcer, which reads this boolean (D-VH-7). Both declared gates are
|
|
4483
|
-
* `onError: halt`, so a handler that exited non-zero would turn every block
|
|
4484
|
-
* — and every caller typo — into a step-1 COMMAND error, handled by policy
|
|
4485
|
-
* rather than by verdict: `halt` would stop the build on a typo and `skip`
|
|
4486
|
-
* would swallow a real block as a warning (T-5-25).
|
|
4487
|
-
* - Inert when either capability flag is off, and the inert answer is given
|
|
4488
|
-
* BEFORE the vault or the diff is read — so a violation present on disk in
|
|
4489
|
-
* an inactive project is still answered with silence rather than suppressed
|
|
4490
|
-
* after the fact (D-05-02's inherited constraint).
|
|
4491
|
-
* - Delegates EVERY verdict to `vault-audit.cjs`. This handler holds no
|
|
4492
|
-
* comparison, no message composition and no range logic; `vault audit` on
|
|
4493
|
-
* the operator router serializes the same envelope from the same engine,
|
|
4494
|
-
* and a second computation here would be a second place the report can
|
|
4495
|
-
* disagree with the gate that blocks on it.
|
|
4496
|
-
*
|
|
4497
|
-
* Verdict envelope: the leaf's own `runVaultAudit` result — `{ status, block,
|
|
4498
|
-
* message, range, rangeSource, practices[], notes[], … }` — plus the resolved
|
|
4499
|
-
* `phaseDir`, so an auditor can see WHAT was scoped rather than infer it. The
|
|
4500
|
-
* four surface-level answers (`inert`, `bad-argument`, `no-argument`,
|
|
4501
|
-
* `unresolved`) are this handler's own and are the only envelopes it composes.
|
|
4502
|
-
* All four are about the CALL, never about the comparison: the pass-through
|
|
4503
|
-
* rule above is a rule about verdicts, and "your argument was malformed" is not
|
|
4504
|
-
* a verdict on any practice.
|
|
4505
|
-
*
|
|
4506
|
-
* Invocable as: pp_run check vault.audit <phase-dir> [--range <from>..<to>] [--raw]
|
|
4507
|
-
*
|
|
4508
|
-
* THE PHASE ARGUMENT IS A DIRECTORY, and neither dispatch point passes `--ws`,
|
|
4509
|
-
* because the path already carries the workstream partition
|
|
4510
|
-
* (`check-query-gate.md:27`). The obligation that transfers here is the one the
|
|
4511
|
-
* omitted-`--ws` defect taught: this handler must accept a phase DIRECTORY and
|
|
4512
|
-
* must never silently fall back to a flat `.planning/phases` lookup on a
|
|
4513
|
-
* partitioned project. It resolves the positional through the SAME
|
|
4514
|
-
* `resolveTracePhaseDir` the trace gate uses — one resolver, so the two gates
|
|
4515
|
-
* at these two points can never disagree about which phase they audited.
|
|
4516
|
-
*
|
|
4517
|
-
* IT READS ONE FLAG, `--range`, AND IGNORES EVERY OTHER, and the ignoring is
|
|
4518
|
-
* LOAD-BEARING rather than incidental (04-16/WR-09, the lesson
|
|
4519
|
-
* `cmdShipPractices` records). Both dispatch points set `GATE_UNPAIRED` for the
|
|
4520
|
-
* WHOLE loop, not for one gate, so this handler receives an appended
|
|
4521
|
-
* `--unpaired <value>` it never asked for and must not read.
|
|
4522
|
-
*
|
|
4523
|
-
* `--range` (08-06, D-08-19) is the operator's, and NO enforcer call site
|
|
4524
|
-
* appends it: it exists so a phase can be audited by hand over a range the
|
|
4525
|
-
* operator names when the phase-start ladder has nothing to resolve from.
|
|
4526
|
-
*
|
|
4527
|
-
* NEITHER PAIR NEEDS AN ALLOWLIST ENTRY IN THE POSITIONAL SCANNER, and adding
|
|
4528
|
-
* one would be a regression rather than a safeguard. `readTracePositional`
|
|
4529
|
-
* skips the token after ANY flag by rule (04-16, WR-08) — the allowlist it
|
|
4530
|
-
* replaced was a correctness dependency on somebody remembering to extend it —
|
|
4531
|
-
* so `--range <value>` is skipped by the same rule that skips
|
|
4532
|
-
* `--unpaired <value>`, and no future parameter of either kind can silently
|
|
4533
|
-
* become the phase.
|
|
4534
|
-
*
|
|
4535
|
-
* The dot→hyphen normalization in routeCheckCommand maps "vault.audit" to the
|
|
4536
|
-
* "vault-audit" branch.
|
|
4537
|
-
*/
|
|
4538
|
-
function cmdVaultAudit(cwd, args, raw) {
|
|
4539
|
-
// Step 1 — root. Copied from cmdShipPractices: the resolution belongs to the
|
|
4540
|
-
// handler, because this handler reads none of its arguments for it.
|
|
4541
|
-
let root;
|
|
4542
|
-
try {
|
|
4543
|
-
root = findProjectRoot(cwd) || cwd;
|
|
4544
|
-
}
|
|
4545
|
-
catch {
|
|
4546
|
-
root = cwd;
|
|
4547
|
-
}
|
|
4548
|
-
// Step 1b (16-05, D-37; research C-13) — bindings first, before the switch
|
|
4549
|
-
// read, any vault read or any git subprocess. No `.platform-bindings.json`
|
|
4550
|
-
// means this is not a Pareto project, and a non-Pareto project answers inert
|
|
4551
|
-
// here, never `warn`. Before 16-05 this handler had no bindings guard and
|
|
4552
|
-
// answered a blocking `warn` (`range_unresolvable`, no phase manifest) on
|
|
4553
|
-
// such a project with the switches on: a gate blocking a ship where no vault
|
|
4554
|
-
// exists to govern it. The envelope is this handler's existing inert shape.
|
|
4555
|
-
if (!hasPlatformBindings(root)) {
|
|
4556
|
-
output({ status: 'inert', block: false, message: '' }, raw, undefined);
|
|
4557
|
-
return;
|
|
4558
|
-
}
|
|
4559
|
-
// Step 2 — the capability's own kill switch, answered before any git
|
|
4560
|
-
// subprocess. It no longer rests on the registered gate's `when` keeping
|
|
4561
|
-
// this handler unreached in this state: Phase 16's seal gates (16-08) render
|
|
4562
|
-
// with the switch off and dispatch this handler, so the answer here, not the
|
|
4563
|
-
// `when`, is what decides.
|
|
4564
|
-
//
|
|
4565
|
-
// 16-09 (SEAL-02; D-21, D-25 — research C-1's corrected order):
|
|
4566
|
-
// 1. bindings-inert (Step 1b above);
|
|
4567
|
-
// 2. switch off → standards mode active → `inert` (a default standards
|
|
4568
|
-
// project carries no `vault.practices_enabled`, and its records belong
|
|
4569
|
-
// to the standards trace gate, the C-1 cell); else the positional's
|
|
4570
|
-
// phase holds recorded vault artifacts → `disabled_with_artifacts`,
|
|
4571
|
-
// blocking, naming them and the switch; else `inert`;
|
|
4572
|
-
// 3. mode refusal; 4. standards `not_applicable`; 5. range, positional and
|
|
4573
|
-
// the audit itself.
|
|
4574
|
-
// The artifact read is the ONLY vault read before the mode refusal, and it
|
|
4575
|
-
// runs only where standards mode is not active.
|
|
4576
|
-
const { enabled, practicesEnabled } = readShipPracticesFlags(root);
|
|
4577
|
-
if (!enabled || !practicesEnabled) {
|
|
4578
|
-
if (!isStandardsModeActive(cwd)) {
|
|
4579
|
-
const sealed = disabledWithArtifactsBody(root, cwd, args, !enabled ? 'vault.enabled' : 'vault.practices_enabled', false);
|
|
4580
|
-
if (sealed) {
|
|
4581
|
-
output({ status: 'disabled_with_artifacts', block: true, present: true, ...sealed }, raw, undefined);
|
|
4582
|
-
return;
|
|
4583
|
-
}
|
|
4584
|
-
}
|
|
4585
|
-
output({ status: 'inert', block: false, message: '' }, raw, undefined);
|
|
4586
|
-
return;
|
|
4587
|
-
}
|
|
4588
|
-
// 13-06 (D-08, D-22) — after this handler's own inert guard, an unrecognised
|
|
4589
|
-
// `vault.mode` blocks whatever `vault.required` says; never read as practices.
|
|
4590
|
-
const modeVerdict = vaultModeVerdict(cwd);
|
|
4591
|
-
if (modeVerdict.active && !modeVerdict.ok) {
|
|
4592
|
-
output({ status: 'bad-config', block: true, reason: modeVerdict.reason, message: vaultModeLeaf.vaultModeRefusalMessage(modeVerdict.reason) }, raw, undefined);
|
|
4593
|
-
return;
|
|
4594
|
-
}
|
|
4595
|
-
// 13-07 (D-13, PARK-02) — standards mode has no practice layer to audit. The
|
|
4596
|
-
// envelope is the SAME builder output `vault audit` emits (D-05-02), with a
|
|
4597
|
-
// non-blocking flag: the enforcer halts only on that boolean (D-VH-7), so
|
|
4598
|
-
// verify:post and ship:pre proceed. Placed above the range and the phase
|
|
4599
|
-
// argument, as the operator arm is, so the two agree on every argument shape.
|
|
4600
|
-
// The flag is restated on the emitting line (equal to the builder's) so the
|
|
4601
|
-
// producer audit's line probe sees this site; check-query-gate.md cites it.
|
|
4602
|
-
if (isStandardsModeActive(cwd)) {
|
|
4603
|
-
output({ ...vaultModeLeaf.notApplicableGateEnvelope('vault.audit'), block: false }, raw, undefined);
|
|
4604
|
-
return;
|
|
4605
|
-
}
|
|
4606
|
-
// Step 2b — the operator's range, refused as DATA if it is malformed
|
|
4607
|
-
// (08-06, D-08-19, T-08-32).
|
|
4608
|
-
//
|
|
4609
|
-
// ABOVE PHASE RESOLUTION, on the same argument the trace handler's stage and
|
|
4610
|
-
// unpaired arms are placed by: a caller mistake is reported before the phase
|
|
4611
|
-
// is looked up, so a bad flag and a bad positional cannot race to different
|
|
4612
|
-
// answers depending on which the filesystem happened to resolve. BELOW the
|
|
4613
|
-
// inertness guard, for the other half of that argument: a project this gate
|
|
4614
|
-
// must not exist for still answers `inert` however wrong the arguments are
|
|
4615
|
-
// (T-03-43).
|
|
4616
|
-
//
|
|
4617
|
-
// It BLOCKS, like the two argument verdicts below it: a range that was
|
|
4618
|
-
// refused compared nothing, and nothing-compared must never read as passed.
|
|
4619
|
-
const rangeScope = readAuditRange(args);
|
|
4620
|
-
if (!rangeScope.ok) {
|
|
4621
|
-
output({ status: 'bad-argument', block: true, message: AUDIT_RANGE_REFUSED_MESSAGE }, raw, undefined);
|
|
4622
|
-
return;
|
|
4623
|
-
}
|
|
4624
|
-
// Step 3 — the caller's argument. This is a payload verdict, never
|
|
4625
|
-
// `error(...)`: the contract above promises exit 0 on every path, and both
|
|
4626
|
-
// hooks that dispatch this gate are `onError: halt` (WR-01, T-03-41).
|
|
4627
|
-
//
|
|
4628
|
-
// It BLOCKS, because a gate that was told nothing has proved nothing, and
|
|
4629
|
-
// "proved nothing" reading as "passed" is the silence this workstream exists
|
|
4630
|
-
// to end. It sits below the inertness guard for the same reason the trace
|
|
4631
|
-
// handler's does: a project the gate must not exist for still answers `inert`
|
|
4632
|
-
// however wrong the arguments are (T-03-43).
|
|
4633
|
-
const positional = readTracePositional(args);
|
|
4634
|
-
if (!positional) {
|
|
4635
|
-
output({
|
|
4636
|
-
status: 'no-argument',
|
|
4637
|
-
block: true,
|
|
4638
|
-
message: 'Vault audit: this gate was invoked with no phase directory, so no practice was '
|
|
4639
|
-
+ 'compared against anything. Repair: re-run it with the phase directory as the '
|
|
4640
|
-
+ 'first argument.',
|
|
4641
|
-
}, raw, undefined);
|
|
4642
|
-
return;
|
|
4643
|
-
}
|
|
4644
|
-
const phase = resolveTracePhaseDir(root, cwd, positional);
|
|
4645
|
-
if (!phase) {
|
|
4646
|
-
// Fail closed, and state only what was MEASURED. A gate message is read by
|
|
4647
|
-
// orchestrators that may perform the remedy it names, so it names a re-run
|
|
4648
|
-
// and never a deletion (T-4-75).
|
|
4649
|
-
output({
|
|
4650
|
-
status: 'unresolved',
|
|
4651
|
-
block: true,
|
|
4652
|
-
message: `Vault audit: "${sanitizeTraceText(positional, TRACE_PATH_CAP)}" is neither an existing `
|
|
4653
|
-
+ 'directory inside the project nor a phase the locator resolves, so no practice was '
|
|
4654
|
-
+ 'compared against anything. Repair: re-run this gate with the phase directory.',
|
|
4655
|
-
}, raw, undefined);
|
|
4656
|
-
return;
|
|
4657
|
-
}
|
|
4658
|
-
// Step 4 — the audit itself. Lazily required INSIDE the handler rather than
|
|
4659
|
-
// imported at the top of the file: this router is loaded for every `check`
|
|
4660
|
-
// subcommand, and the evaluator is only ever needed by this one.
|
|
4661
|
-
//
|
|
4662
|
-
// The envelope is typed as an OPAQUE record on purpose. This handler adds one
|
|
4663
|
-
// key and reads none, so naming `block` or `message` in a type here would be
|
|
4664
|
-
// both unnecessary and misleading — the producer-audit row cites the lines at
|
|
4665
|
-
// which this handler can emit a block, and a type annotation spelling the
|
|
4666
|
-
// field is not one of them. Keeping the envelope opaque means the pass-through
|
|
4667
|
-
// rule is enforced by the type rather than by discipline.
|
|
4668
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
4669
|
-
const auditMod = require('./vault-audit.cjs');
|
|
4670
|
-
// The key is SPREAD IN rather than passed as `range: rangeScope.range`. The
|
|
4671
|
-
// leaf tests `typeof opts.range === 'string'` to decide whether the ladder is
|
|
4672
|
-
// consulted at all, so an explicit `range: undefined` would be harmless today
|
|
4673
|
-
// and one refactor away from meaning "an operator range that is not a
|
|
4674
|
-
// string" — which is the refusal arm. Absent stays absent.
|
|
4675
|
-
const result = auditMod.runVaultAudit({
|
|
4676
|
-
root,
|
|
4677
|
-
phaseDir: phase.abs,
|
|
4678
|
-
...(rangeScope.range !== null ? { range: rangeScope.range } : {}),
|
|
4679
|
-
});
|
|
4680
|
-
// The leaf's envelope, verbatim, plus the directory it was scoped to. Nothing
|
|
4681
|
-
// is recomputed and no field is rewritten — `block` is already the strict
|
|
4682
|
-
// boolean the enforcer reads and `message` is already bounded.
|
|
4683
|
-
output({ ...result, phaseDir: phase.rel }, raw, undefined);
|
|
4684
|
-
}
|
|
4685
|
-
/**
|
|
4686
|
-
* The flags `check vault-review` recognizes — which is NONE beyond the single
|
|
4687
|
-
* phase-directory positional.
|
|
4688
|
-
*
|
|
4689
|
-
* `--raw` never reaches here: `pp-tools.cjs` splices it out of argv before the
|
|
4690
|
-
* router is called, so it is accepted at the CLI and is not this set's business.
|
|
4691
|
-
* Every other `--`-prefixed token, and every SECOND positional, is rejected BY
|
|
4692
|
-
* NAME — and the rejection still exits 0, because this command is advisory and
|
|
4693
|
-
* a typo in a workflow step must never break a review run.
|
|
4694
|
-
*/
|
|
4695
|
-
const RECOGNIZED_VAULT_REVIEW_FLAGS = new Set();
|
|
4696
|
-
/** A phase directory's leading token: `06`, `6` or `06.1`. */
|
|
4697
|
-
const VAULT_REVIEW_PHASE_TOKEN = /^(\d{1,3}(?:\.\d{1,3})?)-/;
|
|
4698
|
-
/**
|
|
4699
|
-
* cmdCheckVaultReview — `check vault-review` / `check vault.review` handler
|
|
4700
|
-
* (Vault Assurance #71924, phase 06 plan 04; VERIFY-02, VERIFY-03, D-06-01).
|
|
4701
|
-
*
|
|
4702
|
-
* ## The contract, copied verbatim from `cmdCheckPracticesReview`
|
|
4703
|
-
*
|
|
4704
|
-
* Exit status is 0 on EVERY path — no positional, an unresolvable phase
|
|
4705
|
-
* directory, an unrecognized flag, a second bare positional, an absent vault,
|
|
4706
|
-
* an unreadable diff — and **no envelope this handler emits carries a `block`
|
|
4707
|
-
* key on any path.** The two properties are not the same promise and both are
|
|
4708
|
-
* load-bearing:
|
|
4709
|
-
*
|
|
4710
|
-
* - exit 0, because a silent non-zero from an advisory command would be read
|
|
4711
|
-
* by an `onError: halt` caller as a halt it never asked for; and
|
|
4712
|
-
* - no `block` key, because that is the field a gate enforcer READS. This arm
|
|
4713
|
-
* is in no capability's `gates[]` and is dispatched from no loop point, so
|
|
4714
|
-
* the absence of the key is the second, structural barrier D-06-01 asks for:
|
|
4715
|
-
* even a future wiring mistake cannot turn a Phase 6 verdict into a halt,
|
|
4716
|
-
* because there is nothing shaped like a halt to find.
|
|
4717
|
-
*
|
|
4718
|
-
* A REVIEW THAT PRINTED NOTHING WOULD BE THE DEFECT. The precedent's own header
|
|
4719
|
-
* states it: a review path named without its flag produces the same zero-byte
|
|
4720
|
-
* output as a clean review. So every degenerate path here emits a `statement`
|
|
4721
|
-
* naming what did not happen, and the payload's `statements[]` carries the same
|
|
4722
|
-
* discipline for the paths that DID run (D-06-14, D-06-15).
|
|
4723
|
-
*
|
|
4724
|
-
* ## Deliberately NOT the blocking sibling
|
|
4725
|
-
*
|
|
4726
|
-
* `check vault-audit` (05-06) is the BLOCKING mechanical tier, declared at
|
|
4727
|
-
* `ship:pre` and `verify:post`. This is a different command with a different
|
|
4728
|
-
* name doing the opposite thing (D-06-04), and the two are never merged: the
|
|
4729
|
-
* one-letter distance between them is exactly why the retroactive surface was
|
|
4730
|
-
* renamed `review` rather than kept as `audit`.
|
|
4731
|
-
*
|
|
4732
|
-
* ## Inertness, before anything is read
|
|
4733
|
-
*
|
|
4734
|
-
* `vault.enabled` / `vault.practices_enabled` are resolved through the shared
|
|
4735
|
-
* `readShipPracticesFlags`, a delegate to the gate-identical readers in
|
|
4736
|
-
* `src/vault.cts` (Phase 15 D-24/D-25; it no longer reads the root file
|
|
4737
|
-
* itself). It reports both false for an absent key and for
|
|
4738
|
-
* an explicit `false` — so the two spellings of "off" produce byte-identical
|
|
4739
|
-
* output, and neither reaches the vault, the trace or a git subprocess. The
|
|
4740
|
-
* inert envelope carries no payload at all: an inert run that emitted an empty
|
|
4741
|
-
* review shape would be a review of nothing wearing a review's clothes.
|
|
4742
|
-
*
|
|
4743
|
-
* ## Every vault byte is fenced by the SHIPPED renderer
|
|
4744
|
-
*
|
|
4745
|
-
* The remainder practices the agent is handed go through
|
|
4746
|
-
* `renderPracticeEntry` (`src/vault-practices.cts`) — `boundedToken` +
|
|
4747
|
-
* `defangFenceMarkers` + the reference-data preamble, between the two genuine
|
|
4748
|
-
* fence markers. No second renderer is written here; a second renderer is a
|
|
4749
|
-
* second place a client-authored byte can reach an agent context unfenced,
|
|
4750
|
-
* which is precisely 05-REVIEW's CR-02.
|
|
4751
|
-
*
|
|
4752
|
-
* Test seams: `_review` injects a mock of the compiled review leaf, `_core` a
|
|
4753
|
-
* mock output sink. Production callers pass neither.
|
|
4754
|
-
*/
|
|
4755
|
-
function cmdCheckVaultReview(cwd, args, raw, _review, _core) {
|
|
4756
|
-
const c = _core ?? _defaultCheckCore;
|
|
4757
|
-
const say = (status, statement) => {
|
|
4758
|
-
c.output({ status, statement }, raw, undefined);
|
|
4759
|
-
};
|
|
4760
|
-
// Step 1 — the project root. The handler owns the resolution, as
|
|
4761
|
-
// `cmdVaultAudit` and `cmdShipPractices` both do.
|
|
4762
|
-
let root;
|
|
4763
|
-
try {
|
|
4764
|
-
root = findProjectRoot(cwd) || cwd;
|
|
4765
|
-
}
|
|
4766
|
-
catch {
|
|
4767
|
-
root = cwd;
|
|
4768
|
-
}
|
|
4769
|
-
// Step 1b (16-05, D-37; research C-13) — bindings first, through the one
|
|
4770
|
-
// predicate every vault handler asks. A project with no
|
|
4771
|
-
// `.platform-bindings.json` is not a Pareto project; before 16-05 this
|
|
4772
|
-
// handler reviewed it anyway (`reviewed`, arm `reconstruct`) whenever the
|
|
4773
|
-
// switches were on. The inert sentence names why, which is a different
|
|
4774
|
-
// report from a phase that was reviewed and found clean. No `block` key, as
|
|
4775
|
-
// on every path of this handler (D-06-01).
|
|
4776
|
-
if (!hasPlatformBindings(root)) {
|
|
4777
|
-
say('inert', 'Vault review: this project has no platform bindings, so it is not a vault project; no ' +
|
|
4778
|
-
'practice was reviewed and no review payload was produced. This is a project the feature ' +
|
|
4779
|
-
'does not apply to, which is a different report from a phase that was reviewed and found clean.');
|
|
4780
|
-
return;
|
|
4781
|
-
}
|
|
4782
|
-
// Step 2 — the capability's kill switch, answered BEFORE any vault read and
|
|
4783
|
-
// before any git subprocess, so a violation present on disk in an inactive
|
|
4784
|
-
// project is answered with an inert sentence rather than suppressed after the
|
|
4785
|
-
// fact. Both spellings of "off" arrive here identically.
|
|
4786
|
-
const { enabled, practicesEnabled } = readShipPracticesFlags(root);
|
|
4787
|
-
if (!enabled || !practicesEnabled) {
|
|
4788
|
-
say('inert', 'Vault review: the vault practices capability is off for this project, so no practice was ' +
|
|
4789
|
-
'reviewed and no review payload was produced. This is a project the feature does not apply ' +
|
|
4790
|
-
'to, which is a different report from a phase that was reviewed and found clean.');
|
|
4791
|
-
return;
|
|
4792
|
-
}
|
|
4793
|
-
// 13-06 (D-08, D-22) — after the inert answer, an unrecognised `vault.mode`
|
|
4794
|
-
// answers bad-config. NO `block` key, on this path as on every other: the
|
|
4795
|
-
// review gate's structural barrier is that it never carries one (D-06-01).
|
|
4796
|
-
const modeVerdict = vaultModeVerdict(cwd);
|
|
4797
|
-
if (modeVerdict.active && !modeVerdict.ok) {
|
|
4798
|
-
say('bad-config', vaultModeLeaf.vaultModeRefusalMessage(modeVerdict.reason));
|
|
4799
|
-
return;
|
|
4800
|
-
}
|
|
4801
|
-
// 13-07 (D-14, PARK-03) — standards mode: no practice to review. The
|
|
4802
|
-
// statement names where standards mode records the documents instead; no
|
|
4803
|
-
// `block` key here either (D-06-01). `vault-review.md` stops on it (13-04).
|
|
4804
|
-
if (isStandardsModeActive(cwd)) {
|
|
4805
|
-
say('not_applicable', vaultModeLeaf.notApplicableReviewStatement());
|
|
4806
|
-
return;
|
|
4807
|
-
}
|
|
4808
|
-
// Step 3 — the arguments. One positional, nothing else. A rejection is a
|
|
4809
|
-
// payload verdict at exit 0, never an `error(...)`.
|
|
4810
|
-
let positional;
|
|
4811
|
-
for (const tok of args.slice(2)) {
|
|
4812
|
-
if (typeof tok !== 'string' || tok === '')
|
|
4813
|
-
continue;
|
|
4814
|
-
if (tok.startsWith('--') && !RECOGNIZED_VAULT_REVIEW_FLAGS.has(tok)) {
|
|
4815
|
-
say('usage', `Vault review: "${sanitizeTraceText(tok, TRACE_PATH_CAP)}" is not a flag this command reads, ` +
|
|
4816
|
-
'so nothing was reviewed. Usage: check vault-review <phase-dir> [--raw].');
|
|
4817
|
-
return;
|
|
4818
|
-
}
|
|
4819
|
-
if (tok.startsWith('--'))
|
|
4820
|
-
continue;
|
|
4821
|
-
if (positional !== undefined) {
|
|
4822
|
-
// A second bare positional is never silently discarded: discarding it
|
|
4823
|
-
// exits 0 having reviewed a directory the caller did not name, which is a
|
|
4824
|
-
// wrong answer wearing a right answer's shape.
|
|
4825
|
-
say('usage', `Vault review: a second argument "${sanitizeTraceText(tok, TRACE_PATH_CAP)}" was given and this ` +
|
|
4826
|
-
'command reviews exactly one phase directory, so nothing was reviewed. Usage: ' +
|
|
4827
|
-
'check vault-review <phase-dir> [--raw].');
|
|
4828
|
-
return;
|
|
4829
|
-
}
|
|
4830
|
-
positional = tok;
|
|
4831
|
-
}
|
|
4832
|
-
if (positional === undefined || positional === '') {
|
|
4833
|
-
say('no-argument', 'Vault review: no phase directory was given, so no phase was reviewed and no practice was ' +
|
|
4834
|
-
'looked at. Re-run with the phase directory as the first argument.');
|
|
4835
|
-
return;
|
|
4836
|
-
}
|
|
4837
|
-
// Step 4 — the phase directory, resolved through the SAME resolver the trace
|
|
4838
|
-
// and audit gates use, so the three cannot disagree about which phase they
|
|
4839
|
-
// were pointed at on a workstream-partitioned project.
|
|
4840
|
-
const phase = resolveTracePhaseDir(root, cwd, positional);
|
|
4841
|
-
if (!phase) {
|
|
4842
|
-
say('unresolved', `Vault review: "${sanitizeTraceText(positional, TRACE_PATH_CAP)}" is neither an existing ` +
|
|
4843
|
-
'directory inside the project nor a phase the locator resolves, so no phase was reviewed. ' +
|
|
4844
|
-
'Re-run this command with the phase directory.');
|
|
4845
|
-
return;
|
|
4846
|
-
}
|
|
4847
|
-
// Step 5 — the trace, read from the phase's own artifacts. An ABSENT trace is
|
|
4848
|
-
// not refused (D-06-14): it takes the reconstruct arm and says so.
|
|
4849
|
-
const scan = collectTraceArtifacts(phase, TRACE_ALL_STAGES);
|
|
4850
|
-
const traceSources = [];
|
|
4851
|
-
const ledgerIds = [];
|
|
4852
|
-
const traceBlocks = [];
|
|
4853
|
-
let universe = null;
|
|
4854
|
-
let pinSha = null;
|
|
4855
|
-
for (const artifact of scan.artifacts) {
|
|
4856
|
-
let content;
|
|
4857
|
-
try {
|
|
4858
|
-
content = node_fs_1.default.readFileSync(artifact.abs, 'utf-8');
|
|
4859
|
-
}
|
|
4860
|
-
catch {
|
|
4861
|
-
continue;
|
|
4862
|
-
}
|
|
4863
|
-
const parsed = parseVaultTraceBlock(content, artifact.rel);
|
|
4864
|
-
if (!parsed.present || !parsed.statement)
|
|
4865
|
-
continue;
|
|
4866
|
-
traceSources.push(artifact.rel);
|
|
4867
|
-
for (const p of parsed.statement.predicate.practices) {
|
|
4868
|
-
if (typeof p.id === 'string' && p.id !== '')
|
|
4869
|
-
ledgerIds.push(p.id);
|
|
4870
|
-
}
|
|
4871
|
-
const vault = parsed.statement.predicate.vault;
|
|
4872
|
-
// A plain object or nothing. An array, a string or a number is not a
|
|
4873
|
-
// record, and handing one over as though it were would put a fabricated
|
|
4874
|
-
// bypass in front of a reader — the same predicate `cmdVaultTrace` applies
|
|
4875
|
-
// to the same field, so the gate and the review cannot disagree about what
|
|
4876
|
-
// counts as a recorded override.
|
|
4877
|
-
//
|
|
4878
|
-
// THE `Array.isArray` HALF IS UNREACHABLE, and the measurement is recorded
|
|
4879
|
-
// rather than the clause removed.
|
|
4880
|
-
//
|
|
4881
|
-
// `parseVaultTraceBlock` (src/vault-trace.cts) already normalises a
|
|
4882
|
-
// non-record `override` to `null` before any consumer sees it, so
|
|
4883
|
-
// `rawOverride` can never BE an array here. Removing this clause reddens
|
|
4884
|
-
// nothing — measured on the 08-06 ladder, 262 tests green with it gone —
|
|
4885
|
-
// and so does removing the identical clause in `cmdVaultTrace`. The guard
|
|
4886
|
-
// that actually holds is the PARSER's: mutating that one reddens the
|
|
4887
|
-
// `override: is a LIST` test in the integration suite, which is where that
|
|
4888
|
-
// behaviour is therefore pinned.
|
|
4889
|
-
//
|
|
4890
|
-
// It stays because three readers of this one field spelling the predicate
|
|
4891
|
-
// the same way is what stops them drifting if the parser's normalisation is
|
|
4892
|
-
// ever relaxed. A later reader who mutates this clause, sees a green suite
|
|
4893
|
-
// and concludes it is dead code would be reading a true measurement and
|
|
4894
|
-
// drawing a false conclusion about WHERE the guarantee lives.
|
|
4895
|
-
const rawOverride = parsed.statement.predicate.override;
|
|
4896
|
-
const str = (v) => (typeof v === 'string' ? v : null);
|
|
4897
|
-
const override = rawOverride !== null && typeof rawOverride === 'object' && !Array.isArray(rawOverride)
|
|
4898
|
-
? (() => {
|
|
4899
|
-
const o = rawOverride;
|
|
4900
|
-
return {
|
|
4901
|
-
gate: str(o.gate),
|
|
4902
|
-
reason: str(o.reason),
|
|
4903
|
-
suppressedReason: str(o.suppressedReason),
|
|
4904
|
-
who: str(o.who),
|
|
4905
|
-
when: str(o.when),
|
|
4906
|
-
};
|
|
4907
|
-
})()
|
|
4908
|
-
: null;
|
|
4909
|
-
traceBlocks.push({
|
|
4910
|
-
source: artifact.rel,
|
|
4911
|
-
state: typeof vault.state === 'string' ? vault.state : null,
|
|
4912
|
-
reason: typeof vault.reason === 'string' ? vault.reason : null,
|
|
4913
|
-
override,
|
|
4914
|
-
});
|
|
4915
|
-
if (Array.isArray(vault.practiceUniverse)) {
|
|
4916
|
-
universe = [...(universe ?? []), ...vault.practiceUniverse];
|
|
4917
|
-
}
|
|
4918
|
-
if (pinSha === null && typeof vault.resolvedSha === 'string')
|
|
4919
|
-
pinSha = vault.resolvedSha;
|
|
4920
|
-
}
|
|
4921
|
-
// Step 6 — the live vault cache's own pin, for the divergence advisory the
|
|
4922
|
-
// leaf composes. Read-and-swallow: a manifest that cannot be read simply
|
|
4923
|
-
// yields no advisory, and the leaf's own notes say what was not measured.
|
|
4924
|
-
let cacheSha = null;
|
|
4925
|
-
try {
|
|
4926
|
-
const manifest = readManifest(root);
|
|
4927
|
-
if (manifest && typeof manifest.resolvedSha === 'string')
|
|
4928
|
-
cacheSha = manifest.resolvedSha;
|
|
4929
|
-
}
|
|
4930
|
-
catch {
|
|
4931
|
-
cacheSha = null;
|
|
4932
|
-
}
|
|
4933
|
-
// Step 7 — the vault itself, and the review. Both modules are required LAZILY
|
|
4934
|
-
// inside this function rather than at the top of the file: this router is
|
|
4935
|
-
// loaded for every `check` subcommand, and neither module is needed by any
|
|
4936
|
-
// other one.
|
|
4937
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
4938
|
-
const practices = require('./vault-practices.cjs');
|
|
4939
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
4940
|
-
const reviewMod = _review ?? require('./vault-review.cjs');
|
|
4941
|
-
const discovery = practices.discoverPracticeFiles(root);
|
|
4942
|
-
const tokenMatch = VAULT_REVIEW_PHASE_TOKEN.exec(node_path_1.default.basename(phase.abs));
|
|
4943
|
-
const phaseToken = tokenMatch ? tokenMatch[1] : null;
|
|
4944
|
-
// The matcher is run over the review's own file set, so the entries the agent
|
|
4945
|
-
// is handed carry the winning globs and matched files for THIS range and not
|
|
4946
|
-
// for some other one. The payload builder resolves the range itself, so the
|
|
4947
|
-
// match is run twice-scoped: once here for the entries, and the derivation
|
|
4948
|
-
// uses the range's own file list.
|
|
4949
|
-
const payload = reviewMod.buildReviewPayload({
|
|
4950
|
-
cwd: root,
|
|
4951
|
-
phaseDir: phase.rel,
|
|
4952
|
-
phaseToken,
|
|
4953
|
-
trace: {
|
|
4954
|
-
present: traceSources.length > 0,
|
|
4955
|
-
sources: traceSources,
|
|
4956
|
-
blocks: traceBlocks,
|
|
4957
|
-
ledgerIds,
|
|
4958
|
-
universe,
|
|
4959
|
-
pinSha,
|
|
4960
|
-
},
|
|
4961
|
-
cacheSha,
|
|
4962
|
-
discovery,
|
|
4963
|
-
practices: discovery.practices,
|
|
4964
|
-
skipped: discovery.skipped,
|
|
4965
|
-
degraded: discovery.degraded,
|
|
4966
|
-
matchEntries: (files) => collectVaultReviewEntries(practices, root, files),
|
|
4967
|
-
renderEntry: (entry) => practices.renderPracticeEntry(entry),
|
|
4968
|
-
});
|
|
4969
|
-
c.output({ ...payload, phaseDir: phase.rel }, raw, undefined);
|
|
4970
|
-
}
|
|
4971
|
-
/**
|
|
4972
|
-
* The matcher entries whose prose the remainder practices are rendered from.
|
|
4973
|
-
*
|
|
4974
|
-
* Called by the leaf with THIS REVIEW'S resolved file set, so a scoped
|
|
4975
|
-
* practice's winning globs and matched files describe this range and no other.
|
|
4976
|
-
* Kept READ-AND-SWALLOW: an unreadable vault yields no entries, the leaf's own
|
|
4977
|
-
* not-derived record says the vault could not be read, and the review still
|
|
4978
|
-
* emits everything else it measured. A throw here would turn an unreadable
|
|
4979
|
-
* vault into a non-zero exit from a command whose whole contract is exit 0.
|
|
4980
|
-
*/
|
|
4981
|
-
function collectVaultReviewEntries(practices, root, files) {
|
|
4982
|
-
try {
|
|
4983
|
-
const matched = practices.matchPractices({ cwd: root, files: [...files] });
|
|
4984
|
-
return [...matched.matched, ...matched.global];
|
|
4985
|
-
}
|
|
4986
|
-
catch {
|
|
4987
|
-
return [];
|
|
4988
|
-
}
|
|
4989
|
-
}
|
|
4990
|
-
function routeCheckCommand({ args, cwd, raw, _check, _core, _review }) {
|
|
3083
|
+
function routeCheckCommand({ args, cwd, raw }) {
|
|
4991
3084
|
// Normalize dots to hyphens in the subcommand so both forms are accepted.
|
|
4992
3085
|
// This makes `check.query = "ui.plan-gate"` (dotted form in capability.json gates)
|
|
4993
3086
|
// directly runnable as `pp_run check ui.plan-gate` — the dot is normalized to
|
|
@@ -5031,12 +3124,6 @@ function routeCheckCommand({ args, cwd, raw, _check, _core, _review }) {
|
|
|
5031
3124
|
cmdShipPlatformDrift(cwd, args, raw);
|
|
5032
3125
|
return;
|
|
5033
3126
|
}
|
|
5034
|
-
if (subcommand === 'ship-practices') {
|
|
5035
|
-
// ship.practices — deterministic branch-diff practices re-check (INJ-03, Phase 28).
|
|
5036
|
-
// Dot-to-hyphen normalization means query "ship.practices" routes here.
|
|
5037
|
-
cmdShipPractices(cwd, args, raw);
|
|
5038
|
-
return;
|
|
5039
|
-
}
|
|
5040
3127
|
if (subcommand === 'vault-status') {
|
|
5041
3128
|
// vault.status — vault load verdict (Phase 13; block computed as of GATE-01,
|
|
5042
3129
|
// superseding the advisory-only contract of D-01/D-02).
|
|
@@ -5048,38 +3135,21 @@ function routeCheckCommand({ args, cwd, raw, _check, _core, _review }) {
|
|
|
5048
3135
|
return;
|
|
5049
3136
|
}
|
|
5050
3137
|
if (subcommand === 'vault-trace') {
|
|
5051
|
-
// vault.trace — the
|
|
3138
|
+
// vault.trace — the standards-record validator (Phase 15).
|
|
5052
3139
|
// Dot-to-hyphen normalization means query "vault.trace" routes here.
|
|
5053
|
-
// Pure read;
|
|
5054
|
-
//
|
|
5055
|
-
//
|
|
5056
|
-
// the practices capability is off, and the inert answer is given before any
|
|
3140
|
+
// Pure read; reports its verdict as a payload boolean and still exits 0 on
|
|
3141
|
+
// every path (the halt is the workflow enforcer's); inert when bindings are
|
|
3142
|
+
// absent or the vault is off, and the inert answer is given before any
|
|
5057
3143
|
// policy is read.
|
|
5058
3144
|
cmdVaultTrace(cwd, args, raw);
|
|
5059
3145
|
return;
|
|
5060
3146
|
}
|
|
5061
|
-
if (subcommand === 'vault-audit') {
|
|
5062
|
-
// vault.audit — the mechanical practice audit (CHECK-03, Phase 05-06),
|
|
5063
|
-
// declared blocking at ship:pre and verify:post (D-05-02).
|
|
5064
|
-
// Dot-to-hyphen normalization means query "vault.audit" routes here.
|
|
5065
|
-
// Pure read; delegates every verdict to vault-audit.cjs; reports its
|
|
5066
|
-
// verdict as a payload boolean and still exits 0 on every path (the halt is
|
|
5067
|
-
// the workflow enforcer's); inert when the practices capability is off, and
|
|
5068
|
-
// the inert answer is given before the vault or the diff is read.
|
|
5069
|
-
cmdVaultAudit(cwd, args, raw);
|
|
5070
|
-
return;
|
|
5071
|
-
}
|
|
5072
3147
|
if (subcommand === 'vault-review') {
|
|
5073
|
-
// vault
|
|
5074
|
-
//
|
|
5075
|
-
//
|
|
5076
|
-
//
|
|
5077
|
-
|
|
5078
|
-
// `ship:pre` and `verify:post`, this one is declared in NO capability's
|
|
5079
|
-
// gates[] and emits no `block` key on any path, so nothing can read its
|
|
5080
|
-
// verdict as a halt (D-06-01's structural barrier, VERIFY-03).
|
|
5081
|
-
// Exits 0 on every path; inert before the vault or the diff is read.
|
|
5082
|
-
cmdCheckVaultReview(cwd, args, raw, _review, _core);
|
|
3148
|
+
// vault-review — the derived facts for /pp-vault-review (FORK-ONLY). A
|
|
3149
|
+
// REPORT, never a gate: declared in no capability's gates[], and no
|
|
3150
|
+
// envelope carries `block`. `--validate <artifact>` checks a written
|
|
3151
|
+
// VAULT-REVIEW.md against the same derivation. Exits 0 on every path.
|
|
3152
|
+
cmdCheckVaultReview(cwd, args, raw);
|
|
5083
3153
|
return;
|
|
5084
3154
|
}
|
|
5085
3155
|
if (subcommand === 'verify-schema-drift') {
|
|
@@ -5097,20 +3167,6 @@ function routeCheckCommand({ args, cwd, raw, _check, _core, _review }) {
|
|
|
5097
3167
|
cmdVerifyCodebaseDrift(cwd, raw);
|
|
5098
3168
|
return;
|
|
5099
3169
|
}
|
|
5100
|
-
if (subcommand === 'practices-review') {
|
|
5101
|
-
// IN-02: this arm deliberately diverges from every sibling `check`
|
|
5102
|
-
// subcommand in two ways. (1) It always emits raw text rather than
|
|
5103
|
-
// honouring the `raw` flag's JSON envelope — the workflow steps and their
|
|
5104
|
-
// tests depend on byte-identical operator lines, not a JSON wrapper.
|
|
5105
|
-
// (2) `--review` paths resolve against the invoking process's working
|
|
5106
|
-
// directory (inside cmdCheckPracticesReview), not the router-resolved
|
|
5107
|
-
// `cwd` parameter — which is what the workflow's `"${REVIEW_PATH}"`
|
|
5108
|
-
// expansion already supplies. Both shapes are pinned by the CLI-seam
|
|
5109
|
-
// tests in tests/practices-review-check.test.cjs, so changing either is a
|
|
5110
|
-
// deliberate contract change, not a refactor.
|
|
5111
|
-
cmdCheckPracticesReview({ args }, _check, _core);
|
|
5112
|
-
return;
|
|
5113
|
-
}
|
|
5114
3170
|
if (subcommand === 'prohibition-enforcement') {
|
|
5115
3171
|
// The deterministic test-tier prohibition PRODUCER/gate (#1259, ADR-550 D5d). Locates the
|
|
5116
3172
|
// wired mechanical check (node-test or lint-rule), confirms fail-first, runs it, builds
|
|
@@ -5119,7 +3175,7 @@ function routeCheckCommand({ args, cwd, raw, _check, _core, _review }) {
|
|
|
5119
3175
|
(0, prohibition_enforcement_cjs_1.routeProhibitionEnforcement)(args, raw);
|
|
5120
3176
|
return;
|
|
5121
3177
|
}
|
|
5122
|
-
error('Unknown check subcommand. Available: auto-mode, decision-coverage-plan, decision-coverage-verify, gap-analysis-plan-post,
|
|
3178
|
+
error('Unknown check subcommand. Available: auto-mode, decision-coverage-plan, decision-coverage-verify, gap-analysis-plan-post, prohibition-enforcement, ship-platform-drift, tdd-review-checkpoint, ui-plan-gate, ui-safety-gate, vault-review, vault-status, vault-trace, verify-schema-drift, verify-codebase-drift', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
5123
3179
|
}
|
|
5124
3180
|
module.exports = {
|
|
5125
3181
|
routeCheckCommand,
|
|
@@ -5127,10 +3183,6 @@ module.exports = {
|
|
|
5127
3183
|
// them. Nothing in the shipped code path reads it from outside this module;
|
|
5128
3184
|
// that is the point of it being a value at all (04-11).
|
|
5129
3185
|
TRACE_HANDLER_KIND,
|
|
5130
|
-
// Exported for the SAME reason: the disjointness of "kinds that author their
|
|
5131
|
-
// own tail" and "kinds that get the section tail" is a claim about two sets,
|
|
5132
|
-
// and a claim about a set is only checkable when the set is a value (04-11).
|
|
5133
|
-
TRACE_LEDGER_SECTION_KINDS,
|
|
5134
3186
|
// 15-08: the standards gate's own kind table, re-exported from the leaf that
|
|
5135
3187
|
// owns it so the guards that enumerate this handler's kinds read every table
|
|
5136
3188
|
// from one module. A re-export, never a copy.
|
|
@@ -5141,20 +3193,12 @@ module.exports = {
|
|
|
5141
3193
|
computeUiSafetyGate,
|
|
5142
3194
|
cmdGapAnalysisPlanPost,
|
|
5143
3195
|
cmdTddReviewCheckpoint,
|
|
3196
|
+
tddGateKeys,
|
|
5144
3197
|
cmdShipPlatformDrift,
|
|
5145
|
-
cmdShipPractices,
|
|
5146
3198
|
cmdVaultStatus,
|
|
5147
|
-
// Exported so tests HARVEST the volatility sentence
|
|
5148
|
-
//
|
|
5149
|
-
//
|
|
5150
|
-
// drifted away from it — an assertion about the test file, not the product.
|
|
5151
|
-
// VAULT_PASS_TEXT joins them (quick 261003-lqi) so the pass vocabulary is
|
|
5152
|
-
// harvested the same way.
|
|
3199
|
+
// Exported so tests HARVEST the volatility sentence rather than restating its
|
|
3200
|
+
// text (the 05-05 harvest-or-export rule). A test that re-typed it would keep
|
|
3201
|
+
// passing against a shipped message that had drifted away from it.
|
|
5153
3202
|
VAULT_VERDICT_VOLATILITY_SENTENCE,
|
|
5154
|
-
VAULT_PASS_TEXT,
|
|
5155
|
-
buildVaultSuccessMessage,
|
|
5156
3203
|
cmdVaultTrace,
|
|
5157
|
-
cmdVaultAudit,
|
|
5158
|
-
cmdCheckPracticesReview,
|
|
5159
|
-
cmdCheckVaultReview,
|
|
5160
3204
|
};
|