@pareto4/pp-core 1.12.0 → 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-activation.cjs +19 -9
- 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/config-loader.cjs +151 -8
- 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 +111 -35
- 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 +313 -11
- 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 +20 -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/audit-milestone.md +1 -1
- package/pp-core/workflows/autonomous.md +3 -3
- package/pp-core/workflows/check-todos.md +1 -1
- package/pp-core/workflows/code-review-fix.md +3 -1
- package/pp-core/workflows/code-review.md +1 -1
- 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/discuss-phase.md +2 -2
- 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 +29 -12
- 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 +13 -10
- package/pp-core/workflows/quick.md +5 -1
- package/pp-core/workflows/secure-phase.md +1 -1
- package/pp-core/workflows/ship.md +7 -3
- package/pp-core/workflows/undo.md +1 -1
- package/pp-core/workflows/validate-phase.md +5 -1
- package/pp-core/workflows/vault-review.md +65 -0
- package/pp-core/workflows/verify-work.md +6 -3
- 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
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* src/vault-update.cts — `pp-tools vault update [<tag>] [--yes]`.
|
|
4
|
+
*
|
|
5
|
+
* Moves a project to another version of its client vault. The
|
|
6
|
+
* vault version is a PROJECT property: `client.vaultRef` in the committed
|
|
7
|
+
* `.platform-bindings.json` is the one place it lives, so no pp-core release
|
|
8
|
+
* can move it — changing it is a deliberate act, and this is its command.
|
|
9
|
+
*
|
|
10
|
+
* Distinct from its neighbours:
|
|
11
|
+
* - `pp-tools vault fetch` re-reads the vault at the version the project is
|
|
12
|
+
* already pinned to; it never changes `client.vaultRef`.
|
|
13
|
+
* - `/pp-update` updates pp-core itself, not a project's vault.
|
|
14
|
+
*
|
|
15
|
+
* Two steps, so the confirmation is explicit and works the same for a person
|
|
16
|
+
* and for an agent shell: without `--yes` it only reports the plan
|
|
17
|
+
* (current → target) and writes nothing; with `--yes` it writes
|
|
18
|
+
* `client.vaultRef`, fetches the vault at the new version, and reports any
|
|
19
|
+
* practice whose `schemaVersion` is newer than this pp-core supports (the
|
|
20
|
+
* vault version a project picks implies a minimum pp-core, never the reverse).
|
|
21
|
+
* It never commits: the command it prints is the operator's to run.
|
|
22
|
+
*
|
|
23
|
+
* With no `<tag>` it proposes the newest stable `vX.Y.Z` tag of the configured
|
|
24
|
+
* vault repository, listed with `git ls-remote` at the same clone target the
|
|
25
|
+
* fetch uses (`resolveClientCoordinates` + `composeCloneTarget`). Nothing here
|
|
26
|
+
* reads an org profile for a version: the vault version is the project's.
|
|
27
|
+
*
|
|
28
|
+
* Every repository coordinate it prints goes through `redactCloneTarget`, and
|
|
29
|
+
* every value it echoes from the bindings file or the command line is
|
|
30
|
+
* bounded to one short line.
|
|
31
|
+
*
|
|
32
|
+
* ADR-457 build-at-publish: this TypeScript file is the SOURCE OF TRUTH. It
|
|
33
|
+
* compiles to the git-ignored `pp-core/bin/lib/vault-update.cjs`.
|
|
34
|
+
*/
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.AVAILABLE_TAGS_CAP = void 0;
|
|
40
|
+
exports.vaultUpdate = vaultUpdate;
|
|
41
|
+
exports.renderVaultUpdate = renderVaultUpdate;
|
|
42
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
43
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
44
|
+
const project_root_cjs_1 = require("./project-root.cjs");
|
|
45
|
+
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
46
|
+
const semver_compare_cjs_1 = require("./semver-compare.cjs");
|
|
47
|
+
const compose_bindings_cjs_1 = require("./compose-bindings.cjs");
|
|
48
|
+
const git_provider_cjs_1 = require("./git-provider.cjs");
|
|
49
|
+
const vault_coordinates_cjs_1 = require("./vault-coordinates.cjs");
|
|
50
|
+
const platform_bindings_cjs_1 = require("./platform-bindings.cjs");
|
|
51
|
+
// The one coordinate redactor. Cycle-free — src/vault.cts imports nothing from
|
|
52
|
+
// this module; the lazy `require` in runFetch exists for the `_fetch` seam default.
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
54
|
+
const vaultModule = require("./vault.cjs");
|
|
55
|
+
const { redactCloneTarget } = vaultModule;
|
|
56
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
57
|
+
/** How many tags a report lists — enough to pick from, bounded for a long history. */
|
|
58
|
+
exports.AVAILABLE_TAGS_CAP = 10;
|
|
59
|
+
const BINDINGS_FILE = '.platform-bindings.json';
|
|
60
|
+
/** Matches the degrade warning `parsePracticeFile` emits for a newer schemaVersion. */
|
|
61
|
+
const NEWER_SCHEMA_WARNING_RE = /schemaVersion \d+ is newer than this pp-core supports/;
|
|
62
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
63
|
+
function isObject(v) {
|
|
64
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
65
|
+
}
|
|
66
|
+
function usableString(v) {
|
|
67
|
+
return typeof v === 'string' && v.trim() !== '' ? v.trim() : null;
|
|
68
|
+
}
|
|
69
|
+
/** A value from the bindings file or the command line, made safe to print: one line, at most 80 characters. */
|
|
70
|
+
function shown(v) {
|
|
71
|
+
const flat = v.replace(/[\u0000-\u001F\u007F]+/g, ' ').trim();
|
|
72
|
+
return flat.length > 80 ? `${flat.slice(0, 80)}…` : flat;
|
|
73
|
+
}
|
|
74
|
+
function resolveRoot(cwd) {
|
|
75
|
+
try {
|
|
76
|
+
return (0, project_root_cjs_1.findProjectRoot)(cwd) || cwd;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return cwd;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** Resolve a requested tag against the available list; accepts a missing `v`. */
|
|
83
|
+
function matchRequestedTag(requested, tags) {
|
|
84
|
+
const trimmed = requested.trim();
|
|
85
|
+
if (tags.includes(trimmed))
|
|
86
|
+
return trimmed;
|
|
87
|
+
if (!trimmed.startsWith('v') && tags.includes(`v${trimmed}`))
|
|
88
|
+
return `v${trimmed}`;
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
function runFetch(root, opts) {
|
|
92
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
93
|
+
const fetchFn = opts._fetch ?? require('./vault.cjs').vaultFetch;
|
|
94
|
+
const manifest = fetchFn(root);
|
|
95
|
+
const warnings = [];
|
|
96
|
+
if (typeof manifest.warning === 'string')
|
|
97
|
+
warnings.push(manifest.warning);
|
|
98
|
+
if (Array.isArray(manifest.warnings)) {
|
|
99
|
+
for (const w of manifest.warnings)
|
|
100
|
+
if (typeof w === 'string' && !warnings.includes(w))
|
|
101
|
+
warnings.push(w);
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
source: typeof manifest.source === 'string' ? manifest.source : 'unknown',
|
|
105
|
+
ref: typeof manifest.ref === 'string' ? manifest.ref : null,
|
|
106
|
+
tier: typeof manifest.tier === 'string' ? manifest.tier : null,
|
|
107
|
+
warnings,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function findNewerSchemaPractices(root, opts) {
|
|
111
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
112
|
+
const discover = opts._discover ?? require('./vault-practices.cjs').discoverPracticeFiles;
|
|
113
|
+
const discovery = discover(root);
|
|
114
|
+
const ids = [];
|
|
115
|
+
for (const entry of discovery.degraded ?? []) {
|
|
116
|
+
if (entry.warnings.some((w) => NEWER_SCHEMA_WARNING_RE.test(w)) && !ids.includes(entry.id))
|
|
117
|
+
ids.push(entry.id);
|
|
118
|
+
}
|
|
119
|
+
return ids;
|
|
120
|
+
}
|
|
121
|
+
// ─── vaultUpdate ─────────────────────────────────────────────────────────────
|
|
122
|
+
/**
|
|
123
|
+
* Plan — and with `apply` perform — a move of the project's client vault to
|
|
124
|
+
* another stable tag. Never throws: every fault is a result with `ok: false`
|
|
125
|
+
* and a `reason`. Writes exactly one field (`client.vaultRef`) of an existing
|
|
126
|
+
* `.platform-bindings.json`, and only after the whole document still passes
|
|
127
|
+
* `validateBindings`.
|
|
128
|
+
*/
|
|
129
|
+
function vaultUpdate(cwd, opts = {}) {
|
|
130
|
+
const root = resolveRoot(cwd);
|
|
131
|
+
const bindingsPath = node_path_1.default.join(root, BINDINGS_FILE);
|
|
132
|
+
const result = {
|
|
133
|
+
ok: false,
|
|
134
|
+
applied: false,
|
|
135
|
+
reason: null,
|
|
136
|
+
message: '',
|
|
137
|
+
bindingsPath,
|
|
138
|
+
repo: null,
|
|
139
|
+
from: null,
|
|
140
|
+
to: null,
|
|
141
|
+
latest: null,
|
|
142
|
+
available: [],
|
|
143
|
+
fetch: null,
|
|
144
|
+
newerSchemaPractices: [],
|
|
145
|
+
nextStep: null,
|
|
146
|
+
};
|
|
147
|
+
const fail = (reason, message) => ({
|
|
148
|
+
...result,
|
|
149
|
+
ok: false,
|
|
150
|
+
reason,
|
|
151
|
+
message,
|
|
152
|
+
});
|
|
153
|
+
if (!node_fs_1.default.existsSync(bindingsPath)) {
|
|
154
|
+
return fail('no_bindings', `No ${BINDINGS_FILE} in ${root} — run "pp-tools init" first; the vault version lives there.`);
|
|
155
|
+
}
|
|
156
|
+
let bindings;
|
|
157
|
+
try {
|
|
158
|
+
const parsed = JSON.parse(node_fs_1.default.readFileSync(bindingsPath, 'utf-8'));
|
|
159
|
+
if (!isObject(parsed))
|
|
160
|
+
throw new Error('not an object');
|
|
161
|
+
bindings = parsed;
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return fail('bindings_unreadable', `${BINDINGS_FILE} could not be read as a JSON object — fix it before updating the vault.`);
|
|
165
|
+
}
|
|
166
|
+
const client = isObject(bindings.client) ? bindings.client : null;
|
|
167
|
+
const repo = client ? usableString(client.vaultRepo) : null;
|
|
168
|
+
if (!client || !repo) {
|
|
169
|
+
return fail('no_client_vault', `${BINDINGS_FILE} names no client vault (client.vaultRepo) — there is no vault version to update.`);
|
|
170
|
+
}
|
|
171
|
+
result.repo = redactCloneTarget(repo);
|
|
172
|
+
result.from = usableString(client.vaultRef);
|
|
173
|
+
let tags;
|
|
174
|
+
try {
|
|
175
|
+
const provider = (0, git_provider_cjs_1.resolveProvider)(root).provider;
|
|
176
|
+
// The same host chain the fetch uses (client.vaultHost, platform host, provider default).
|
|
177
|
+
const host = (0, vault_coordinates_cjs_1.resolveClientCoordinates)(root, provider).host;
|
|
178
|
+
const target = (0, git_provider_cjs_1.composeCloneTarget)(provider, repo, host);
|
|
179
|
+
tags = (0, compose_bindings_cjs_1.listStableTagsAt)(target.url, { _lsRemote: opts._lsRemote });
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return fail('tag_lookup_failed', `The tags of the vault repo "${redactCloneTarget(repo)}" could not be listed — check your network and git access to it.`);
|
|
183
|
+
}
|
|
184
|
+
tags = [...new Set(tags)].sort((a, b) => (0, semver_compare_cjs_1.compareSemverCore)(b, a));
|
|
185
|
+
result.available = tags.slice(0, exports.AVAILABLE_TAGS_CAP);
|
|
186
|
+
result.latest = tags[0] ?? null;
|
|
187
|
+
if (tags.length === 0) {
|
|
188
|
+
return fail('no_tags', `The vault repo "${redactCloneTarget(repo)}" has no stable vX.Y.Z tag to pin to.`);
|
|
189
|
+
}
|
|
190
|
+
const requested = usableString(opts.target);
|
|
191
|
+
const to = requested ? matchRequestedTag(requested, tags) : tags[0];
|
|
192
|
+
if (!to) {
|
|
193
|
+
return fail('unknown_tag', `The vault repo has no tag "${shown(requested ?? "")}". Available (newest first): ${result.available.join(', ')}.`);
|
|
194
|
+
}
|
|
195
|
+
result.to = to;
|
|
196
|
+
const fromLabel = result.from === null ? 'main (unpinned)' : shown(result.from);
|
|
197
|
+
if (result.from === to) {
|
|
198
|
+
return { ...result, ok: true, message: `The project is already on vault ${to}.` };
|
|
199
|
+
}
|
|
200
|
+
if (opts.apply !== true) {
|
|
201
|
+
return {
|
|
202
|
+
...result,
|
|
203
|
+
ok: true,
|
|
204
|
+
message: `The project would move from vault ${fromLabel} to ${to}. Nothing was changed.`,
|
|
205
|
+
nextStep: `pp-tools vault update ${to} --yes`,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
const updated = { ...bindings, client: { ...client, vaultRef: to } };
|
|
209
|
+
const validation = (0, platform_bindings_cjs_1.validateBindings)(updated);
|
|
210
|
+
if (!validation.valid) {
|
|
211
|
+
return fail('invalid_bindings', `The updated ${BINDINGS_FILE} would not be valid: ${validation.errors.join('; ')}. Nothing was changed.`);
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
(0, shell_command_projection_cjs_1.platformWriteSync)(bindingsPath, JSON.stringify(updated, null, 2) + '\n');
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return fail('write_failed', `${BINDINGS_FILE} could not be written. Nothing was changed.`);
|
|
218
|
+
}
|
|
219
|
+
const applied = {
|
|
220
|
+
...result,
|
|
221
|
+
ok: true,
|
|
222
|
+
applied: true,
|
|
223
|
+
message: `The project moved from vault ${fromLabel} to ${to}.`,
|
|
224
|
+
nextStep: `git add ${BINDINGS_FILE} && git commit -m "chore(vault): update client vault to ${to}"`,
|
|
225
|
+
};
|
|
226
|
+
try {
|
|
227
|
+
applied.fetch = runFetch(root, opts);
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
applied.fetch = { source: 'error', ref: null, tier: null, warnings: ['The vault could not be fetched at the new version — run "pp-tools vault fetch" to retry.'] };
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
applied.newerSchemaPractices = findNewerSchemaPractices(root, opts);
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
applied.newerSchemaPractices = [];
|
|
237
|
+
}
|
|
238
|
+
return applied;
|
|
239
|
+
}
|
|
240
|
+
/** Plain-text rendering of a `vaultUpdate` result for the non-`--raw` CLI path. */
|
|
241
|
+
function renderVaultUpdate(r) {
|
|
242
|
+
const lines = [r.message];
|
|
243
|
+
if (r.ok && r.latest && r.to !== r.latest) {
|
|
244
|
+
lines.push(`Newest available: ${r.latest}.`);
|
|
245
|
+
}
|
|
246
|
+
if (r.fetch) {
|
|
247
|
+
if (r.fetch.tier !== null && r.fetch.tier !== 'client' && r.fetch.source !== 'error') {
|
|
248
|
+
// A fallback-tier fetch is not the client vault at the new version, and
|
|
249
|
+
// must not read as if it were.
|
|
250
|
+
lines.push(`The client vault at ${r.to ?? 'the new version'} was NOT loaded: the fetch fell back to the ${r.fetch.tier} tier` +
|
|
251
|
+
`${r.fetch.ref ? ` at ${r.fetch.ref}` : ''}. Check access to the client vault, then run "pp-tools vault fetch".`);
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
lines.push(`Fetched the vault: ${r.fetch.source}${r.fetch.ref ? ` at ${r.fetch.ref}` : ''}.`);
|
|
255
|
+
}
|
|
256
|
+
for (const w of r.fetch.warnings)
|
|
257
|
+
lines.push(w);
|
|
258
|
+
}
|
|
259
|
+
if (r.newerSchemaPractices.length > 0) {
|
|
260
|
+
lines.push(`${r.newerSchemaPractices.length} practice(s) in this vault version were written for a newer pp-core and apply only in degraded form: ${r.newerSchemaPractices.join(', ')}. Update pp-core to apply them fully.`);
|
|
261
|
+
}
|
|
262
|
+
if (r.nextStep) {
|
|
263
|
+
lines.push(r.applied ? `Commit the change: ${r.nextStep}` : `To apply: ${r.nextStep}`);
|
|
264
|
+
}
|
|
265
|
+
return lines.join('\n');
|
|
266
|
+
}
|
|
@@ -242,6 +242,25 @@ function promotePreflight({ cwd, filePath, _vault, _practices }) {
|
|
|
242
242
|
const cause = parsed.warning.replace(/^.*?\s+skipped:\s*/, '');
|
|
243
243
|
return refusal(PROMOTE_GUARD.VALIDATION, ERROR_REASON.VAULT_PROMOTE_INVALID_PRACTICE, `This practice was not promoted: ${cause} Fix the practice file and run the promotion again.`);
|
|
244
244
|
}
|
|
245
|
+
// 3b. Degraded-parse refusal. Write-side strictness is deliberate: the read
|
|
246
|
+
// side keeps a practice it does not fully understand, because it already
|
|
247
|
+
// governs work and dropping it would silently un-govern that work; promoting
|
|
248
|
+
// it would push the defect into the shared vault every machine fetches. Both
|
|
249
|
+
// consumers of DOCUMENTED_FRONTMATTER_KEYS move in the one change that
|
|
250
|
+
// widened it — had only the reader moved, an unknown key would have quietly
|
|
251
|
+
// become promotable.
|
|
252
|
+
if (parsed.degradedWarnings !== undefined && parsed.degradedWarnings.length > 0) {
|
|
253
|
+
const cause = parsed.degradedWarnings[0].replace(/^.*?\s+degraded:\s*/, '');
|
|
254
|
+
return refusal(PROMOTE_GUARD.VALIDATION, ERROR_REASON.VAULT_PROMOTE_INVALID_PRACTICE, `This practice was not promoted: ${cause} Fix the practice file and run the promotion again.`);
|
|
255
|
+
}
|
|
256
|
+
// 3c. Invalid-check-block refusal. The reader reports this defect on its own
|
|
257
|
+
// channel (`practice.checkInvalid`), which is neither `!parsed.ok` nor
|
|
258
|
+
// `degradedWarnings` — discovery folds it into `degraded[]`, but this path
|
|
259
|
+
// never calls discovery — so both rungs above miss it.
|
|
260
|
+
if (typeof parsed.practice.checkInvalid === 'string' && parsed.practice.checkInvalid.length > 0) {
|
|
261
|
+
return refusal(PROMOTE_GUARD.VALIDATION, ERROR_REASON.VAULT_PROMOTE_INVALID_PRACTICE, `This practice was not promoted: ${parsed.practice.checkInvalid} ` +
|
|
262
|
+
'Fix the practice file and run the promotion again.');
|
|
263
|
+
}
|
|
245
264
|
// 4. Secret scan — a hit refuses hard, no override (D-14). The matched
|
|
246
265
|
// text is never carried, quoted, echoed, logged, or truncated into the
|
|
247
266
|
// refusal — a truncated secret is still a partial disclosure.
|