@pareto4/pp-core 1.9.0 → 1.11.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-fixer.md +11 -0
- package/agents/pp-code-reviewer.md +77 -1
- package/agents/pp-executor.md +34 -1
- package/agents/pp-planner.md +24 -103
- package/commands/pp-ns-manage.md +2 -1
- package/commands/pp-vault-promote.md +31 -0
- package/gemini-extension.json +1 -1
- package/package.json +5 -3
- package/pareto-skills/pp-brand/SKILL.md +1 -1
- package/pp-core/bin/lib/capability-registry.cjs +160 -3
- package/pp-core/bin/lib/capability-state.cjs +41 -3
- package/pp-core/bin/lib/check-command-router.cjs +473 -3
- package/pp-core/bin/lib/clusters.cjs +3 -0
- package/pp-core/bin/lib/compose-bindings.cjs +736 -0
- package/pp-core/bin/lib/git-provider.cjs +279 -0
- package/pp-core/bin/lib/init-contract.cjs +989 -0
- package/pp-core/bin/lib/init-interactive.cjs +183 -0
- package/pp-core/bin/lib/io.cjs +9 -0
- package/pp-core/bin/lib/org-profile.cjs +696 -0
- package/pp-core/bin/lib/platform-bindings.cjs +268 -64
- package/pp-core/bin/lib/practices-review-check.cjs +938 -0
- package/pp-core/bin/lib/vault-command-router.cjs +239 -2
- package/pp-core/bin/lib/vault-coordinates.cjs +485 -0
- package/pp-core/bin/lib/vault-practices.cjs +982 -0
- package/pp-core/bin/lib/vault-write-back.cjs +842 -0
- package/pp-core/bin/lib/vault.cjs +170 -51
- package/pp-core/bin/pp-tools.cjs +225 -2
- package/pp-core/references/planner-checkpoints.md +72 -0
- package/pp-core/references/planner-tdd.md +55 -0
- package/pp-core/references/planner-vault-practices.md +67 -0
- package/pp-core/templates/practices/README.md +122 -0
- package/pp-core/templates/practices/error-handling.md +18 -0
- package/pp-core/templates/practices/minimal-comments.md +14 -0
- package/pp-core/templates/practices/naming-convention.md +18 -0
- package/pp-core/templates/practices/practice-template.md +23 -0
- package/pp-core/workflows/code-review.md +74 -18
- package/pp-core/workflows/discovery-sprint.md +1 -1
- package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +1 -1
- package/pp-core/workflows/init.md +137 -76
- package/pp-core/workflows/ship.md +1 -0
- package/pp-core/workflows/vault-write-back.md +213 -0
- package/scripts/gen-capability-registry.cjs +68 -8
- package/scripts/gen-platform-bindings-schema.cjs +148 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +8 -1
- package/scripts/lint-descriptions.cjs +3 -1
- package/scripts/lint-legacy-dir-name.cjs +5 -0
- package/scripts/lint-test-file-count.allowlist.json +10 -1
|
@@ -29,11 +29,62 @@
|
|
|
29
29
|
*/
|
|
30
30
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
31
31
|
const io = require("./io.cjs");
|
|
32
|
+
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
32
33
|
const { ERROR_REASON } = io;
|
|
33
34
|
// Default CoreModule implementation. _core seam overrides this for test injection.
|
|
34
35
|
const _defaultCore = { output: io.output };
|
|
36
|
+
/**
|
|
37
|
+
* Render a short plain-text `vault org-profile` report from its structured
|
|
38
|
+
* shape — one line naming the pointer, one naming the resolution, a
|
|
39
|
+
* contributes list (or a "contributes nothing" line), an overridden list
|
|
40
|
+
* (D-05 drift made observable) prefixed by the precedence sentence, the
|
|
41
|
+
* ignored/rejected key names when non-empty, and the warnings verbatim.
|
|
42
|
+
* Kept to plain sentences a person can read without knowing pp-core
|
|
43
|
+
* internals (Phase 33-04).
|
|
44
|
+
*/
|
|
45
|
+
function renderOrgProfileReport(report) {
|
|
46
|
+
const lines = [];
|
|
47
|
+
if (report.pointer.value === null) {
|
|
48
|
+
lines.push('No org profile is configured (org.profile is unset) — pp-core\'s built-in defaults apply.');
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
lines.push(`org.profile is set to "${report.pointer.value}".`);
|
|
52
|
+
}
|
|
53
|
+
if (report.profile.resolved) {
|
|
54
|
+
lines.push(`Resolved org profile "${report.profile.name}" version ${report.profile.version ?? 'unknown'} from ${report.profile.source}.`);
|
|
55
|
+
}
|
|
56
|
+
else if (report.pointer.value !== null) {
|
|
57
|
+
lines.push(`The org profile "${report.pointer.value}" could not be resolved from the project or the global npm root.`);
|
|
58
|
+
}
|
|
59
|
+
const contributesKeys = Object.keys(report.contributes);
|
|
60
|
+
if (contributesKeys.length === 0) {
|
|
61
|
+
lines.push('The org profile contributes nothing.');
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
lines.push('The org profile contributes:');
|
|
65
|
+
for (const key of contributesKeys) {
|
|
66
|
+
lines.push(` ${key} = ${report.contributes[key]}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (report.overridden.length > 0) {
|
|
70
|
+
lines.push('A value the project sets itself always wins over anything an org profile contributes:');
|
|
71
|
+
for (const entry of report.overridden) {
|
|
72
|
+
lines.push(` ${entry.key}: profile offered "${entry.profile}", but "${entry.effective}" won.`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (report.ignored.length > 0) {
|
|
76
|
+
lines.push(`Ignored (not on the allowlist): ${report.ignored.join(', ')}`);
|
|
77
|
+
}
|
|
78
|
+
if (report.rejected.length > 0) {
|
|
79
|
+
lines.push(`Rejected: ${report.rejected.join(', ')}`);
|
|
80
|
+
}
|
|
81
|
+
for (const warning of report.warnings) {
|
|
82
|
+
lines.push(warning);
|
|
83
|
+
}
|
|
84
|
+
return lines.join('\n');
|
|
85
|
+
}
|
|
35
86
|
// ─── Implementation ───────────────────────────────────────────────────────────
|
|
36
|
-
function routeVaultCommand({ args, cwd, raw, error, _vault, _core }) {
|
|
87
|
+
function routeVaultCommand({ args, cwd, raw, error, _vault, _core, _practices, _exec, _writeBack, _orgProfile, _coordinates, _provider, }) {
|
|
37
88
|
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
|
|
38
89
|
const vault = _vault ?? require('./vault.cjs');
|
|
39
90
|
const c = _core ?? _defaultCore;
|
|
@@ -47,8 +98,194 @@ function routeVaultCommand({ args, cwd, raw, error, _vault, _core }) {
|
|
|
47
98
|
else if (subcommand === 'summary') {
|
|
48
99
|
c.output(vault.vaultSummary(cwd), raw);
|
|
49
100
|
}
|
|
101
|
+
else if (subcommand === 'practices') {
|
|
102
|
+
const verb = args[2];
|
|
103
|
+
if (verb === 'match') {
|
|
104
|
+
// T-24-08b: reject any unrecognized `--`-prefixed argument BEFORE the
|
|
105
|
+
// `--changed` git call (so a typo never spawns a subprocess) instead
|
|
106
|
+
// of silently filtering it out of the positional file list. Matched on
|
|
107
|
+
// the WHOLE token so `--files=…` is caught by the same check as
|
|
108
|
+
// `--files value`. DECISION (recorded, not deferred): `--files=` is
|
|
109
|
+
// NOT implemented as a comma-splitting alias here, unlike
|
|
110
|
+
// `/pp-code-review`'s `--files=a,b` (pp-core/workflows/code-review.md).
|
|
111
|
+
// The auditor marked the alias "optional and secondary"; the required
|
|
112
|
+
// behavior is loud failure, and the error message below names the
|
|
113
|
+
// supported forms instead. The host (pp-core/bin/pp-tools.cjs) already
|
|
114
|
+
// splices out its own global flags (--json-errors/--cwd/--ws/--raw/
|
|
115
|
+
// --pick/--default) before dispatch, which is why the recognized set
|
|
116
|
+
// here is only two flags wide.
|
|
117
|
+
const RECOGNIZED_MATCH_FLAGS = new Set(['--changed', '--raw', '--block']);
|
|
118
|
+
const unrecognizedFlag = args.slice(3).find((a) => a.startsWith('--') && !RECOGNIZED_MATCH_FLAGS.has(a));
|
|
119
|
+
if (unrecognizedFlag !== undefined) {
|
|
120
|
+
error(`Unrecognized flag "${unrecognizedFlag}" for vault practices match. Supported: positional file paths, --changed, --raw, or --block.`, ERROR_REASON.SDK_FAIL_FAST);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
|
|
124
|
+
const practices = _practices ?? require('./vault-practices.cjs');
|
|
125
|
+
const runGit = _exec ?? shell_command_projection_cjs_1.execGit;
|
|
126
|
+
let files;
|
|
127
|
+
if (args.includes('--changed')) {
|
|
128
|
+
const gitResult = runGit(['diff', '--name-only', 'HEAD'], { cwd });
|
|
129
|
+
if (gitResult.exitCode !== 0) {
|
|
130
|
+
error('Could not determine changed files: git diff failed (WR-02)', ERROR_REASON.SDK_FAIL_FAST);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
files = gitResult.stdout.split('\n').map((f) => f.trim()).filter((f) => f.length > 0);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
files = args.slice(3).filter((a) => !a.startsWith('--'));
|
|
137
|
+
}
|
|
138
|
+
const result = practices.matchPractices({ cwd, files });
|
|
139
|
+
if (args.includes('--block')) {
|
|
140
|
+
// T-25-08: emit the finished <vault_practices> block so the planner
|
|
141
|
+
// embeds it VERBATIM. The planner never renames a field or applies a
|
|
142
|
+
// fence itself — that transformation used to be prose-only, which is
|
|
143
|
+
// what left the phase's prompt-injection mitigation unenforced.
|
|
144
|
+
c.output(result, true, practices.renderPracticesBlock(result));
|
|
145
|
+
}
|
|
146
|
+
else if (raw) {
|
|
147
|
+
c.output(result, true, JSON.stringify(result));
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
c.output(result, true, practices.renderPracticesReport(result));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else if (verb === 'promote') {
|
|
154
|
+
// T-29-16: reject any unrecognized `--`-prefixed argument BEFORE the
|
|
155
|
+
// lazy require of the write-back module and before any subprocess can
|
|
156
|
+
// spawn — mirrors the `match` branch's T-24-08b guard exactly. Matched
|
|
157
|
+
// on the WHOLE token so `--name=value` is caught by the same check as
|
|
158
|
+
// `--name value`. The recognized set holds exactly three flags — no
|
|
159
|
+
// flag that disables, bypasses, or relaxes any guard exists.
|
|
160
|
+
const RECOGNIZED_PROMOTE_FLAGS = new Set(['--confirmed', '--expect', '--raw']);
|
|
161
|
+
const promoteArgs = args.slice(3);
|
|
162
|
+
const unrecognizedPromoteFlag = promoteArgs.find((a) => a.startsWith('--') && !RECOGNIZED_PROMOTE_FLAGS.has(a));
|
|
163
|
+
if (unrecognizedPromoteFlag !== undefined) {
|
|
164
|
+
error(`Unrecognized flag "${unrecognizedPromoteFlag}" for vault practices promote. Supported: --confirmed, --expect <token>, --raw.`, ERROR_REASON.SDK_FAIL_FAST);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
// Positional file path: the first token that is not `--`-prefixed and
|
|
168
|
+
// is not the value immediately following `--expect`.
|
|
169
|
+
const expectFlagIdx = promoteArgs.indexOf('--expect');
|
|
170
|
+
const expectValueIdx = expectFlagIdx !== -1 ? expectFlagIdx + 1 : -1;
|
|
171
|
+
const filePath = promoteArgs.find((a, i) => !a.startsWith('--') && i !== expectValueIdx);
|
|
172
|
+
if (filePath === undefined) {
|
|
173
|
+
error('No practice file was given. Usage: vault practices promote <file> [--confirmed --expect <token>] [--raw].', ERROR_REASON.SDK_MISSING_ARG);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const confirmed = promoteArgs.includes('--confirmed');
|
|
177
|
+
const expect = expectValueIdx !== -1 ? promoteArgs[expectValueIdx] : undefined;
|
|
178
|
+
if (confirmed && (expect === undefined || expect.startsWith('--'))) {
|
|
179
|
+
error('This practice was not pushed: the confirmation is missing the preview token it needs. ' +
|
|
180
|
+
'Run the promotion without confirming first, then pass the token it prints.', ERROR_REASON.VAULT_PROMOTE_CONCURRENT_EDIT);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
|
|
184
|
+
const writeBack = _writeBack ?? require('./vault-write-back.cjs');
|
|
185
|
+
const verdict = writeBack.promotePractice({ cwd, filePath, confirmed, expect });
|
|
186
|
+
// Emit the structured verdict BEFORE any error call — a refusal still
|
|
187
|
+
// carries its verdict to stdout. This ordering is the whole point of
|
|
188
|
+
// the uniform D-16 refusal contract and is what lets a wrapper
|
|
189
|
+
// workflow render results without scraping prose.
|
|
190
|
+
if (raw) {
|
|
191
|
+
c.output(verdict, true, JSON.stringify(verdict));
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
c.output(verdict, true, writeBack.renderPromoteReport(verdict));
|
|
195
|
+
}
|
|
196
|
+
if (verdict.status === 'refused') {
|
|
197
|
+
// An action command's refusal must be observable in the exit code —
|
|
198
|
+
// do not adopt the ship gate's always-exit-zero convention here.
|
|
199
|
+
error(verdict.message ?? 'This practice was not promoted.', verdict.reason ?? ERROR_REASON.UNKNOWN);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
error('Unknown practices subcommand. Available: match, promote', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
else if (subcommand === 'org-profile') {
|
|
207
|
+
// T-33-13: reject any unrecognized `--`-prefixed argument BEFORE any
|
|
208
|
+
// lazy require or resolution work — mirrors the `practices match`
|
|
209
|
+
// T-24-08b guard exactly, so a typo can never trigger filesystem work.
|
|
210
|
+
const RECOGNIZED_ORG_PROFILE_FLAGS = new Set(['--raw']);
|
|
211
|
+
const unrecognizedOrgProfileFlag = args
|
|
212
|
+
.slice(2)
|
|
213
|
+
.find((a) => a.startsWith('--') && !RECOGNIZED_ORG_PROFILE_FLAGS.has(a));
|
|
214
|
+
if (unrecognizedOrgProfileFlag !== undefined) {
|
|
215
|
+
error(`Unrecognized flag "${unrecognizedOrgProfileFlag}" for vault org-profile. Supported: --raw.`, ERROR_REASON.SDK_FAIL_FAST);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
|
|
219
|
+
const orgProfile = _orgProfile ?? require('./org-profile.cjs');
|
|
220
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
|
|
221
|
+
const coordinates = _coordinates ?? require('./vault-coordinates.cjs');
|
|
222
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
|
|
223
|
+
const providerModule = _provider ?? require('./git-provider.cjs');
|
|
224
|
+
const resolution = orgProfile.resolveOrgProfile(cwd);
|
|
225
|
+
// Resolve the EFFECTIVE values by calling the real resolvers, so the
|
|
226
|
+
// report shows what actually won rather than what the profile offered.
|
|
227
|
+
// `vault.fallback_host` has no standalone resolver of its own — the
|
|
228
|
+
// shared resolution's own `host` is the honest answer to "what host
|
|
229
|
+
// does the shared tier actually use", which is the question the
|
|
230
|
+
// operator is asking.
|
|
231
|
+
const { provider } = providerModule.resolveProvider(cwd);
|
|
232
|
+
const shared = coordinates.resolveSharedCoordinates(cwd, provider);
|
|
233
|
+
const platformHost = coordinates.resolvePlatformHost(cwd);
|
|
234
|
+
const effectiveByKey = {
|
|
235
|
+
'vault.provider': provider,
|
|
236
|
+
[coordinates.SHARED_REPO_CONFIG_KEY]: shared.repo,
|
|
237
|
+
[coordinates.SHARED_REF_CONFIG_KEY]: shared.ref,
|
|
238
|
+
[coordinates.SHARED_HOST_CONFIG_KEY]: shared.host,
|
|
239
|
+
[coordinates.PLATFORM_HOST_CONFIG_KEY]: platformHost.host,
|
|
240
|
+
};
|
|
241
|
+
// Build `effective`'s key list from ORG_PROFILE_ALLOWLIST rather than a
|
|
242
|
+
// fresh literal array, so a future allowlist change surfaces here
|
|
243
|
+
// automatically instead of silently dropping a key from the diagnostic.
|
|
244
|
+
const effective = {};
|
|
245
|
+
for (const key of orgProfile.ORG_PROFILE_ALLOWLIST) {
|
|
246
|
+
effective[key] = Object.prototype.hasOwnProperty.call(effectiveByKey, key) ? effectiveByKey[key] : null;
|
|
247
|
+
}
|
|
248
|
+
// `overridden` holds every key present in `contributes` whose
|
|
249
|
+
// contributed value is not strictly equal to its effective value — D-05's
|
|
250
|
+
// drift made observable, and the whole reason this command resolves
|
|
251
|
+
// effective values at all rather than just echoing the document.
|
|
252
|
+
const overridden = [];
|
|
253
|
+
for (const key of Object.keys(resolution.contributes)) {
|
|
254
|
+
const profileValue = resolution.contributes[key];
|
|
255
|
+
const effectiveValue = Object.prototype.hasOwnProperty.call(effective, key) ? effective[key] : null;
|
|
256
|
+
if (effectiveValue !== profileValue) {
|
|
257
|
+
overridden.push({ key, profile: profileValue, effective: effectiveValue });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const report = {
|
|
261
|
+
pointer: { key: orgProfile.ORG_PROFILE_CONFIG_KEY, value: resolution.configured },
|
|
262
|
+
profile: {
|
|
263
|
+
resolved: resolution.resolved,
|
|
264
|
+
name: resolution.packageName,
|
|
265
|
+
version: resolution.version,
|
|
266
|
+
source: resolution.source,
|
|
267
|
+
dataPath: resolution.dataPath,
|
|
268
|
+
},
|
|
269
|
+
contributes: resolution.contributes,
|
|
270
|
+
effective,
|
|
271
|
+
overridden,
|
|
272
|
+
ignored: resolution.ignored,
|
|
273
|
+
rejected: resolution.rejected,
|
|
274
|
+
warnings: resolution.warnings,
|
|
275
|
+
};
|
|
276
|
+
// Never call error() for a project with no profile, and never exit
|
|
277
|
+
// non-zero for a resolution failure — this is a diagnostic, not an
|
|
278
|
+
// action command (D-09's posture: a broken profile is a warning, not a
|
|
279
|
+
// failure).
|
|
280
|
+
if (raw) {
|
|
281
|
+
c.output(report, true, JSON.stringify(report));
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
c.output(report, true, renderOrgProfileReport(report));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
50
287
|
else {
|
|
51
|
-
error('Unknown vault subcommand. Available: fetch, status, summary', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
288
|
+
error('Unknown vault subcommand. Available: fetch, status, summary, practices, org-profile', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
52
289
|
}
|
|
53
290
|
}
|
|
54
291
|
module.exports = {
|