@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.
Files changed (48) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/agents/pp-code-fixer.md +11 -0
  3. package/agents/pp-code-reviewer.md +77 -1
  4. package/agents/pp-executor.md +34 -1
  5. package/agents/pp-planner.md +24 -103
  6. package/commands/pp-ns-manage.md +2 -1
  7. package/commands/pp-vault-promote.md +31 -0
  8. package/gemini-extension.json +1 -1
  9. package/package.json +5 -3
  10. package/pareto-skills/pp-brand/SKILL.md +1 -1
  11. package/pp-core/bin/lib/capability-registry.cjs +160 -3
  12. package/pp-core/bin/lib/capability-state.cjs +41 -3
  13. package/pp-core/bin/lib/check-command-router.cjs +473 -3
  14. package/pp-core/bin/lib/clusters.cjs +3 -0
  15. package/pp-core/bin/lib/compose-bindings.cjs +736 -0
  16. package/pp-core/bin/lib/git-provider.cjs +279 -0
  17. package/pp-core/bin/lib/init-contract.cjs +989 -0
  18. package/pp-core/bin/lib/init-interactive.cjs +183 -0
  19. package/pp-core/bin/lib/io.cjs +9 -0
  20. package/pp-core/bin/lib/org-profile.cjs +696 -0
  21. package/pp-core/bin/lib/platform-bindings.cjs +268 -64
  22. package/pp-core/bin/lib/practices-review-check.cjs +938 -0
  23. package/pp-core/bin/lib/vault-command-router.cjs +239 -2
  24. package/pp-core/bin/lib/vault-coordinates.cjs +485 -0
  25. package/pp-core/bin/lib/vault-practices.cjs +982 -0
  26. package/pp-core/bin/lib/vault-write-back.cjs +842 -0
  27. package/pp-core/bin/lib/vault.cjs +170 -51
  28. package/pp-core/bin/pp-tools.cjs +225 -2
  29. package/pp-core/references/planner-checkpoints.md +72 -0
  30. package/pp-core/references/planner-tdd.md +55 -0
  31. package/pp-core/references/planner-vault-practices.md +67 -0
  32. package/pp-core/templates/practices/README.md +122 -0
  33. package/pp-core/templates/practices/error-handling.md +18 -0
  34. package/pp-core/templates/practices/minimal-comments.md +14 -0
  35. package/pp-core/templates/practices/naming-convention.md +18 -0
  36. package/pp-core/templates/practices/practice-template.md +23 -0
  37. package/pp-core/workflows/code-review.md +74 -18
  38. package/pp-core/workflows/discovery-sprint.md +1 -1
  39. package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +1 -1
  40. package/pp-core/workflows/init.md +137 -76
  41. package/pp-core/workflows/ship.md +1 -0
  42. package/pp-core/workflows/vault-write-back.md +213 -0
  43. package/scripts/gen-capability-registry.cjs +68 -8
  44. package/scripts/gen-platform-bindings-schema.cjs +148 -0
  45. package/scripts/lint-allow-test-rule-refs.allowlist.json +8 -1
  46. package/scripts/lint-descriptions.cjs +3 -1
  47. package/scripts/lint-legacy-dir-name.cjs +5 -0
  48. package/scripts/lint-test-file-count.allowlist.json +10 -1
