@pareto4/pp-core 1.10.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/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 +49 -1
- 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 +2 -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/vault-command-router.cjs +133 -2
- package/pp-core/bin/lib/vault-coordinates.cjs +485 -0
- package/pp-core/bin/lib/vault-write-back.cjs +178 -92
- package/pp-core/bin/lib/vault.cjs +158 -81
- package/pp-core/bin/pp-tools.cjs +199 -2
- 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/scripts/gen-platform-bindings-schema.cjs +148 -0
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* src/vault-coordinates.cts — the coordinate seam (milestone v1.11.0, Phase
|
|
4
|
+
* 31-01; 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/vault-coordinates.cjs`; do NOT
|
|
8
|
+
* hand-edit the compiled output. Behaviour is locked by
|
|
9
|
+
* `tests/vault-coordinates.test.cjs`.
|
|
10
|
+
*
|
|
11
|
+
* This module is the single place every vault coordinate decision in pp-core
|
|
12
|
+
* routes through — the host, repo and ref for the client tier, and the host,
|
|
13
|
+
* repo and ref for the shared tier — for both the read path (`src/vault.cts`)
|
|
14
|
+
* and the write path (`src/vault-write-back.cts`). Before this module
|
|
15
|
+
* existed, `DEFAULT_FALLBACK_REPO` was a `const` in `src/vault.cts` and four
|
|
16
|
+
* separate `'gitlab.com'` fallback chains were hand-written across the two
|
|
17
|
+
* files, two of which read `platform.host` from `.platform-bindings.json`
|
|
18
|
+
* and two of which read it from `.planning/config.json` (D-09) — the same
|
|
19
|
+
* one-value-two-owners shape that produced defect P-1 in Phase 29 for
|
|
20
|
+
* clone-target composition, and that Phase 30 closed for that concern by
|
|
21
|
+
* building `src/git-provider.cts`. This module is the equivalent owner for
|
|
22
|
+
* coordinates.
|
|
23
|
+
*
|
|
24
|
+
* It imports ONLY node builtins plus `./git-provider.cjs` and
|
|
25
|
+
* `./org-profile.cjs` — no `vault.cjs`, no `vault-write-back.cjs`, no
|
|
26
|
+
* `io.cjs`, no `shell-command-projection.cjs` — so that both `src/vault.cts`
|
|
27
|
+
* and `src/vault-write-back.cts` can import it without forming a require
|
|
28
|
+
* cycle in the CommonJS graph `require()` loads. `git-provider.cjs` and
|
|
29
|
+
* `org-profile.cjs` are both safe to import here: neither imports this
|
|
30
|
+
* module back at their own top level (org-profile.cjs's only reference back
|
|
31
|
+
* to git-provider.cjs is a call-time lazy require, never load-time), so
|
|
32
|
+
* depending on either does not risk a cycle the way depending on `vault.cjs`
|
|
33
|
+
* would.
|
|
34
|
+
*
|
|
35
|
+
* Every coordinate this module resolves now consults an org profile (D-07)
|
|
36
|
+
* strictly between the config read and the built-in default —
|
|
37
|
+
* `resolvePlatformHost` and `resolveSharedCoordinates` call
|
|
38
|
+
* `readProfileValue` directly; `resolveClientCoordinates` gains no direct
|
|
39
|
+
* call of its own because its host chain already reaches
|
|
40
|
+
* `resolvePlatformHost` transitively. This supersedes the tier-keyed
|
|
41
|
+
* `orgProfileCoordinates(cwd, tier)` stub Phase 31 froze here — see the
|
|
42
|
+
* supersession note above `resolvePlatformHost` for the full rationale.
|
|
43
|
+
*
|
|
44
|
+
* Export style: named `export function` / `export const` declarations,
|
|
45
|
+
* matching `src/git-provider.cts`'s export surface rather than
|
|
46
|
+
* `src/vault.cts`'s `export =` object. Both consumers already import
|
|
47
|
+
* `./git-provider.cjs` with a named-import block, so this module is imported
|
|
48
|
+
* the identical way and no consumer needs a different call convention for
|
|
49
|
+
* the two seams.
|
|
50
|
+
*/
|
|
51
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
52
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
53
|
+
};
|
|
54
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
55
|
+
exports.SHARED_OPT_OUT_REASON = exports.PLATFORM_HOST_CONFIG_KEY = exports.SHARED_HOST_CONFIG_KEY = exports.SHARED_REF_CONFIG_KEY = exports.SHARED_REPO_CONFIG_KEY = exports.DEFAULT_SHARED_REF = exports.DEFAULT_SHARED_REPO = exports.DEFAULT_HOST_FOR_PROVIDER = void 0;
|
|
56
|
+
exports.defaultHostForProvider = defaultHostForProvider;
|
|
57
|
+
exports.resolvePlatformHost = resolvePlatformHost;
|
|
58
|
+
exports.resolveClientCoordinates = resolveClientCoordinates;
|
|
59
|
+
exports.resolveSharedCoordinates = resolveSharedCoordinates;
|
|
60
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
61
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
62
|
+
const org_profile_cjs_1 = require("./org-profile.cjs");
|
|
63
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
64
|
+
/**
|
|
65
|
+
* D-08: the last-resort host, derived from the resolved provider. New code,
|
|
66
|
+
* not a relocation — structured the way `PROVIDERS`/`DEFAULT_PROVIDER` are
|
|
67
|
+
* structured in `src/git-provider.cts`. Byte-identical for every gitlab
|
|
68
|
+
* install (ROADMAP SC3); correct out of the box for Azure DevOps cloud.
|
|
69
|
+
*/
|
|
70
|
+
exports.DEFAULT_HOST_FOR_PROVIDER = Object.freeze({
|
|
71
|
+
gitlab: 'gitlab.com',
|
|
72
|
+
azure: 'dev.azure.com',
|
|
73
|
+
});
|
|
74
|
+
/** The values moving out of `src/vault.cts`'s `DEFAULT_FALLBACK_REPO`/`DEFAULT_FALLBACK_REF`. */
|
|
75
|
+
exports.DEFAULT_SHARED_REPO = 'pareto4/pareto-vault';
|
|
76
|
+
exports.DEFAULT_SHARED_REF = 'main';
|
|
77
|
+
/** Dotted `.planning/config.json` keys, named so no consumer hand-writes a key string. */
|
|
78
|
+
exports.SHARED_REPO_CONFIG_KEY = 'vault.fallback_repo';
|
|
79
|
+
exports.SHARED_REF_CONFIG_KEY = 'vault.fallback_ref';
|
|
80
|
+
exports.SHARED_HOST_CONFIG_KEY = 'vault.fallback_host';
|
|
81
|
+
exports.PLATFORM_HOST_CONFIG_KEY = 'platform.host';
|
|
82
|
+
/**
|
|
83
|
+
* D-13/WR-01: the `suppressedReason` for an `explicit-opt-out`. Worded so an
|
|
84
|
+
* operator reading a degraded manifest recognises their OWN decision in it —
|
|
85
|
+
* names the key and states the tier was disabled on purpose — rather than
|
|
86
|
+
* being told to act on something they already did. Exported so
|
|
87
|
+
* `tests/vault-coordinates.test.cjs` asserts the shipped sentence itself
|
|
88
|
+
* (read from this module) instead of a restated literal that could drift
|
|
89
|
+
* from it.
|
|
90
|
+
*/
|
|
91
|
+
exports.SHARED_OPT_OUT_REASON = `the shared vault tier was intentionally disabled (${exports.SHARED_REPO_CONFIG_KEY} is set to an empty string)`;
|
|
92
|
+
// ─── Private readers ─────────────────────────────────────────────────────────
|
|
93
|
+
/**
|
|
94
|
+
* Read a dotted config key from `.planning/config.json`, returning `fallback`
|
|
95
|
+
* on any miss/unreadable-file/parse-error/non-object. Never throws.
|
|
96
|
+
*
|
|
97
|
+
* This is a byte-for-byte mirror of `cfgGet` in `src/git-provider.cts` (lines
|
|
98
|
+
* 78-89), which itself mirrors the canonical original at `src/vault.cts`
|
|
99
|
+
* (lines 124-135). It is written inline rather than imported: `src/vault.cts`
|
|
100
|
+
* will import THIS module, so importing a reader back out of `vault.cjs`
|
|
101
|
+
* would be the require cycle this whole leaf-module shape exists to avoid.
|
|
102
|
+
*
|
|
103
|
+
* Importing the reader from `git-provider.cjs` instead was also considered
|
|
104
|
+
* and rejected — not because of a require cycle (this module already
|
|
105
|
+
* depends on the provider seam, and the seam imports nothing back, so
|
|
106
|
+
* exporting a value from it would be safe), but because the reader is not
|
|
107
|
+
* the provider seam's responsibility to publish. Exporting it would make a
|
|
108
|
+
* private implementation detail of one seam part of another seam's public
|
|
109
|
+
* contract, so a future change to either module's reader would become a
|
|
110
|
+
* cross-module API change instead of a local one. What actually prevents
|
|
111
|
+
* drift is that all copies are byte-identical and each carries a comment
|
|
112
|
+
* naming the canonical original — that is the property to preserve, not a
|
|
113
|
+
* shared import.
|
|
114
|
+
*/
|
|
115
|
+
function cfgGet(cwd, key, fallback) {
|
|
116
|
+
try {
|
|
117
|
+
const cfg = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(cwd, '.planning', 'config.json'), 'utf-8'));
|
|
118
|
+
const v = key.split('.').reduce((o, k) => (o && typeof o === 'object' ? o[k] : undefined), cfg);
|
|
119
|
+
return v === undefined ? fallback : v;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return fallback;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function isObject(v) {
|
|
126
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Read `.platform-bindings.json` at the project root. Mirrors `readBindings`
|
|
130
|
+
* in `src/vault.cts` (lines 141-150) exactly: any problem
|
|
131
|
+
* (absent/malformed/non-object) → `{}`. Written inline for the same
|
|
132
|
+
* require-cycle reason `cfgGet` above is inline.
|
|
133
|
+
*/
|
|
134
|
+
function readBindings(cwd) {
|
|
135
|
+
try {
|
|
136
|
+
const p = node_path_1.default.join(cwd, '.platform-bindings.json');
|
|
137
|
+
if (!node_fs_1.default.existsSync(p))
|
|
138
|
+
return {};
|
|
139
|
+
const parsed = JSON.parse(node_fs_1.default.readFileSync(p, 'utf-8'));
|
|
140
|
+
return isObject(parsed) ? parsed : {};
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return {};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Bound a rejected config value before it is echoed into a warning.
|
|
148
|
+
* Byte-for-byte mirror of `sanitizeForWarning` in `src/git-provider.cts`
|
|
149
|
+
* (lines 98-100): strip CR/LF, collapse internal whitespace, trim, truncate
|
|
150
|
+
* to 40 characters. Every echoed config value in this module goes through
|
|
151
|
+
* it — no second, weaker bounding rule is written (T-31-05).
|
|
152
|
+
*/
|
|
153
|
+
function sanitizeForWarning(value) {
|
|
154
|
+
return value.replace(/[\r\n]+/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 40);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Append only strings not already present in `target`, mirroring the
|
|
158
|
+
* exact-string de-duplication `src/vault.cts` already performs when folding
|
|
159
|
+
* two resolutions' warnings (lines 436-439). `resolveSharedCoordinates` can
|
|
160
|
+
* consult the org profile up to three times per call and `resolvePlatformHost`
|
|
161
|
+
* a fourth time within it — D-09 promises one bounded warning per rejected
|
|
162
|
+
* thing, not four copies of the same string.
|
|
163
|
+
*/
|
|
164
|
+
function pushDeduped(target, incoming) {
|
|
165
|
+
for (const w of incoming) {
|
|
166
|
+
if (!target.includes(w))
|
|
167
|
+
target.push(w);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// ─── defaultHostForProvider ──────────────────────────────────────────────────
|
|
171
|
+
/** Read the D-08 host table for `provider`. */
|
|
172
|
+
function defaultHostForProvider(provider) {
|
|
173
|
+
return exports.DEFAULT_HOST_FOR_PROVIDER[provider];
|
|
174
|
+
}
|
|
175
|
+
// ─── D-07 supersession: the tier-keyed stub is gone ──────────────────────────
|
|
176
|
+
//
|
|
177
|
+
// Phase 31 froze `orgProfileCoordinates(cwd, tier)` — a stub taking a TIER
|
|
178
|
+
// (`'client' | 'shared'`) as the contract Phase 33 would fill. Phase 33
|
|
179
|
+
// (D-07) supersedes that shape rather than filling it: D-06's allowlist is a
|
|
180
|
+
// set of config KEYS (`vault.provider`, `vault.fallback_repo`,
|
|
181
|
+
// `vault.fallback_ref`, `vault.fallback_host`, `platform.host`), not tiers,
|
|
182
|
+
// and neither `vault.provider` nor `platform.host` fits a per-tier return at
|
|
183
|
+
// all. One key-keyed function — `readProfileValue(cwd, key)` in
|
|
184
|
+
// `src/org-profile.cts` — now serves all three consuming resolvers
|
|
185
|
+
// (`resolveProvider` in `src/git-provider.cts`, plus `resolvePlatformHost`
|
|
186
|
+
// and `resolveSharedCoordinates` below), where the tier-keyed shape could
|
|
187
|
+
// only ever have served two of them.
|
|
188
|
+
//
|
|
189
|
+
// Nothing is lost by this supersession: the client tier needs no direct call
|
|
190
|
+
// of its own, because its host chain already runs
|
|
191
|
+
// `client.vaultHost → resolvePlatformHost → defaultHostForProvider`, so a
|
|
192
|
+
// profile-supplied `platform.host` reaches it transitively through
|
|
193
|
+
// `resolvePlatformHost` below.
|
|
194
|
+
// ─── resolvePlatformHost (D-09, D-07) ────────────────────────────────────────
|
|
195
|
+
/**
|
|
196
|
+
* Resolve `platform.host` for the project at `cwd`, in four steps (widened
|
|
197
|
+
* from three by D-07): `.platform-bindings.json` → `.planning/config.json` →
|
|
198
|
+
* the org profile → `none`. `.platform-bindings.json` stays authoritative
|
|
199
|
+
* (Phase 31 D-09) — an org profile is below config, never above it (D-05).
|
|
200
|
+
*
|
|
201
|
+
* Read `.platform-bindings.json` first: when its `platform` block carries a
|
|
202
|
+
* string `host` whose trimmed value is non-empty, return that trimmed value
|
|
203
|
+
* with source `bindings` and no warnings — bindings win, and the org profile
|
|
204
|
+
* is never consulted on this branch. Otherwise read `platform.host` from
|
|
205
|
+
* `.planning/config.json`: when that trimmed value is non-empty, return it
|
|
206
|
+
* with source `config` and exactly one warning; the org profile is not
|
|
207
|
+
* consulted on this branch either — an explicit config value is the
|
|
208
|
+
* operator's, even from an older layer, and outranks a profile precisely
|
|
209
|
+
* because D-05 never lets a profile outrank anything explicitly set. The
|
|
210
|
+
* warning states that `platform.host` was read from `.planning/config.json`,
|
|
211
|
+
* that `.platform-bindings.json` is authoritative, and that the value should
|
|
212
|
+
* be moved there; it does not echo the host value at all, so this warning
|
|
213
|
+
* carries no injected-content surface. The whole warning is capped at 200
|
|
214
|
+
* characters.
|
|
215
|
+
*
|
|
216
|
+
* Only when BOTH bindings and config are silent is the org profile
|
|
217
|
+
* consulted, via `readProfileValue(cwd, PLATFORM_HOST_CONFIG_KEY)`. When its
|
|
218
|
+
* value is defined, return `{ host: value, source: 'profile', warnings }`;
|
|
219
|
+
* otherwise return `{ host: null, source: 'none', warnings }`. Both of these
|
|
220
|
+
* final returns carry the SAME profile warnings, so a broken profile is
|
|
221
|
+
* reported whether or not it happened to supply this particular key.
|
|
222
|
+
*/
|
|
223
|
+
function resolvePlatformHost(cwd) {
|
|
224
|
+
const bindings = readBindings(cwd);
|
|
225
|
+
const platform = isObject(bindings.platform) ? bindings.platform : null;
|
|
226
|
+
const bindingsHostRaw = platform && typeof platform.host === 'string' ? platform.host.trim() : '';
|
|
227
|
+
if (bindingsHostRaw) {
|
|
228
|
+
return { host: bindingsHostRaw, source: 'bindings', warnings: [] };
|
|
229
|
+
}
|
|
230
|
+
const configHostRaw = cfgGet(cwd, exports.PLATFORM_HOST_CONFIG_KEY, undefined);
|
|
231
|
+
const configHost = typeof configHostRaw === 'string' ? configHostRaw.trim() : '';
|
|
232
|
+
if (configHost) {
|
|
233
|
+
const warning = `${exports.PLATFORM_HOST_CONFIG_KEY} was read from .planning/config.json — .platform-bindings.json is authoritative for ${exports.PLATFORM_HOST_CONFIG_KEY}; move the value there.`.slice(0, 200);
|
|
234
|
+
return { host: configHost, source: 'config', warnings: [warning] };
|
|
235
|
+
}
|
|
236
|
+
const profile = (0, org_profile_cjs_1.readProfileValue)(cwd, exports.PLATFORM_HOST_CONFIG_KEY);
|
|
237
|
+
if (profile.value !== undefined) {
|
|
238
|
+
return { host: profile.value, source: 'profile', warnings: profile.warnings };
|
|
239
|
+
}
|
|
240
|
+
return { host: null, source: 'none', warnings: profile.warnings };
|
|
241
|
+
}
|
|
242
|
+
// ─── resolveClientCoordinates ────────────────────────────────────────────────
|
|
243
|
+
/**
|
|
244
|
+
* Resolve the client-tier coordinates for `provider` at `cwd`.
|
|
245
|
+
*
|
|
246
|
+
* `repo` is `client.vaultRepo` when it is a string, else `null`, passed
|
|
247
|
+
* through with no trimming and no rewriting — the raw value is what the
|
|
248
|
+
* T-29-26 flag-shaped refusal and `isLiteralCloneTarget` inspect downstream,
|
|
249
|
+
* so this function must not touch it. `ref` is `client.vaultRef` trimmed
|
|
250
|
+
* when it is a non-blank string, else `'main'`. `host` is, in order:
|
|
251
|
+
* `client.vaultHost` trimmed when non-blank; else `resolvePlatformHost(cwd)`;
|
|
252
|
+
* else `defaultHostForProvider(provider)`. `resolvePlatformHost`'s warnings
|
|
253
|
+
* are folded into the returned `warnings` only when it is actually
|
|
254
|
+
* consulted (i.e. when no client-supplied host wins), so a project with an
|
|
255
|
+
* explicit `client.vaultHost` never surfaces a D-09 warning it has no
|
|
256
|
+
* bearing on.
|
|
257
|
+
*
|
|
258
|
+
* Trimming `host` and `ref` is the one deliberate unification in this
|
|
259
|
+
* function — the write path trimmed these values and the read path did not,
|
|
260
|
+
* so the two paths disagreed for a whitespace-padded value. For any value
|
|
261
|
+
* with no surrounding whitespace, which is every real install, the result is
|
|
262
|
+
* byte-identical to both paths' previous behaviour.
|
|
263
|
+
*
|
|
264
|
+
* This function deliberately gains no direct org-profile call (D-07): its
|
|
265
|
+
* host chain already runs `client.vaultHost → resolvePlatformHost →
|
|
266
|
+
* defaultHostForProvider`, so a profile-supplied `platform.host` reaches it
|
|
267
|
+
* transitively through `resolvePlatformHost` above. A future reader should
|
|
268
|
+
* not "fix" this apparent omission by adding a direct `readProfileValue`
|
|
269
|
+
* call here — that would consult the profile twice for the same key on a
|
|
270
|
+
* project that also resolves the shared tier in the same request.
|
|
271
|
+
*/
|
|
272
|
+
function resolveClientCoordinates(cwd, provider) {
|
|
273
|
+
const bindings = readBindings(cwd);
|
|
274
|
+
const client = isObject(bindings.client) ? bindings.client : null;
|
|
275
|
+
const repo = client && typeof client.vaultRepo === 'string' ? client.vaultRepo : null;
|
|
276
|
+
const rawRef = client && typeof client.vaultRef === 'string' ? client.vaultRef.trim() : '';
|
|
277
|
+
const ref = rawRef || 'main';
|
|
278
|
+
const rawClientHost = client && typeof client.vaultHost === 'string' ? client.vaultHost.trim() : '';
|
|
279
|
+
let host;
|
|
280
|
+
let warnings = [];
|
|
281
|
+
if (rawClientHost) {
|
|
282
|
+
host = rawClientHost;
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
const platformRes = resolvePlatformHost(cwd);
|
|
286
|
+
if (platformRes.host !== null) {
|
|
287
|
+
host = platformRes.host;
|
|
288
|
+
warnings = warnings.concat(platformRes.warnings);
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
host = defaultHostForProvider(provider);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return { repo, ref, host, warnings };
|
|
295
|
+
}
|
|
296
|
+
// ─── resolveSharedCoordinates ────────────────────────────────────────────────
|
|
297
|
+
/**
|
|
298
|
+
* Resolve the shared-tier coordinates for `provider` at `cwd`.
|
|
299
|
+
*
|
|
300
|
+
* `ref`: `vault.fallback_ref` trimmed when a non-blank string; else (D-07) the
|
|
301
|
+
* org profile's `vault.fallback_ref`; else `DEFAULT_SHARED_REF`.
|
|
302
|
+
*
|
|
303
|
+
* `host`, in D-03's order, widened by D-07: `vault.fallback_host` trimmed
|
|
304
|
+
* when a non-blank string; else the org profile's `vault.fallback_host`; else
|
|
305
|
+
* `resolvePlatformHost(cwd)`; else `defaultHostForProvider(provider)`. When
|
|
306
|
+
* `vault.fallback_host` is present but is not a usable string (a number, an
|
|
307
|
+
* array, an object, or a blank string), it is skipped and exactly one bounded
|
|
308
|
+
* warning is added naming `vault.fallback_host` and the bounded echo of the
|
|
309
|
+
* rejected value, in the same sentence shape `resolveProvider`
|
|
310
|
+
* (`src/git-provider.cts`) uses for an unrecognized provider. A usable
|
|
311
|
+
* operator-supplied host is never rewritten or "fixed" — passed through as
|
|
312
|
+
* written, warned about only if something is worth saying. That is the same
|
|
313
|
+
* rule `composeCloneTarget` follows for an operator-supplied `.git` suffix
|
|
314
|
+
* (T-30-08). Ordering matters and is not arbitrary: a profile's
|
|
315
|
+
* `vault.fallback_host` is a statement about the shared tier specifically, so
|
|
316
|
+
* it outranks the more general `platform.host` for this tier exactly the way
|
|
317
|
+
* `vault.fallback_host` config outranks it.
|
|
318
|
+
*
|
|
319
|
+
* `repo`, in the config-then-org-profile-then-built-in-default order D-06
|
|
320
|
+
* fixes, now reporting suppression through the D-13/WR-01 `suppressedKind`
|
|
321
|
+
* discriminator rather than a single always-derivable boolean:
|
|
322
|
+
*
|
|
323
|
+
* - `vault.fallback_repo` present as a string and non-empty: that value
|
|
324
|
+
* wins, returned as written with no trimming. `suppressed: false`,
|
|
325
|
+
* `suppressedKind: null`, `suppressedReason: null`. The org profile is
|
|
326
|
+
* not consulted on this branch at all (D-05).
|
|
327
|
+
* - `vault.fallback_repo` present as the EXACT empty string `''`: an
|
|
328
|
+
* explicit, deliberate "shared tier disabled" — a decision, not an
|
|
329
|
+
* absence. Returns `repo: null`, `suppressed: TRUE`,
|
|
330
|
+
* `suppressedKind: 'explicit-opt-out'`, `suppressedReason:
|
|
331
|
+
* SHARED_OPT_OUT_REASON`. True on both providers and both values of
|
|
332
|
+
* `applyBuiltInDefault`; no warning is added (there is nothing wrong to
|
|
333
|
+
* report — the operator did this on purpose). The org profile is NOT
|
|
334
|
+
* consulted on this branch either, for the same D-05 reason as the
|
|
335
|
+
* non-empty case: an explicit `''` IS a set value, not an absence a
|
|
336
|
+
* profile may fill.
|
|
337
|
+
* - `vault.fallback_repo` present but not a string: treated as absent,
|
|
338
|
+
* falling through with one bounded warning naming the key and the
|
|
339
|
+
* bounded echo of the rejected value.
|
|
340
|
+
* - Key absent (or fell through): the org profile is consulted for
|
|
341
|
+
* `vault.fallback_repo` (D-07's successor to the frozen
|
|
342
|
+
* `orgProfileCoordinates(cwd, 'shared')` stub — see the supersession note
|
|
343
|
+
* above `resolvePlatformHost`). When it supplies a value, that value wins
|
|
344
|
+
* with `suppressed: false`, `suppressedKind: null`, `suppressedReason:
|
|
345
|
+
* null`, and the whole `applyBuiltInDefault` ladder below is skipped —
|
|
346
|
+
* `applyBuiltInDefault` withholds only the BUILT-IN default, never a
|
|
347
|
+
* configured profile value, so a project with a profile-supplied shared
|
|
348
|
+
* repo now performs a shared-shadow lookup on the write path where an
|
|
349
|
+
* unconfigured project still performs none. That is correct (the profile
|
|
350
|
+
* is a configured value, not a built-in) and cannot affect any existing
|
|
351
|
+
* install, because an install with no profile takes an unchanged path.
|
|
352
|
+
* - Nothing resolved, `provider` is `gitlab`: `DEFAULT_SHARED_REPO` when
|
|
353
|
+
* `applyBuiltInDefault` is true, else `null` — but `suppressed: false`
|
|
354
|
+
* and `suppressedKind: null` under BOTH flag values. Nothing was
|
|
355
|
+
* suppressed either way: with the flag true the built-in default
|
|
356
|
+
* applied, and with it false the caller asked for no default and got
|
|
357
|
+
* none. This must stay `false` under both flag values — making it `true`
|
|
358
|
+
* would put a suppression note on every GitLab project's promotion
|
|
359
|
+
* preview, exactly the regression ROADMAP SC3 exists to prevent.
|
|
360
|
+
* - Nothing resolved, `provider` is anything other than `gitlab`:
|
|
361
|
+
* `repo: null`, `suppressed: TRUE`, `suppressedKind:
|
|
362
|
+
* 'provider-default-unavailable'`, `suppressedReason` reading exactly
|
|
363
|
+
* `no shared vault configured for provider "<provider>"` — regardless of
|
|
364
|
+
* `applyBuiltInDefault`. This is the WR-01 fix itself: withholding the
|
|
365
|
+
* built-in default is why the write path could not previously see this
|
|
366
|
+
* state, and the fix is for the resolver to describe the state rather
|
|
367
|
+
* than describe what it did about it. The existing gitlab-only WARNING
|
|
368
|
+
* stays gated on `applyBuiltInDefault` exactly as before — the read
|
|
369
|
+
* path's warning list is byte-identical and the write path (which passes
|
|
370
|
+
* `applyBuiltInDefault: false`) gains no new warning.
|
|
371
|
+
*
|
|
372
|
+
* Two callers deliberately pass different values for `applyBuiltInDefault`,
|
|
373
|
+
* and this is not a divergence to unify: the read-path ladder and
|
|
374
|
+
* `assertVaultWriteTarget` both apply the built-in default because both do
|
|
375
|
+
* so today, while the write path's shared-shadow lookup does not because it
|
|
376
|
+
* never has (it reads the key with an empty-string default) — making it
|
|
377
|
+
* start applying the default would give existing gitlab installs a shared
|
|
378
|
+
* clone they do not perform today, breaking ROADMAP SC3.
|
|
379
|
+
*/
|
|
380
|
+
function resolveSharedCoordinates(cwd, provider, opts) {
|
|
381
|
+
const applyBuiltInDefault = opts?.applyBuiltInDefault ?? true;
|
|
382
|
+
const warnings = [];
|
|
383
|
+
// ─ ref ─
|
|
384
|
+
const rawRefCfg = cfgGet(cwd, exports.SHARED_REF_CONFIG_KEY, undefined);
|
|
385
|
+
let ref;
|
|
386
|
+
if (typeof rawRefCfg === 'string' && rawRefCfg.trim()) {
|
|
387
|
+
ref = rawRefCfg.trim();
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
const refProfile = (0, org_profile_cjs_1.readProfileValue)(cwd, exports.SHARED_REF_CONFIG_KEY);
|
|
391
|
+
pushDeduped(warnings, refProfile.warnings);
|
|
392
|
+
ref = refProfile.value !== undefined ? refProfile.value : exports.DEFAULT_SHARED_REF;
|
|
393
|
+
}
|
|
394
|
+
// ─ host (D-03 order, widened by D-07) ─
|
|
395
|
+
const rawHostCfg = cfgGet(cwd, exports.SHARED_HOST_CONFIG_KEY, undefined);
|
|
396
|
+
let host;
|
|
397
|
+
if (typeof rawHostCfg === 'string' && rawHostCfg.trim()) {
|
|
398
|
+
host = rawHostCfg.trim();
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
if (rawHostCfg !== undefined) {
|
|
402
|
+
const rawStr = typeof rawHostCfg === 'string' ? rawHostCfg : JSON.stringify(rawHostCfg);
|
|
403
|
+
const echoed = sanitizeForWarning(rawStr ?? '');
|
|
404
|
+
warnings.push(`Unrecognized ${exports.SHARED_HOST_CONFIG_KEY} value "${echoed}" — expected a non-blank string. Falling back to ${exports.PLATFORM_HOST_CONFIG_KEY} or the provider default.`.slice(0, 200));
|
|
405
|
+
}
|
|
406
|
+
const hostProfile = (0, org_profile_cjs_1.readProfileValue)(cwd, exports.SHARED_HOST_CONFIG_KEY);
|
|
407
|
+
pushDeduped(warnings, hostProfile.warnings);
|
|
408
|
+
if (hostProfile.value !== undefined) {
|
|
409
|
+
host = hostProfile.value;
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
const platformRes = resolvePlatformHost(cwd);
|
|
413
|
+
if (platformRes.host !== null) {
|
|
414
|
+
host = platformRes.host;
|
|
415
|
+
pushDeduped(warnings, platformRes.warnings);
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
host = defaultHostForProvider(provider);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
// ─ repo (D-06 order, D-13/WR-01 suppression kinds) ─
|
|
423
|
+
let repo;
|
|
424
|
+
let suppressed = false;
|
|
425
|
+
let suppressedKind = null;
|
|
426
|
+
let suppressedReason = null;
|
|
427
|
+
const rawRepoCfg = cfgGet(cwd, exports.SHARED_REPO_CONFIG_KEY, undefined);
|
|
428
|
+
if (typeof rawRepoCfg === 'string') {
|
|
429
|
+
if (rawRepoCfg === '') {
|
|
430
|
+
// D-13/WR-01: an explicit empty string is a DECISION, not an absence —
|
|
431
|
+
// the org profile must NOT be consulted here (D-05 never lets a
|
|
432
|
+
// profile outrank an explicitly set value, and an explicit '' is a set
|
|
433
|
+
// value). No warning: there is nothing wrong to report.
|
|
434
|
+
repo = null;
|
|
435
|
+
suppressed = true;
|
|
436
|
+
suppressedKind = 'explicit-opt-out';
|
|
437
|
+
suppressedReason = exports.SHARED_OPT_OUT_REASON;
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
repo = rawRepoCfg;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
if (rawRepoCfg !== undefined) {
|
|
445
|
+
const echoed = sanitizeForWarning(JSON.stringify(rawRepoCfg) ?? '');
|
|
446
|
+
warnings.push(`Unrecognized ${exports.SHARED_REPO_CONFIG_KEY} value "${echoed}" — expected a string. Falling through to the org profile / built-in default.`.slice(0, 200));
|
|
447
|
+
}
|
|
448
|
+
// D-07: the org profile fills the key when config left it absent — this
|
|
449
|
+
// is the real consultation that supersedes the frozen stub call.
|
|
450
|
+
const repoProfile = (0, org_profile_cjs_1.readProfileValue)(cwd, exports.SHARED_REPO_CONFIG_KEY);
|
|
451
|
+
pushDeduped(warnings, repoProfile.warnings);
|
|
452
|
+
if (repoProfile.value !== undefined) {
|
|
453
|
+
repo = repoProfile.value;
|
|
454
|
+
// suppressed/suppressedKind/suppressedReason stay at their
|
|
455
|
+
// false/null/null defaults: applyBuiltInDefault withholds only the
|
|
456
|
+
// BUILT-IN default, never a configured profile value.
|
|
457
|
+
}
|
|
458
|
+
else if (provider === 'gitlab') {
|
|
459
|
+
// Nothing was suppressed either way: with the flag true the built-in
|
|
460
|
+
// default applied, and with it false the caller asked for no default
|
|
461
|
+
// and got none. This MUST stay false under both flag values — making
|
|
462
|
+
// it true would put a suppression note on every GitLab project's
|
|
463
|
+
// promotion preview, precisely the regression ROADMAP SC3 exists to
|
|
464
|
+
// prevent.
|
|
465
|
+
repo = applyBuiltInDefault ? exports.DEFAULT_SHARED_REPO : null;
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
// D-13/WR-01 fix: withholding the built-in default is why the write
|
|
469
|
+
// path could not previously see this state — the resolver now
|
|
470
|
+
// describes the state itself, regardless of applyBuiltInDefault,
|
|
471
|
+
// rather than describing what it did about it. The WARNING stays
|
|
472
|
+
// gated on applyBuiltInDefault exactly as before, so the read path's
|
|
473
|
+
// warning list is byte-identical and the write path (which passes
|
|
474
|
+
// applyBuiltInDefault: false) gains no new warning.
|
|
475
|
+
repo = null;
|
|
476
|
+
suppressed = true;
|
|
477
|
+
suppressedKind = 'provider-default-unavailable';
|
|
478
|
+
suppressedReason = `no shared vault configured for provider "${provider}"`;
|
|
479
|
+
if (applyBuiltInDefault) {
|
|
480
|
+
warnings.push(`The built-in shared vault default applies only to the gitlab provider — set ${exports.SHARED_REPO_CONFIG_KEY} explicitly to use a shared vault on this provider.`.slice(0, 200));
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return { repo, ref, host, warnings, suppressed, suppressedKind, suppressedReason };
|
|
485
|
+
}
|