@pareto4/pp-core 1.12.1 → 1.13.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/agents/pp-code-reviewer.md +1 -1
- package/agents/pp-executor.md +2 -13
- package/agents/pp-planner.md +1 -1
- package/agents/pp-vault-reviewer.md +51 -0
- package/commands/pp-ns-review.md +2 -1
- package/commands/pp-vault-review.md +33 -0
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/pp-core/bin/lib/capability-registry.cjs +200 -10
- package/pp-core/bin/lib/check-command-router.cjs +1242 -37
- package/pp-core/bin/lib/clusters.cjs +1 -0
- package/pp-core/bin/lib/command-aliases.cjs +7 -0
- package/pp-core/bin/lib/compose-bindings.cjs +41 -27
- package/pp-core/bin/lib/decisions.cjs +104 -27
- package/pp-core/bin/lib/dispatch-plan.cjs +12 -1
- package/pp-core/bin/lib/frontmatter.cjs +37 -0
- package/pp-core/bin/lib/git-base-branch.cjs +136 -10
- package/pp-core/bin/lib/init-contract.cjs +180 -66
- package/pp-core/bin/lib/init-interactive.cjs +4 -20
- package/pp-core/bin/lib/loop-gates.cjs +213 -0
- package/pp-core/bin/lib/loop-resolver.cjs +107 -45
- package/pp-core/bin/lib/org-profile.cjs +218 -44
- package/pp-core/bin/lib/phase-command-router.cjs +7 -1
- package/pp-core/bin/lib/phase-manifest.cjs +527 -0
- package/pp-core/bin/lib/phase-tier.cjs +15 -2
- package/pp-core/bin/lib/phase.cjs +51 -1
- package/pp-core/bin/lib/vault-audit.cjs +2070 -0
- package/pp-core/bin/lib/vault-command-router.cjs +460 -1
- package/pp-core/bin/lib/vault-practices.cjs +1142 -104
- package/pp-core/bin/lib/vault-review.cjs +2359 -0
- package/pp-core/bin/lib/vault-trace.cjs +2831 -0
- package/pp-core/bin/lib/vault-update.cjs +266 -0
- package/pp-core/bin/lib/vault-write-back.cjs +19 -0
- package/pp-core/bin/lib/vault.cjs +277 -12
- package/pp-core/bin/pp-tools.cjs +30 -13
- package/pp-core/bin/shared/model-catalog.json +2 -1
- package/pp-core/references/executor-vault-trace.md +47 -0
- package/pp-core/references/loop-hook-dispatch.md +13 -2
- package/pp-core/references/planner-vault-practices.md +46 -1
- package/pp-core/references/planning-config.md +1 -1
- package/pp-core/templates/practices/README.md +140 -11
- package/pp-core/templates/project-charter.md +226 -0
- package/pp-core/workflows/add-tests.md +4 -0
- package/pp-core/workflows/audit-fix.md +2 -0
- package/pp-core/workflows/check-todos.md +1 -1
- package/pp-core/workflows/code-review-fix.md +2 -0
- package/pp-core/workflows/debug.md +4 -0
- package/pp-core/workflows/discovery-sprint/prefill.md +9 -0
- package/pp-core/workflows/discovery-sprint/prototype.md +2 -1
- package/pp-core/workflows/discovery-sprint/vault-fetch.md +37 -0
- package/pp-core/workflows/discovery-sprint.md +1 -1
- package/pp-core/workflows/discovery-to-build.md +1 -1
- package/pp-core/workflows/execute-phase/steps/serial-group-dispatch.md +1 -0
- package/pp-core/workflows/execute-phase/steps/vault-door-gate.md +19 -0
- package/pp-core/workflows/execute-phase/steps/vault-fetch.md +1 -1
- package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +10 -8
- package/pp-core/workflows/execute-phase.md +22 -5
- package/pp-core/workflows/execute-plan.md +4 -0
- package/pp-core/workflows/fast.md +4 -0
- package/pp-core/workflows/help/modes/full.md +3 -2
- package/pp-core/workflows/init.md +2 -2
- package/pp-core/workflows/new-project.md +1 -1
- package/pp-core/workflows/plan-phase/steps/phase-tier.md +9 -0
- package/pp-core/workflows/plan-phase.md +7 -4
- package/pp-core/workflows/quick.md +4 -0
- package/pp-core/workflows/ship.md +5 -1
- package/pp-core/workflows/undo.md +1 -1
- package/pp-core/workflows/validate-phase.md +4 -0
- package/pp-core/workflows/vault-review.md +65 -0
- package/pp-core/workflows/verify-work.md +3 -0
- package/scripts/gen-capability-registry.cjs +11 -0
- package/scripts/lint-test-file-count.allowlist.json +10 -1
- package/scripts/prompt-injection-scan.sh +2 -0
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
/**
|
|
3
6
|
* Vault command router — CLI subcommand dispatcher for `pp-tools vault`.
|
|
4
7
|
*
|
|
@@ -29,7 +32,14 @@
|
|
|
29
32
|
*/
|
|
30
33
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
31
34
|
const io = require("./io.cjs");
|
|
35
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
36
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
32
37
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
38
|
+
// The trace render arm resolves its containment boundary from the project
|
|
39
|
+
// root. project-root.cjs is a leaf (node builtins only), so this adds no cycle.
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
41
|
+
const projectRootMod = require("./project-root.cjs");
|
|
42
|
+
const { findProjectRoot } = projectRootMod;
|
|
33
43
|
const { ERROR_REASON } = io;
|
|
34
44
|
// Default CoreModule implementation. _core seam overrides this for test injection.
|
|
35
45
|
const _defaultCore = { output: io.output };
|
|
@@ -46,7 +56,86 @@ const VAULT_SUBCOMMANDS = Object.freeze([
|
|
|
46
56
|
{ name: 'practices', readsVerdict: false },
|
|
47
57
|
{ name: 'org-profile', readsVerdict: false },
|
|
48
58
|
{ name: 'probe', readsVerdict: false },
|
|
59
|
+
{ name: 'trace', readsVerdict: false },
|
|
60
|
+
{ name: 'audit', readsVerdict: false },
|
|
61
|
+
{ name: 'update', readsVerdict: false },
|
|
62
|
+
{ name: 'review', readsVerdict: false },
|
|
49
63
|
]);
|
|
64
|
+
/**
|
|
65
|
+
* The two trace stages and the two trace verbs, declared once: the `--stage`
|
|
66
|
+
* validation, the verb check and the operator-facing messages that name the
|
|
67
|
+
* permitted values are all derived from these lists, so they cannot disagree.
|
|
68
|
+
* The values are the frozen contract's (`docs/vault-trace-format.md`).
|
|
69
|
+
*/
|
|
70
|
+
const TRACE_STAGES = ['plan', 'execute'];
|
|
71
|
+
const TRACE_VERBS = ['scaffold', 'render'];
|
|
72
|
+
/**
|
|
73
|
+
* The plan-identifier shape `vault trace scaffold --plan` accepts: a phase
|
|
74
|
+
* segment (digits, an optional letter, optional decimal parts) followed by one
|
|
75
|
+
* or more numeric segments — the plan-identifier grammar `agents/pp-planner.md`
|
|
76
|
+
* and `src/phase-id.cts` use. It is exactly the prefix of the artifact's own
|
|
77
|
+
* filename (`04-02` for `04-02-PLAN.md`, `02.3-01` for `02.3-01-PLAN.md`), so a
|
|
78
|
+
* trace gate comparing the recorded identifier with the filename can never be
|
|
79
|
+
* handed one the writer could not emit. It rejects what is not a plan
|
|
80
|
+
* identifier at all: a bare plan number (`01`), a task label (`T1`), a slug
|
|
81
|
+
* (`04-01-vault-trace`) or an empty string.
|
|
82
|
+
*/
|
|
83
|
+
const SCAFFOLD_PLAN_ID_RE = /^\d+[A-Za-z]?(?:\.\d+)*(?:-\d+)+$/;
|
|
84
|
+
/**
|
|
85
|
+
* The required form, in ONE wording, stated in this rejection message and in
|
|
86
|
+
* `pp-core/references/planner-vault-practices.md`. ASCII only and no
|
|
87
|
+
* backticks, so the same bytes survive a JSON string, markdown and a terminal.
|
|
88
|
+
*/
|
|
89
|
+
const SCAFFOLD_PLAN_ID_FORM = "The plan identifier must be the plan's phase-and-number identifier, exactly the " +
|
|
90
|
+
'prefix of its artifact filename (for example 04-02), not a bare plan number, a task ' +
|
|
91
|
+
'label or a slug.';
|
|
92
|
+
/**
|
|
93
|
+
* Bound on the head read the `trace render` arm performs. The trace lives in
|
|
94
|
+
* the artifact's FRONTMATTER, so a head read is the exact region the parser
|
|
95
|
+
* needs, and an artifact body of any size can never make this arm read more.
|
|
96
|
+
*/
|
|
97
|
+
const TRACE_ARTIFACT_READ_BYTES = 262_144;
|
|
98
|
+
/** Repo-relative artifact paths, long enough for a workstream phase tree. */
|
|
99
|
+
const TRACE_REFUSAL_PATH_CAP = 200;
|
|
100
|
+
/** A `gitCommit` digest: lowercase hex at sha1 or sha256 length. */
|
|
101
|
+
const GIT_COMMIT_DIGEST_RE = /^[0-9a-f]{40}$|^[0-9a-f]{64}$/;
|
|
102
|
+
/**
|
|
103
|
+
* Bound and control-character-collapse a caller-supplied path before it
|
|
104
|
+
* reaches a refusal message: the message reaches an operator's terminal and,
|
|
105
|
+
* on the workflow path, an agent's context, so a hostile path must not forge
|
|
106
|
+
* an extra line of output nor run to arbitrary length.
|
|
107
|
+
*/
|
|
108
|
+
function boundedPath(value) {
|
|
109
|
+
const collapsed = value
|
|
110
|
+
.replace(/[\u0000-\u001F\u007F]+/g, ' ')
|
|
111
|
+
.replace(/\s+/g, ' ')
|
|
112
|
+
.trim();
|
|
113
|
+
if (collapsed === '')
|
|
114
|
+
return '(an empty value)';
|
|
115
|
+
return collapsed.length > TRACE_REFUSAL_PATH_CAP
|
|
116
|
+
? `${collapsed.slice(0, TRACE_REFUSAL_PATH_CAP)}…`
|
|
117
|
+
: collapsed;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Is the vault-practices capability fully active for this project? Both
|
|
121
|
+
* `vault.enabled` and `vault.practices_enabled` must be strictly `true` — the
|
|
122
|
+
* same requires-closure the promote preflight and `ship.practices` apply. The
|
|
123
|
+
* trace arms gate on this FIRST, before the flag allowlist, so an inert project
|
|
124
|
+
* stays byte-identically silent on every path, a mistyped one included.
|
|
125
|
+
*/
|
|
126
|
+
function tracePracticesActive(cfgGet, cwd) {
|
|
127
|
+
return cfgGet(cwd, 'vault.enabled', false) === true && cfgGet(cwd, 'vault.practices_enabled', false) === true;
|
|
128
|
+
}
|
|
129
|
+
/** The value token after each of `flags`, by index, so a flag's value is never read as a positional. */
|
|
130
|
+
function flagValueIndices(argv, flags) {
|
|
131
|
+
const indices = new Set();
|
|
132
|
+
for (const flag of flags) {
|
|
133
|
+
const i = argv.indexOf(flag);
|
|
134
|
+
if (i !== -1)
|
|
135
|
+
indices.add(i + 1);
|
|
136
|
+
}
|
|
137
|
+
return indices;
|
|
138
|
+
}
|
|
50
139
|
/**
|
|
51
140
|
* Render a short plain-text `vault org-profile` report from its structured
|
|
52
141
|
* shape — one line naming the pointer, one naming the resolution, a
|
|
@@ -98,7 +187,7 @@ function renderOrgProfileReport(report) {
|
|
|
98
187
|
return lines.join('\n');
|
|
99
188
|
}
|
|
100
189
|
// ─── Implementation ───────────────────────────────────────────────────────────
|
|
101
|
-
function routeVaultCommand({ args, cwd, raw, error, _vault, _core, _practices, _exec, _writeBack, _orgProfile, _coordinates, _provider, }) {
|
|
190
|
+
function routeVaultCommand({ args, cwd, raw, error, _vault, _core, _practices, _exec, _writeBack, _orgProfile, _coordinates, _provider, _trace, _audit, _update, }) {
|
|
102
191
|
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
|
|
103
192
|
const vault = _vault ?? require('./vault.cjs');
|
|
104
193
|
const c = _core ?? _defaultCore;
|
|
@@ -268,6 +357,9 @@ function routeVaultCommand({ args, cwd, raw, error, _vault, _core, _practices, _
|
|
|
268
357
|
[coordinates.SHARED_REF_CONFIG_KEY]: shared.ref,
|
|
269
358
|
[coordinates.SHARED_HOST_CONFIG_KEY]: shared.host,
|
|
270
359
|
[coordinates.PLATFORM_HOST_CONFIG_KEY]: platformHost.host,
|
|
360
|
+
// The policy row, through the same ladder the load gate reads, so a
|
|
361
|
+
// profile value that won is not reported as overridden by null.
|
|
362
|
+
[orgProfile.VAULT_REQUIRED_CONFIG_KEY]: String(orgProfile.resolveVaultRequired(cwd).required),
|
|
271
363
|
};
|
|
272
364
|
// Build `effective`'s key list from ORG_PROFILE_ALLOWLIST rather than a
|
|
273
365
|
// fresh literal array, so a future allowlist change surfaces here
|
|
@@ -334,11 +426,378 @@ function routeVaultCommand({ args, cwd, raw, error, _vault, _core, _practices, _
|
|
|
334
426
|
c.output(report, true, renderOrgProfileReport(report));
|
|
335
427
|
}
|
|
336
428
|
}
|
|
429
|
+
else if (subcommand === 'trace') {
|
|
430
|
+
routeTrace({ args, cwd, raw, error, vault, c, _exec, _trace });
|
|
431
|
+
}
|
|
432
|
+
else if (subcommand === 'audit') {
|
|
433
|
+
routeAudit({ args, cwd, raw, error, vault, c, _exec, _audit });
|
|
434
|
+
}
|
|
435
|
+
else if (subcommand === 'review') {
|
|
436
|
+
routeReview({ args, cwd, raw, c });
|
|
437
|
+
}
|
|
438
|
+
else if (subcommand === 'update') {
|
|
439
|
+
// `vault update [<tag>] [--yes]`: without --yes it reports the plan and
|
|
440
|
+
// writes nothing; see src/vault-update.cts for the contract.
|
|
441
|
+
const RECOGNIZED_UPDATE_FLAGS = new Set(['--raw', '--yes']);
|
|
442
|
+
const unrecognizedUpdateFlag = args.slice(2).find((a) => a.startsWith('--') && !RECOGNIZED_UPDATE_FLAGS.has(a));
|
|
443
|
+
if (unrecognizedUpdateFlag !== undefined) {
|
|
444
|
+
error(`Unrecognized flag "${boundedPath(unrecognizedUpdateFlag)}" for vault update. Supported: --yes, --raw.`, ERROR_REASON.SDK_FAIL_FAST);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
const updatePositionals = args.slice(2).filter((a) => !a.startsWith('--'));
|
|
448
|
+
if (updatePositionals.length > 1) {
|
|
449
|
+
error('vault update takes at most one tag, e.g. "pp-tools vault update v1.4.0".', ERROR_REASON.SDK_FAIL_FAST);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
|
|
453
|
+
const updateMod = _update ?? require('./vault-update.cjs');
|
|
454
|
+
const updateResult = updateMod.vaultUpdate(cwd, {
|
|
455
|
+
target: updatePositionals[0] ?? null,
|
|
456
|
+
apply: args.includes('--yes'),
|
|
457
|
+
});
|
|
458
|
+
if (!updateResult.ok) {
|
|
459
|
+
error(updateResult.message, ERROR_REASON.SDK_FAIL_FAST);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
if (raw) {
|
|
463
|
+
c.output(updateResult, true, JSON.stringify(updateResult));
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
c.output(updateResult, true, updateMod.renderVaultUpdate(updateResult));
|
|
467
|
+
}
|
|
468
|
+
}
|
|
337
469
|
else {
|
|
338
470
|
error(`Unknown vault subcommand. Available: ${VAULT_SUBCOMMANDS.map((x) => x.name).join(', ')}`, ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
339
471
|
}
|
|
340
472
|
}
|
|
473
|
+
/**
|
|
474
|
+
* `vault trace scaffold|render` — the practice-trace producer.
|
|
475
|
+
*
|
|
476
|
+
* The CLI owns everything that must be PROVABLE — the pinned ref, the
|
|
477
|
+
* resolved sha, the practice-id set and the per-file digests — and the agent
|
|
478
|
+
* writes only the judgement half (`status`, `reason`, `tasks`). Provability
|
|
479
|
+
* cannot rest on an agent transcribing a forty-character sha.
|
|
480
|
+
*
|
|
481
|
+
* NEITHER ARM WRITES A FILE. The producer emits text the agent embeds in the
|
|
482
|
+
* single write it already performs. Patching a plan's frontmatter here would
|
|
483
|
+
* have to round-trip it through the shared serializer, which cannot re-emit
|
|
484
|
+
* nested object lists, so the same write would damage the plan's own
|
|
485
|
+
* must-haves block.
|
|
486
|
+
*/
|
|
487
|
+
function routeTrace({ args, cwd, raw, error, vault, c, _exec, _trace, }) {
|
|
488
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
489
|
+
const realVault = require('./vault.cjs');
|
|
490
|
+
const cfgGet = vault.cfgGet ? vault.cfgGet.bind(vault) : realVault.cfgGet;
|
|
491
|
+
const verb = args[2];
|
|
492
|
+
const traceArgs = args.slice(3);
|
|
493
|
+
if (verb !== 'scaffold' && verb !== 'render') {
|
|
494
|
+
error(`Unknown trace subcommand. Available: ${TRACE_VERBS.join(', ')}`, ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
// Inert unless both vault keys are on: no output at all, before any flag check.
|
|
498
|
+
if (!tracePracticesActive(cfgGet, cwd))
|
|
499
|
+
return;
|
|
500
|
+
if (verb === 'scaffold') {
|
|
501
|
+
const RECOGNIZED_SCAFFOLD_FLAGS = new Set(['--stage', '--plan', '--raw']);
|
|
502
|
+
const unrecognized = traceArgs.find((a) => a.startsWith('--') && !RECOGNIZED_SCAFFOLD_FLAGS.has(a));
|
|
503
|
+
if (unrecognized !== undefined) {
|
|
504
|
+
error(`Unrecognized flag "${unrecognized}" for vault trace scaffold. ` +
|
|
505
|
+
`Supported: positional file paths, --stage <${TRACE_STAGES.join('|')}>, --plan <id>, --raw.`, ERROR_REASON.SDK_FAIL_FAST);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const valueIndices = flagValueIndices(traceArgs, ['--stage', '--plan']);
|
|
509
|
+
const stageIdx = traceArgs.indexOf('--stage');
|
|
510
|
+
const stage = stageIdx !== -1 ? traceArgs[stageIdx + 1] : undefined;
|
|
511
|
+
if (stage === undefined || !TRACE_STAGES.includes(stage)) {
|
|
512
|
+
error(`Missing or unrecognized --stage value for vault trace scaffold. Supported: ${TRACE_STAGES.join(', ')}.`, ERROR_REASON.SDK_FAIL_FAST);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
const planIdx = traceArgs.indexOf('--plan');
|
|
516
|
+
const planId = planIdx !== -1 ? traceArgs[planIdx + 1] : undefined;
|
|
517
|
+
if (planId === undefined || planId.startsWith('--')) {
|
|
518
|
+
error('No plan identifier was given. Usage: vault trace scaffold ' +
|
|
519
|
+
`--stage <${TRACE_STAGES.join('|')}> --plan <id> [files...] [--raw].`, ERROR_REASON.SDK_MISSING_ARG);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
if (!SCAFFOLD_PLAN_ID_RE.test(planId)) {
|
|
523
|
+
error(`Invalid --plan value "${boundedPath(planId)}" for vault trace scaffold. ${SCAFFOLD_PLAN_ID_FORM} ` +
|
|
524
|
+
'A trace gate compares the identifier a plan artifact records against the one its own filename states.', ERROR_REASON.SDK_FAIL_FAST);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const files = traceArgs.filter((a, i) => !a.startsWith('--') && !valueIndices.has(i));
|
|
528
|
+
// The execute stage records, per governed file, the commit that last
|
|
529
|
+
// produced it, so the statement asserts something falsifiable. A file with
|
|
530
|
+
// no commit yields no digest — the leaf records that gap as a loud note; a
|
|
531
|
+
// HEAD fallback would paper over a real coverage hole.
|
|
532
|
+
const runGit = _exec ?? shell_command_projection_cjs_1.execGit;
|
|
533
|
+
const fileInputs = files.map((name) => {
|
|
534
|
+
if (stage !== 'execute')
|
|
535
|
+
return name;
|
|
536
|
+
const res = runGit(['log', '-1', '--format=%H', '--', name], { cwd });
|
|
537
|
+
const digest = res.exitCode === 0 ? res.stdout.trim() : '';
|
|
538
|
+
return GIT_COMMIT_DIGEST_RE.test(digest) ? { name, gitCommit: digest } : name;
|
|
539
|
+
});
|
|
540
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
|
|
541
|
+
const traceMod = _trace ?? require('./vault-trace.cjs');
|
|
542
|
+
// No files is a REAL path: a documentation-only plan is still governed by
|
|
543
|
+
// the global practices, and its ruled-out set must stay auditable.
|
|
544
|
+
const statement = traceMod.buildTraceStatement({ cwd, stage, plan: planId, files: fileInputs });
|
|
545
|
+
if (raw) {
|
|
546
|
+
c.output(statement, true, JSON.stringify(statement));
|
|
547
|
+
}
|
|
548
|
+
else {
|
|
549
|
+
c.output(statement, true, traceMod.serializeTraceBlock(statement));
|
|
550
|
+
}
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
// ── render ──
|
|
554
|
+
const RECOGNIZED_RENDER_FLAGS = new Set(['--raw']);
|
|
555
|
+
const unrecognizedRender = traceArgs.find((a) => a.startsWith('--') && !RECOGNIZED_RENDER_FLAGS.has(a));
|
|
556
|
+
if (unrecognizedRender !== undefined) {
|
|
557
|
+
error(`Unrecognized flag "${unrecognizedRender}" for vault trace render. Supported: a positional artifact path, --raw.`, ERROR_REASON.SDK_FAIL_FAST);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
const artifactPath = traceArgs.find((a) => !a.startsWith('--'));
|
|
561
|
+
if (artifactPath === undefined) {
|
|
562
|
+
error('No artifact path was given. Usage: vault trace render <artifact> [--raw].', ERROR_REASON.SDK_MISSING_ARG);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
// CONTAINMENT, decided BEFORE the read (the bounded reader returns '' on any
|
|
566
|
+
// problem and never throws, so a decision taken on its result would be none).
|
|
567
|
+
// The rendered section is included verbatim in SUMMARY.md, so an out-of-tree
|
|
568
|
+
// file must never become its content. The predicate is the real leaf's, not
|
|
569
|
+
// the `_vault` seam's: a security decision is not switchable by a mock.
|
|
570
|
+
if (artifactPath.indexOf('\u0000') !== -1) {
|
|
571
|
+
error(`The artifact path is not a usable path: ${boundedPath(artifactPath)}`, ERROR_REASON.SDK_FAIL_FAST);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
let projectRoot;
|
|
575
|
+
try {
|
|
576
|
+
projectRoot = findProjectRoot(cwd) || cwd;
|
|
577
|
+
}
|
|
578
|
+
catch {
|
|
579
|
+
projectRoot = cwd;
|
|
580
|
+
}
|
|
581
|
+
const artifactAbs = node_path_1.default.resolve(cwd, artifactPath);
|
|
582
|
+
if (!realVault.isInsideDir(projectRoot, artifactAbs)) {
|
|
583
|
+
error(`The artifact path resolves outside the project and was not read: ${boundedPath(artifactPath)}`, ERROR_REASON.SDK_FAIL_FAST);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
|
|
587
|
+
const traceMod = _trace ?? require('./vault-trace.cjs');
|
|
588
|
+
const readHead = vault.readHeadBounded ? vault.readHeadBounded.bind(vault) : realVault.readHeadBounded;
|
|
589
|
+
const content = readHead(artifactAbs, TRACE_ARTIFACT_READ_BYTES);
|
|
590
|
+
const parsed = traceMod.parseVaultTraceBlock(content, artifactPath);
|
|
591
|
+
// THE LEDGER, AND ONLY THE LEDGER: this arm never calls the matcher, so the
|
|
592
|
+
// rendered section cannot disagree with the frontmatter it is generated
|
|
593
|
+
// from. An absent ledger yields no section; an empty one yields the heading
|
|
594
|
+
// plus one explanatory line (the leaf's renderer).
|
|
595
|
+
const rendered = parsed.present ? traceMod.renderPracticesApplied(parsed.statement) : '';
|
|
596
|
+
if (raw) {
|
|
597
|
+
c.output(parsed, true, JSON.stringify(parsed));
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
c.output(parsed, true, rendered);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* `vault audit <phase> [--range <from>..<to>] [--raw]` — the operator's view of
|
|
605
|
+
* the mechanical practice audit.
|
|
606
|
+
*
|
|
607
|
+
* A PASS-THROUGH: it collects arguments, calls `runVaultAudit` and prints the
|
|
608
|
+
* leaf's own envelope (`--raw`) or the leaf's own report. `check vault.audit`
|
|
609
|
+
* serializes the same envelope from the same engine, and the phase argument is
|
|
610
|
+
* resolved by the same `resolveDecisionPhaseDir`, so the report cannot disagree
|
|
611
|
+
* with the gate that blocks on it.
|
|
612
|
+
*
|
|
613
|
+
* Inert (no output at all) unless both vault keys are on, decided before the
|
|
614
|
+
* flag allowlist so an inert project is silent on every path. A malformed
|
|
615
|
+
* `--range` is refused by the leaf's own predicate and never echoed. The
|
|
616
|
+
* range's value is excluded by index, so the flag may precede the phase.
|
|
617
|
+
*/
|
|
618
|
+
function routeAudit({ args, cwd, raw, error, vault, c, _exec, _audit, }) {
|
|
619
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
620
|
+
const realVault = require('./vault.cjs');
|
|
621
|
+
const cfgGet = vault.cfgGet ? vault.cfgGet.bind(vault) : realVault.cfgGet;
|
|
622
|
+
if (!tracePracticesActive(cfgGet, cwd))
|
|
623
|
+
return;
|
|
624
|
+
const auditArgs = args.slice(2);
|
|
625
|
+
const RECOGNIZED_AUDIT_FLAGS = new Set(['--raw', '--range']);
|
|
626
|
+
const unrecognized = auditArgs.find((a) => a.startsWith('--') && !RECOGNIZED_AUDIT_FLAGS.has(a));
|
|
627
|
+
if (unrecognized !== undefined) {
|
|
628
|
+
error(`Unrecognized flag "${boundedPath(unrecognized)}" for vault audit. Supported: a phase directory or number, --raw, --range <from>..<to>.`, ERROR_REASON.SDK_FAIL_FAST);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
|
|
632
|
+
const auditMod = _audit ?? require('./vault-audit.cjs');
|
|
633
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
634
|
+
const checkRouter = require('./check-command-router.cjs');
|
|
635
|
+
const rangeAt = auditArgs.indexOf('--range');
|
|
636
|
+
const rangeValue = rangeAt === -1 ? undefined : auditArgs[rangeAt + 1];
|
|
637
|
+
if (rangeAt !== -1) {
|
|
638
|
+
// The real leaf's predicate: a range check is not switchable by a test mock.
|
|
639
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
640
|
+
const leaf = require('./vault-audit.cjs');
|
|
641
|
+
if (typeof rangeValue !== 'string' || !leaf.isValidOperatorRange(rangeValue)) {
|
|
642
|
+
error(checkRouter.AUDIT_RANGE_REFUSED_MESSAGE, ERROR_REASON.SDK_FAIL_FAST);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
const phaseArg = auditArgs.find((a, i) => !a.startsWith('--') && !(rangeAt !== -1 && i === rangeAt + 1));
|
|
647
|
+
if (phaseArg === undefined) {
|
|
648
|
+
error('No phase directory or phase number was given. Usage: vault audit <phase-dir|phase-number> [--range <from>..<to>] [--raw].', ERROR_REASON.SDK_MISSING_ARG);
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
let root;
|
|
652
|
+
try {
|
|
653
|
+
root = findProjectRoot(cwd) || cwd;
|
|
654
|
+
}
|
|
655
|
+
catch {
|
|
656
|
+
root = cwd;
|
|
657
|
+
}
|
|
658
|
+
const phase = checkRouter.resolveDecisionPhaseDir(root, cwd, phaseArg);
|
|
659
|
+
if (!phase) {
|
|
660
|
+
const unresolved = {
|
|
661
|
+
status: 'unresolved',
|
|
662
|
+
block: true,
|
|
663
|
+
message: `Vault audit: "${boundedPath(phaseArg)}" is neither an existing directory inside the project nor a `
|
|
664
|
+
+ 'phase the locator resolves, so no practice was compared against anything. Repair: re-run this '
|
|
665
|
+
+ 'command with the phase directory or number.',
|
|
666
|
+
};
|
|
667
|
+
c.output(unresolved, true, raw ? JSON.stringify(unresolved) : unresolved.message);
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
const result = auditMod.runVaultAudit({
|
|
671
|
+
root,
|
|
672
|
+
phaseDir: phase.abs,
|
|
673
|
+
exec: _exec ?? shell_command_projection_cjs_1.execGit,
|
|
674
|
+
...(typeof rangeValue === 'string' ? { range: rangeValue } : {}),
|
|
675
|
+
});
|
|
676
|
+
// The leaf's object, untouched: the operator view and the gate serialize the
|
|
677
|
+
// same envelope, and nothing between the leaf and the screen rebuilds it.
|
|
678
|
+
if (raw) {
|
|
679
|
+
c.output(result, true, JSON.stringify(result));
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
c.output(result, true, auditMod.renderAuditReport(result));
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
const REVIEW_VERBS = ['scaffold', 'finalize'];
|
|
686
|
+
const REVIEW_USAGE = 'vault review scaffold|finalize <phase> [--force] [--raw]';
|
|
687
|
+
/** The artifact a review writes in its phase directory: `<NN>-VAULT-REVIEW.md`. */
|
|
688
|
+
function reviewArtifactName(phaseToken) {
|
|
689
|
+
return `${phaseToken ? `${phaseToken}-` : ''}VAULT-REVIEW.md`;
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* `vault review scaffold|finalize <phase> [--force] [--raw]` — the artifact half
|
|
693
|
+
* of `/pp-vault-review`. The derivation is `check vault-review`'s, through the
|
|
694
|
+
* same exported resolver and composer, so all three see one phase and one
|
|
695
|
+
* payload.
|
|
696
|
+
*
|
|
697
|
+
* - `scaffold` writes `<phase-dir>/<NN>-VAULT-REVIEW.md` from the payload:
|
|
698
|
+
* every computed section, every derived row, one `TODO` row per practice
|
|
699
|
+
* the agent must judge, and the three computed lines. It does not replace
|
|
700
|
+
* an existing review unless `--force` is given.
|
|
701
|
+
* - `finalize` re-stamps the rate, counts and coverage lines from the rows the
|
|
702
|
+
* agent wrote (the only writer of those lines), writes the file back when
|
|
703
|
+
* that changed it, and reports every problem: the validator's (a `TODO`
|
|
704
|
+
* left, a missing reason, a reclassified derived row, a score) and the
|
|
705
|
+
* population's (a practice dropped, added or duplicated).
|
|
706
|
+
*
|
|
707
|
+
* Exit 0 on every path and no `block` key on any envelope: the review reports
|
|
708
|
+
* and never gates, so a problem is a repair item, never a halt.
|
|
709
|
+
*/
|
|
710
|
+
function routeReview({ args, cwd, raw, c, }) {
|
|
711
|
+
const say = (envelope, text) => {
|
|
712
|
+
c.output(envelope, true, raw ? JSON.stringify(envelope) : text);
|
|
713
|
+
};
|
|
714
|
+
const verb = args[2];
|
|
715
|
+
if (!REVIEW_VERBS.includes(verb)) {
|
|
716
|
+
const statement = `Vault review: "${boundedPath(typeof verb === 'string' ? verb : '')}" is not a review verb, so nothing was ` +
|
|
717
|
+
`reviewed. Usage: ${REVIEW_USAGE}.`;
|
|
718
|
+
say({ status: 'usage', statement }, statement);
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
const force = verb === 'scaffold' && args.includes('--force');
|
|
722
|
+
const tokens = args.slice(3).filter((a) => a !== '--raw' && !(force && a === '--force'));
|
|
723
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
724
|
+
const checkRouter = require('./check-command-router.cjs');
|
|
725
|
+
const target = checkRouter.resolveVaultReviewTarget(cwd, tokens, REVIEW_USAGE);
|
|
726
|
+
if (!target.ok) {
|
|
727
|
+
say({ status: target.status, statement: target.statement }, target.statement);
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
731
|
+
const leaf = require('./vault-review.cjs');
|
|
732
|
+
const payload = checkRouter.composeVaultReviewPayload(target.root, target.phase, target.phaseToken);
|
|
733
|
+
const name = reviewArtifactName(target.phaseToken);
|
|
734
|
+
const file = `${target.phase.rel}/${name}`;
|
|
735
|
+
const abs = node_path_1.default.join(target.phase.abs, name);
|
|
736
|
+
const partition = (payload.partition ?? {});
|
|
737
|
+
const coverage = typeof payload.mechanicalCoverageLine === 'string' ? payload.mechanicalCoverageLine : null;
|
|
738
|
+
if (verb === 'scaffold') {
|
|
739
|
+
if (node_fs_1.default.existsSync(abs) && !force) {
|
|
740
|
+
const statement = `Vault review: ${file} already exists, so it was left as it is. Re-run with --force to replace it ` +
|
|
741
|
+
'with a fresh scaffold, or run vault review finalize to check it.';
|
|
742
|
+
say({ status: 'exists', file, statement }, statement);
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
const text = leaf.renderReviewScaffold(payload, {
|
|
746
|
+
date: new Date().toISOString().slice(0, 10),
|
|
747
|
+
phaseToken: target.phaseToken,
|
|
748
|
+
});
|
|
749
|
+
node_fs_1.default.writeFileSync(abs, text, 'utf-8');
|
|
750
|
+
const toJudge = Array.isArray(partition.remainder) ? partition.remainder.length : 0;
|
|
751
|
+
const derived = (Array.isArray(partition.naUniverse) ? partition.naUniverse.length : 0) +
|
|
752
|
+
(Array.isArray(partition.naNoFile) ? partition.naNoFile.length : 0);
|
|
753
|
+
const statement = `Vault review: wrote ${file} (arm: ${String(payload.arm)}; ${toJudge} practice(s) to judge, ` +
|
|
754
|
+
`${derived} derived N-A${payload.refusesCleanRender === true ? '; no clean review may be rendered from it' : ''}).`;
|
|
755
|
+
say({ status: 'written', file, arm: payload.arm, toJudge, derived, refusesCleanRender: payload.refusesCleanRender === true, statement }, statement);
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
// finalize
|
|
759
|
+
let current;
|
|
760
|
+
try {
|
|
761
|
+
current = node_fs_1.default.readFileSync(abs, 'utf-8');
|
|
762
|
+
}
|
|
763
|
+
catch {
|
|
764
|
+
const statement = `Vault review: ${file} does not exist, so there is nothing to finalize. Run vault review scaffold first.`;
|
|
765
|
+
say({ status: 'absent', file, statement }, statement);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
const stamped = leaf.stampReviewLines(current, { coverageLine: coverage });
|
|
769
|
+
if (stamped.stamped && stamped.text !== current)
|
|
770
|
+
node_fs_1.default.writeFileSync(abs, stamped.text, 'utf-8');
|
|
771
|
+
const result = leaf.validateReviewArtifact(stamped.text, { expectedCoverageLine: coverage });
|
|
772
|
+
const problems = [...result.problems, ...leaf.compareReviewPopulation(result.rows, partition)];
|
|
773
|
+
const envelope = {
|
|
774
|
+
status: problems.length === 0 ? 'valid' : 'problems',
|
|
775
|
+
file,
|
|
776
|
+
rateLine: result.expectedRateLine,
|
|
777
|
+
countsLine: result.expectedCountsLine,
|
|
778
|
+
problems,
|
|
779
|
+
};
|
|
780
|
+
const text = [
|
|
781
|
+
`Vault review: ${file}`,
|
|
782
|
+
result.expectedRateLine,
|
|
783
|
+
result.expectedCountsLine,
|
|
784
|
+
...(problems.length === 0
|
|
785
|
+
? ['No repair items.']
|
|
786
|
+
: [
|
|
787
|
+
`Repair items (${problems.length}) — fix them in the artifact, then run vault review finalize again:`,
|
|
788
|
+
...problems.map((p) => `- ${p.kind}${p.id ? ` (${p.id})` : ''}${p.line ? `, line ${p.line}` : ''}: ${p.detail}`),
|
|
789
|
+
]),
|
|
790
|
+
'This review reports and does not gate.',
|
|
791
|
+
].join('\n');
|
|
792
|
+
say(envelope, text);
|
|
793
|
+
}
|
|
341
794
|
module.exports = {
|
|
342
795
|
routeVaultCommand,
|
|
343
796
|
VAULT_SUBCOMMANDS,
|
|
797
|
+
// The scaffold's plan-identifier shape and its one wording, exported so a
|
|
798
|
+
// trace gate's filename extractor and the planner reference are checked
|
|
799
|
+
// against the running module rather than a restated copy.
|
|
800
|
+
SCAFFOLD_PLAN_ID_RE,
|
|
801
|
+
SCAFFOLD_PLAN_ID_FORM,
|
|
802
|
+
TRACE_STAGES,
|
|
344
803
|
};
|