@@ -0,0 +1,279 @@
1
+ "use strict";
2
+ /**
3
+ * src/git-provider.cts — the provider seam (milestone v1.11.0, Phase 30-01;
4
+ * org-profile layer added Phase 33-03).
5
+ *
6
+ * ADR-457 build-at-publish: this TypeScript file is the SOURCE OF TRUTH. It
7
+ * compiles to the git-ignored `pp-core/bin/lib/git-provider.cjs`; do NOT
8
+ * hand-edit the compiled output. Behaviour is locked by
9
+ * `tests/git-provider.test.cjs`.
10
+ *
11
+ * This module is the single place every git-host decision in pp-core routes
12
+ * through: which provider is configured, whether a value is already a literal
13
+ * clone target, how a `group/project` slug composes into a clone URL for that
14
+ * provider, what environment a clone needs, and whether a host CLI (`glab`)
15
+ * may be spawned at all. It imports node builtins plus `./org-profile.cjs` —
16
+ * still never `./vault.cjs`, `./vault-coordinates.cjs`, `./io.cjs` or
17
+ * `./shell-command-projection.cjs` — so that both `src/vault.cts` and
18
+ * `src/vault-write-back.cts` can import it (since 30-02 and 30-03) without
19
+ * forming a require cycle in the CommonJS graph `require()` loads.
20
+ * `./org-profile.cjs` is the one exception: it is itself a zero-import leaf
21
+ * (node builtins only, see its own header), so importing it here creates no
22
+ * load-time cycle even though it lazily requires this module back at
23
+ * call-time for `normalizeProvider`/`PROVIDERS` reuse (D-10).
24
+ *
25
+ * This plan (30-01) produced no behaviour change on its own: nothing called
26
+ * this module at the time. 33-03 is the first plan to change `resolveProvider`
27
+ * itself, adding the org-profile consultation between the config read and
28
+ * `DEFAULT_PROVIDER` (D-07).
29
+ */
30
+ var __importDefault = (this && this.__importDefault) || function (mod) {
31
+ return (mod && mod.__esModule) ? mod : { "default": mod };
32
+ };
33
+ Object.defineProperty(exports, "__esModule", { value: true });
34
+ exports.LITERAL_CLONE_TARGET_PATTERNS = exports.PROVIDER_CONFIG_KEY = exports.DEFAULT_PROVIDER = exports.PROVIDERS = void 0;
35
+ exports.normalizeProvider = normalizeProvider;
36
+ exports.resolveProvider = resolveProvider;
37
+ exports.isLiteralCloneTarget = isLiteralCloneTarget;
38
+ exports.composeCloneTarget = composeCloneTarget;
39
+ exports.cloneEnv = cloneEnv;
40
+ exports.providerUsesHostCli = providerUsesHostCli;
41
+ const node_fs_1 = __importDefault(require("node:fs"));
42
+ const node_path_1 = __importDefault(require("node:path"));
43
+ const org_profile_cjs_1 = require("./org-profile.cjs");
44
+ // ─── Constants ───────────────────────────────────────────────────────────────
45
+ /** The complete, ordered set of supported provider ids. */
46
+ exports.PROVIDERS = Object.freeze(['gitlab', 'azure']);
47
+ /**
48
+ * Every project that has never heard of this phase resolves to this provider
49
+ * (PROV-04): an absent config key is the normal case, not an error.
50
+ */
51
+ exports.DEFAULT_PROVIDER = 'gitlab';
52
+ /** The dotted `.planning/config.json` key that selects the provider. */
53
+ exports.PROVIDER_CONFIG_KEY = 'vault.provider';
54
+ // ─── normalizeProvider ───────────────────────────────────────────────────────
55
+ /**
56
+ * Normalize an arbitrary config value into a canonical `ProviderId`, or
57
+ * `null` if it is not an exact (case/whitespace-tolerant) member of
58
+ * `PROVIDERS`.
59
+ *
60
+ * Deliberately takes NO host or repo argument. That absence is what makes
61
+ * PROV-01's "never inferred from a hostname string" structurally true rather
62
+ * than a promise this function merely keeps by convention — it has nothing
63
+ * else to look at. Trimming and case-folding are tolerance for operator
64
+ * typing (`" Azure "`, `"GITLAB"`), not inference: there is no substring or
65
+ * prefix matching, only exact membership after normalization.
66
+ */
67
+ function normalizeProvider(value) {
68
+ if (typeof value !== 'string')
69
+ return null;
70
+ const normalized = value.trim().toLowerCase();
71
+ return exports.PROVIDERS.includes(normalized) ? normalized : null;
72
+ }
73
+ // ─── resolveProvider ─────────────────────────────────────────────────────────
74
+ /**
75
+ * Read `.planning/config.json` at `cwd`, returning `fallback` on any
76
+ * miss/unreadable-file/parse-error/non-object. Never throws.
77
+ *
78
+ * This mirrors `cfgGet` in `src/vault.cts` (lines 111-126) exactly, but is
79
+ * written inline rather than imported: `src/vault.cts` will import THIS
80
+ * module in 30-02, so importing `cfgGet` back from `vault.cjs` would create a
81
+ * cycle in the CommonJS graph loaded by `require`. The duplication is about
82
+ * ten lines with no behavioural variance to drift on.
83
+ */
84
+ function cfgGet(cwd, key, fallback) {
85
+ try {
86
+ const cfg = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(cwd, '.planning', 'config.json'), 'utf-8'));
87
+ const v = key.split('.').reduce((o, k) => (o && typeof o === 'object' ? o[k] : undefined), cfg);
88
+ return v === undefined ? fallback : v;
89
+ }
90
+ catch {
91
+ return fallback;
92
+ }
93
+ }
94
+ /**
95
+ * Bound a rejected config value before it is echoed into a warning. The
96
+ * warning is written into `.planning/vault/manifest.json` and printed by
97
+ * `vault status`, so an unbounded or newline-bearing config value would be a
98
+ * log-injection vector (T-30-04): strip CR/LF, collapse internal whitespace,
99
+ * then truncate to 40 characters.
100
+ */
101
+ function sanitizeForWarning(value) {
102
+ return value.replace(/[\r\n]+/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 40);
103
+ }
104
+ /**
105
+ * Resolve the configured provider for the project at `cwd`, in three steps
106
+ * (D-07): `.planning/config.json` → the org profile → `DEFAULT_PROVIDER`.
107
+ *
108
+ * Reads `.planning/config.json`'s `vault.provider` key first — never a host,
109
+ * never a repo shape, never `.platform-bindings.json`. When that value is set
110
+ * to anything other than `undefined`/`null`/`''`, it is the operator's
111
+ * explicit choice and is NEVER overridden by an org profile, even when it is
112
+ * unrecognized (D-05): a recognized value returns with no warnings, and an
113
+ * unrecognized value resolves to `DEFAULT_PROVIDER` with exactly one bounded,
114
+ * single-line warning naming the rejected value and the two supported ids —
115
+ * the org profile is not consulted on this branch at all.
116
+ *
117
+ * When the config key is absent, `null` or `''` — the "not set" branch — the
118
+ * org profile is consulted for `vault.provider` via `readProfileValue`. When
119
+ * it supplies a value, that value is re-normalized here (defensively) before
120
+ * use; a normalized hit is returned, a miss falls through to
121
+ * `DEFAULT_PROVIDER`. Either way the profile layer's own `warnings` are
122
+ * returned unchanged and unfiltered — they are already bounded and already
123
+ * de-duplicated within one `readProfileValue` call. With no profile
124
+ * configured, `readProfileValue` returns `{ value: undefined, warnings: [] }`
125
+ * and this branch returns `DEFAULT_PROVIDER` with `warnings: []` — identical
126
+ * to the pre-33-03 early return (PROV-04 / ORG-02). This function adds no
127
+ * warning of its own for the no-profile case.
128
+ */
129
+ function resolveProvider(cwd) {
130
+ const raw = cfgGet(cwd, exports.PROVIDER_CONFIG_KEY, undefined);
131
+ if (raw === undefined || raw === null || raw === '') {
132
+ const profile = (0, org_profile_cjs_1.readProfileValue)(cwd, exports.PROVIDER_CONFIG_KEY);
133
+ if (profile.value !== undefined) {
134
+ const normalizedProfileValue = normalizeProvider(profile.value);
135
+ if (normalizedProfileValue !== null) {
136
+ return { provider: normalizedProfileValue, warnings: profile.warnings };
137
+ }
138
+ }
139
+ return { provider: exports.DEFAULT_PROVIDER, warnings: profile.warnings };
140
+ }
141
+ const normalized = normalizeProvider(raw);
142
+ if (normalized !== null) {
143
+ return { provider: normalized, warnings: [] };
144
+ }
145
+ const rawStr = typeof raw === 'string' ? raw : JSON.stringify(raw);
146
+ const echoed = sanitizeForWarning(rawStr ?? '');
147
+ const warning = `Unrecognized ${exports.PROVIDER_CONFIG_KEY} value "${echoed}" — supported: ${exports.PROVIDERS.join(', ')}. Falling back to ${exports.DEFAULT_PROVIDER}.`;
148
+ return { provider: exports.DEFAULT_PROVIDER, warnings: [warning.slice(0, 120)] };
149
+ }
150
+ // ─── LITERAL_CLONE_TARGET_PATTERNS ──────────────────────────────────────────
151
+ /**
152
+ * The single regex-expressible definition of the seven forms
153
+ * `isLiteralCloneTarget` accepts, as anchored regex source strings. Entry
154
+ * order is part of the contract: `scripts/gen-platform-bindings-schema.cjs`
155
+ * (plan 02) concatenates these into one alternation in this order, so a
156
+ * reorder here is a deliberate schema-regenerating edit, not a
157
+ * refactor-neutral one. `tests/git-provider.test.cjs` locks length, order and
158
+ * content.
159
+ */
160
+ exports.LITERAL_CLONE_TARGET_PATTERNS = Object.freeze([
161
+ '^[A-Za-z][A-Za-z0-9+.-]*:\\/\\/', // scheme URL — e.g. https://, ssh://, file://
162
+ '^[\\w.][\\w.-]*@', // scp-style ssh — user@host:path
163
+ '^\\/', // absolute unix path
164
+ '^\\.\\/', // explicit relative — ./path
165
+ '^\\.\\.\\/', // parent-relative — ../path
166
+ '^~', // home-relative — ~ or ~/path
167
+ '^[A-Za-z]:[\\\\/]', // Windows drive path — C:\ or C:/
168
+ ]);
169
+ // Compiled once at module load, without the global flag (a global regex
170
+ // carries `lastIndex` state across `test` calls and would make the predicate
171
+ // answer differently on repeated identical inputs).
172
+ const compiledLiteralCloneTargetPatterns = exports.LITERAL_CLONE_TARGET_PATTERNS.map((source) => new RegExp(source));
173
+ // ─── isLiteralCloneTarget (relocated verbatim from src/vault.cts) ───────────
174
+ /**
175
+ * True when `value` is already something git can clone directly — an
176
+ * absolute path, a scheme-qualified URL, or scp-style `user@host:path` —
177
+ * rather than a `group/project` slug that must be composed onto a host.
178
+ *
179
+ * This is now the SINGLE definition. `src/vault.cts` and
180
+ * `src/vault-write-back.cts` import it from here (30-02, 30-03) rather than
181
+ * each keeping a copy — a read/write divergence in this predicate was defect
182
+ * P-1 in Phase 29 (the write path used the value verbatim while the read path
183
+ * always composed a URL, so a slug broke writing and a path broke reading).
184
+ *
185
+ * Security (T-29-26): a value git would read as an OPTION must never take
186
+ * the verbatim branch. The scheme test is anchored at the START — an earlier
187
+ * version used `value.includes('://')`, so `--upload-pack=…#x://` passed
188
+ * through as a flag — and the scp-style test may not begin with `-`. This
189
+ * function is also the newer half of the pair: before it existed, this path
190
+ * ALWAYS composed a URL, so a flag-shaped repo could not reach git here at
191
+ * all. Callers pass `--` before the target as the second layer.
192
+ *
193
+ * The seven forms now live in the exported `LITERAL_CLONE_TARGET_PATTERNS`
194
+ * array so artifacts that cannot call this function — specifically the JSON
195
+ * Schema `pattern` in `schemas/platform-bindings.schema.json` — can be
196
+ * generated from the same definition rather than hand-transcribing it. The
197
+ * array is consumed by `src/platform-bindings.cts` and, through it, by
198
+ * `scripts/gen-platform-bindings-schema.cjs` (plan 02). The leading-dash
199
+ * refusal below is deliberately not one of the seven entries.
200
+ */
201
+ function isLiteralCloneTarget(value) {
202
+ if (value.startsWith('-'))
203
+ return false;
204
+ return compiledLiteralCloneTargetPatterns.some((re) => re.test(value));
205
+ }
206
+ // ─── composeCloneTarget ──────────────────────────────────────────────────────
207
+ /**
208
+ * Compose `repo` into something `git clone` can reach for `provider` at
209
+ * `host`, or pass it through verbatim when it is already a literal clone
210
+ * target (PROV-02's final sentence — this is what keeps the working Azure
211
+ * SSH form `git@ssh.dev.azure.com:v3/{org}/{project}/{repo}` working
212
+ * unchanged).
213
+ *
214
+ * `gitlab`: `https://{host}/{slug}.git` — byte-identical to the template
215
+ * being removed from `src/vault.cts` in 30-02, because PROV-04 depends on
216
+ * this string not moving.
217
+ *
218
+ * `azure`: `https://{host}/{slug}` — no `.git` suffix appended. Azure DevOps
219
+ * rejects a `.git`-suffixed path (`TF401019`); this is the entire reason
220
+ * PROV-02 exists.
221
+ *
222
+ * When `provider` is `azure` and the trimmed slug already ends in `.git`, the
223
+ * suffix is NOT stripped — an operator-configured coordinate is data, not
224
+ * something this function rewrites — but exactly one warning is returned
225
+ * naming `TF401019` and recommending the suffix be removed from the
226
+ * configured value. Composition still never appends; the suffix in that case
227
+ * is the operator's own (T-30-08, accepted risk: rewriting a configured
228
+ * coordinate is a worse failure mode than a clear diagnostic).
229
+ */
230
+ function composeCloneTarget(provider, repo, host) {
231
+ const trimmed = repo.trim();
232
+ if (isLiteralCloneTarget(trimmed)) {
233
+ return { url: trimmed, warnings: [] };
234
+ }
235
+ if (provider === 'azure') {
236
+ const url = `https://${host}/${trimmed}`;
237
+ if (trimmed.endsWith('.git')) {
238
+ return {
239
+ url,
240
+ warnings: [
241
+ 'Azure DevOps rejects a .git-suffixed path (TF401019) — remove the .git suffix from the configured vault repo value.',
242
+ ],
243
+ };
244
+ }
245
+ return { url, warnings: [] };
246
+ }
247
+ // gitlab (and, structurally, any future provider that composes the same
248
+ // shape) — byte-identical to the template this replaces.
249
+ return { url: `https://${host}/${trimmed}.git`, warnings: [] };
250
+ }
251
+ // ─── cloneEnv ────────────────────────────────────────────────────────────────
252
+ /**
253
+ * Build the environment additions a clone needs for `provider` at `host`.
254
+ *
255
+ * `gitlab` needs `GITLAB_HOST` (consumed by the `glab repo clone` flow).
256
+ * `azure` needs nothing — an empty object.
257
+ *
258
+ * `execGit`/`execTool` (`src/shell-command-projection.cts` lines 441-446 and
259
+ * 473) MERGE this object over `process.env`, so the empty object for `azure`
260
+ * is a genuine no-op: it preserves PATH, the ambient credential helper, and
261
+ * the non-interactive defaults Azure authentication relies on. It does not
262
+ * strip anything.
263
+ */
264
+ function cloneEnv(provider, host) {
265
+ if (provider === 'gitlab')
266
+ return { GITLAB_HOST: host };
267
+ return {};
268
+ }
269
+ // ─── providerUsesHostCli ─────────────────────────────────────────────────────
270
+ /**
271
+ * True when `provider` has a first-class host CLI available (`glab`, for
272
+ * `gitlab`). Returns a boolean rather than a program name: no caller should
273
+ * ever build a subprocess program name out of a config-derived string, and
274
+ * returning a boolean makes that impossible rather than merely discouraged
275
+ * (T-30-03).
276
+ */
277
+ function providerUsesHostCli(provider) {
278
+ return provider === 'gitlab';
279
+ }