rcf-lite 0.18.0 → 0.19.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/CHANGELOG.md +38 -0
- package/blueprints/application-api-rest/blueprint.json +12 -4
- package/blueprints/application-api-rest/contributions/adrs/adr-304-application-api-rest-logging.json +3 -3
- package/blueprints/application-api-rest/docs/topics.md +4 -2
- package/blueprints/application-error-handling/README.md +42 -0
- package/blueprints/application-error-handling/assets/schemas/error-record.schema.json +17 -0
- package/blueprints/application-error-handling/blueprint.json +27 -0
- package/blueprints/application-error-handling/contributions/adrs/adr-1701-application-error-handling-record-shape.json +25 -0
- package/blueprints/application-error-handling/contributions/adrs/adr-1702-application-error-handling-classification-vocabulary.json +20 -0
- package/blueprints/application-error-handling/contributions/adrs/adr-1703-application-error-handling-transport-mapping.json +20 -0
- package/blueprints/application-error-handling/contributions/requirements/application-error-handling-req-001.json +15 -0
- package/blueprints/application-error-handling/contributions/requirements/application-error-handling-req-002.json +15 -0
- package/blueprints/application-error-handling/contributions/requirements/application-error-handling-req-003.json +15 -0
- package/blueprints/application-error-handling/contributions/requirements/application-error-handling-req-004.json +15 -0
- package/blueprints/application-error-handling/contributions/tacs/tac-1701-application-error-handling-boundary.json +45 -0
- package/blueprints/application-error-handling/contributions/tacs/tac-1702-application-error-handling-record-factory.json +40 -0
- package/blueprints/application-error-handling/contributions/user-stories/application-error-handling-us-16101.json +25 -0
- package/blueprints/application-error-handling/contributions/user-stories/application-error-handling-us-16102.json +25 -0
- package/blueprints/application-error-handling/contributions/user-stories/application-error-handling-us-16103.json +34 -0
- package/blueprints/application-error-handling/contributions/user-stories/application-error-handling-us-16104.json +25 -0
- package/blueprints/application-error-handling/contributions/user-stories/application-error-handling-us-16105.json +25 -0
- package/blueprints/application-error-handling/contributions/user-stories/application-error-handling-us-16106.json +25 -0
- package/blueprints/application-error-handling/contributions/user-stories/application-error-handling-us-16107.json +25 -0
- package/blueprints/application-error-handling/docs/topics.md +24 -0
- package/blueprints/application-error-handling/guide/application-error-handling.md +35 -0
- package/blueprints/application-spa/blueprint.json +12 -2
- package/blueprints/application-spa/docs/topics.md +2 -0
- package/blueprints/observability-logging/README.md +44 -0
- package/blueprints/observability-logging/assets/samples/log-line.json +13 -0
- package/blueprints/observability-logging/blueprint.json +27 -0
- package/blueprints/observability-logging/contributions/adrs/adr-1601-observability-logging-line-shape.json +25 -0
- package/blueprints/observability-logging/contributions/adrs/adr-1602-observability-logging-correlation-id-header.json +25 -0
- package/blueprints/observability-logging/contributions/adrs/adr-1603-observability-logging-redaction-categories.json +25 -0
- package/blueprints/observability-logging/contributions/adrs/adr-1604-observability-logging-level-vocabulary.json +20 -0
- package/blueprints/observability-logging/contributions/requirements/observability-logging-req-001.json +15 -0
- package/blueprints/observability-logging/contributions/requirements/observability-logging-req-002.json +15 -0
- package/blueprints/observability-logging/contributions/requirements/observability-logging-req-003.json +15 -0
- package/blueprints/observability-logging/contributions/requirements/observability-logging-req-004.json +15 -0
- package/blueprints/observability-logging/contributions/requirements/observability-logging-req-005.json +15 -0
- package/blueprints/observability-logging/contributions/tacs/tac-1601-observability-logging-logger-factory.json +46 -0
- package/blueprints/observability-logging/contributions/tacs/tac-1602-observability-logging-redaction-boundary.json +27 -0
- package/blueprints/observability-logging/contributions/user-stories/observability-logging-us-15101.json +34 -0
- package/blueprints/observability-logging/contributions/user-stories/observability-logging-us-15102.json +43 -0
- package/blueprints/observability-logging/contributions/user-stories/observability-logging-us-15103.json +34 -0
- package/blueprints/observability-logging/contributions/user-stories/observability-logging-us-15104.json +25 -0
- package/blueprints/observability-logging/contributions/user-stories/observability-logging-us-15105.json +34 -0
- package/blueprints/observability-logging/contributions/user-stories/observability-logging-us-15106.json +25 -0
- package/blueprints/observability-logging/contributions/user-stories/observability-logging-us-15107.json +25 -0
- package/blueprints/observability-logging/contributions/user-stories/observability-logging-us-15108.json +25 -0
- package/blueprints/observability-logging/docs/topics.md +21 -0
- package/blueprints/observability-logging/guide/observability-logging.md +36 -0
- package/fixtures/canary-manifest.json +6 -6
- package/guidance/harness-template.md +8 -0
- package/guidance/managed/agent-instructions-block.hash +1 -1
- package/guidance/managed/agent-instructions-block.md +8 -0
- package/package.json +1 -1
- package/rcf/code-nodes/cn-077.json +14 -0
- package/rcf/code-nodes/cn-078.json +15 -0
- package/rcf/code-nodes/cn-079.json +14 -0
- package/rcf/code-nodes/cn-080.json +15 -0
- package/rcf/code-nodes/cn-081.json +15 -0
- package/rcf/code-nodes/cn-082.json +15 -0
- package/rcf/code-nodes/cn-083.json +14 -0
- package/rcf/code-nodes/cn-084.json +15 -0
- package/rcf/code-nodes/cn-085.json +14 -0
- package/rcf/code-nodes/cn-086.json +16 -0
- package/rcf/code-nodes/cn-087.json +14 -0
- package/rcf/code-nodes/cn-088.json +14 -0
- package/rcf/code-nodes/cn-089.json +14 -0
- package/rcf/code-nodes/cn-090.json +14 -0
- package/rcf/code-nodes/cn-091.json +14 -0
- package/rcf/code-nodes/cn-092.json +15 -0
- package/rcf/code-nodes/cn-093.json +15 -0
- package/rcf/code-nodes/cn-094.json +14 -0
- package/rcf/code-nodes/cn-095.json +14 -0
- package/rcf/code-nodes/cn-096.json +14 -0
- package/rcf/code-nodes/cn-097.json +14 -0
- package/rcf/fbs/fbs-028.json +27 -0
- package/rcf/fbs/fbs-029.json +27 -0
- package/rcf/fbs/fbs-030.json +27 -0
- package/rcf/fbs/fbs-031.json +27 -0
- package/rcf/fbs/fbs-032.json +27 -0
- package/rcf/fbs/fbs-033.json +27 -0
- package/rcf/fbs/fbs-034.json +27 -0
- package/rcf/requirements/req-013.json +22 -0
- package/rcf/requirements/req-014.json +22 -0
- package/rcf/requirements/req-015.json +21 -0
- package/rcf/test-suites/ts-038.json +41 -0
- package/rcf/test-suites/ts-039.json +41 -0
- package/rcf/test-suites/ts-040.json +57 -0
- package/rcf/test-suites/ts-041.json +57 -0
- package/rcf/test-suites/ts-042.json +57 -0
- package/rcf/test-suites/ts-043.json +49 -0
- package/rcf/test-suites/ts-044.json +41 -0
- package/rcf/user-stories/us-1301.json +43 -0
- package/rcf/user-stories/us-1302.json +43 -0
- package/rcf/user-stories/us-1401.json +43 -0
- package/rcf/user-stories/us-1402.json +43 -0
- package/rcf/user-stories/us-1403.json +43 -0
- package/rcf/user-stories/us-1404.json +43 -0
- package/rcf/user-stories/us-1501.json +43 -0
- package/releases/releases.yaml +11 -1
- package/src/blueprint/apply.js +8 -0
- package/src/blueprint/companions.js +485 -0
- package/src/blueprint/index.js +16 -0
- package/src/blueprint/loader.js +243 -1
- package/src/cli/blueprint.js +309 -0
- package/src/cli/validate.js +7 -0
package/src/blueprint/loader.js
CHANGED
|
@@ -26,6 +26,24 @@ const CONTRIBUTABLE_KINDS = new Set(['req', 'us', 'tac', 'adr', 'ts', 'cn']);
|
|
|
26
26
|
const ROOT_SINGLETON_KINDS = new Set(['prd', 'tad', 'bs']);
|
|
27
27
|
const EXCLUDED_KINDS = new Set(['fbs']);
|
|
28
28
|
|
|
29
|
+
// Role name grammar for providesRoles[] and suggestedCompanions[].role.
|
|
30
|
+
// Lower camelCase: starts with a lowercase letter, contains only letters
|
|
31
|
+
// and digits, no separators. Same shape as global-topic strings by
|
|
32
|
+
// design (a role name IS the topic name the paired ADR claims, per
|
|
33
|
+
// core-companions spec section 2.2), so `providesRoles: ["logging"]`
|
|
34
|
+
// and the ADR contribution `{"scope":"global","topic":"logging"}`
|
|
35
|
+
// co-locate the two facts on one string.
|
|
36
|
+
const ROLE_NAME_RE = /^[a-z][a-zA-Z0-9]*$/;
|
|
37
|
+
|
|
38
|
+
// Em-dash sentinel + emoji-ish detection for the suggestedCompanions
|
|
39
|
+
// reason string, per the estate-wide banned-tells baseline. Em-dash
|
|
40
|
+
// (U+2014) is refused outright; emojis approximated by a broad
|
|
41
|
+
// symbols / pictographs range. Reason is operator-facing prose, so the
|
|
42
|
+
// same discipline that applies to READMEs and guides applies here.
|
|
43
|
+
// Refused shapes surface as a validation rcfError before apply.
|
|
44
|
+
const EM_DASH = /—/;
|
|
45
|
+
const EMOJI_RE = /[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\u{1F000}-\u{1F2FF}]/u;
|
|
46
|
+
|
|
29
47
|
/**
|
|
30
48
|
* @typedef {object} BlueprintContribution
|
|
31
49
|
* @property {string} id canonical id (bare or already namespaced)
|
|
@@ -33,6 +51,32 @@ const EXCLUDED_KINDS = new Set(['fbs']);
|
|
|
33
51
|
* @property {string} path relative to the blueprint's contributions/
|
|
34
52
|
* @property {'global'} [scope] ADR only; marks whole-project decisions
|
|
35
53
|
* @property {string} [topic] ADR only when scope=global; conflict key
|
|
54
|
+
* @property {boolean} [recommendedDefault] ADR only. Standards-derived
|
|
55
|
+
* discipline (spec 3.2): marks a SHOULD
|
|
56
|
+
* clause, or a choice-shaped MUST per
|
|
57
|
+
* amendment A2 (Baz 2026-09-04T12:20:31Z).
|
|
58
|
+
* @property {boolean} [elicited] ADR only. Marks a MAY clause: the
|
|
59
|
+
* applying operator supplies the value
|
|
60
|
+
* at apply.
|
|
61
|
+
* @property {string} [standardsTraceClause] ADR only. Standard clause
|
|
62
|
+
* identifier verbatim (`WSD-001 clause 3.1`,
|
|
63
|
+
* `RFC 7807 section 3.1`) or the sentinel
|
|
64
|
+
* `"generic enterprise practice"`. Required
|
|
65
|
+
* on every ADR contribution when the
|
|
66
|
+
* blueprint declares standardsTrace[].
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @typedef {object} SuggestedCompanion
|
|
71
|
+
* @property {string} role lower camelCase role name
|
|
72
|
+
* @property {string} reason one-sentence operator-facing reason string
|
|
73
|
+
* (no em-dashes, no emojis)
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @typedef {object} StandardsTraceEntry
|
|
78
|
+
* @property {string} id standard identifier (e.g. `WSD-001`)
|
|
79
|
+
* @property {string} version standard version (free-form)
|
|
36
80
|
*/
|
|
37
81
|
|
|
38
82
|
/**
|
|
@@ -51,6 +95,12 @@ const EXCLUDED_KINDS = new Set(['fbs']);
|
|
|
51
95
|
* vocabulary, so a new category can be
|
|
52
96
|
* minted by adding it to the standard
|
|
53
97
|
* without a code change.
|
|
98
|
+
* @property {string[]} [providesRoles] core-companions spec 2.1
|
|
99
|
+
* @property {SuggestedCompanion[]} [suggestedCompanions] core-companions spec 2.1
|
|
100
|
+
* @property {StandardsTraceEntry[]} [standardsTrace] standards-derived
|
|
101
|
+
* discipline (spec 3.2). When set, every
|
|
102
|
+
* ADR contribution MUST carry a non-null
|
|
103
|
+
* standardsTraceClause.
|
|
54
104
|
* @property {BlueprintContribution[]} contributions
|
|
55
105
|
*/
|
|
56
106
|
|
|
@@ -94,10 +144,33 @@ export async function loadBlueprint(source) {
|
|
|
94
144
|
version: doc.version,
|
|
95
145
|
source: root,
|
|
96
146
|
...(typeof doc.category === 'string' ? { category: doc.category } : {}),
|
|
97
|
-
|
|
147
|
+
...(Array.isArray(doc.providesRoles) ? { providesRoles: doc.providesRoles.slice() } : {}),
|
|
148
|
+
...(Array.isArray(doc.suggestedCompanions)
|
|
149
|
+
? { suggestedCompanions: doc.suggestedCompanions.map((s) => ({ role: s.role, reason: s.reason })) }
|
|
150
|
+
: {}),
|
|
151
|
+
...(Array.isArray(doc.standardsTrace)
|
|
152
|
+
? { standardsTrace: doc.standardsTrace.map((s) => ({ id: s.id, version: s.version })) }
|
|
153
|
+
: {}),
|
|
154
|
+
contributions: Array.isArray(doc.contributions) ? doc.contributions.map(preserveAdrDisciplineFields) : [],
|
|
98
155
|
};
|
|
99
156
|
}
|
|
100
157
|
|
|
158
|
+
// Copy the standards-derived-discipline ADR fields (recommendedDefault,
|
|
159
|
+
// elicited, standardsTraceClause) onto the returned contribution shape
|
|
160
|
+
// verbatim so consumers (apply, tests, tooling) can read them without
|
|
161
|
+
// re-loading the blueprint.json. Non-ADR contributions ignore these
|
|
162
|
+
// fields; the loader does not enforce a kind gate on them because a
|
|
163
|
+
// blueprint author can meaningfully attach recommendedDefault to any
|
|
164
|
+
// ADR-flavoured contribution (the discipline is prose in section 8a,
|
|
165
|
+
// not code, per amendment A2).
|
|
166
|
+
function preserveAdrDisciplineFields(c) {
|
|
167
|
+
const out = { ...c };
|
|
168
|
+
if (out.recommendedDefault !== undefined) out.recommendedDefault = c.recommendedDefault === true;
|
|
169
|
+
if (out.elicited !== undefined) out.elicited = c.elicited === true;
|
|
170
|
+
if (typeof c.standardsTraceClause === 'string') out.standardsTraceClause = c.standardsTraceClause;
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
|
|
101
174
|
function validateMetadata(doc, metaPath) {
|
|
102
175
|
if (typeof doc !== 'object' || doc === null) {
|
|
103
176
|
return rcfError({ kind: 'validation', message: 'blueprint.json must be a JSON object', filePath: metaPath });
|
|
@@ -169,6 +242,175 @@ function validateMetadata(doc, metaPath) {
|
|
|
169
242
|
if (c.scope === 'global' && typeof c.topic !== 'string') {
|
|
170
243
|
return rcfError({ kind: 'validation', message: `blueprint.json: scope=global contribution ${c.id} requires a topic`, filePath: metaPath });
|
|
171
244
|
}
|
|
245
|
+
// Standards-derived discipline (spec 3.2) per-ADR fields. Shape
|
|
246
|
+
// gates only; whether a MUST clause landed on an AC vs an ADR is
|
|
247
|
+
// prose in blueprint-authoring.md section 8a, not code (amendment
|
|
248
|
+
// A2 Baz 2026-09-04T12:20:31Z).
|
|
249
|
+
if (c.recommendedDefault !== undefined && typeof c.recommendedDefault !== 'boolean') {
|
|
250
|
+
return rcfError({ kind: 'validation', message: `blueprint.json: contribution ${c.id} recommendedDefault must be a boolean when set`, filePath: metaPath });
|
|
251
|
+
}
|
|
252
|
+
if (c.elicited !== undefined && typeof c.elicited !== 'boolean') {
|
|
253
|
+
return rcfError({ kind: 'validation', message: `blueprint.json: contribution ${c.id} elicited must be a boolean when set`, filePath: metaPath });
|
|
254
|
+
}
|
|
255
|
+
if (c.standardsTraceClause !== undefined) {
|
|
256
|
+
if (typeof c.standardsTraceClause !== 'string' || c.standardsTraceClause.trim().length === 0) {
|
|
257
|
+
return rcfError({ kind: 'validation', message: `blueprint.json: contribution ${c.id} standardsTraceClause must be a non-empty string when set`, filePath: metaPath });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// Companion-suggestion mechanism fields (core-companions spec 2.1).
|
|
262
|
+
const rolesError = validateProvidesRoles(doc, metaPath);
|
|
263
|
+
if (rolesError) return rolesError;
|
|
264
|
+
const suggestedError = validateSuggestedCompanions(doc, metaPath);
|
|
265
|
+
if (suggestedError) return suggestedError;
|
|
266
|
+
// Paired-ADR gate (spec 2.1): a blueprint declaring a role in
|
|
267
|
+
// providesRoles[] MUST carry a scope:global ADR whose topic string
|
|
268
|
+
// equals the role name. Enforced after per-contribution validation
|
|
269
|
+
// so a mis-authored contribution fails first with the more specific
|
|
270
|
+
// shape error.
|
|
271
|
+
const pairedError = validateProvidesRolesPairedAdrs(doc, metaPath);
|
|
272
|
+
if (pairedError) return pairedError;
|
|
273
|
+
// Standards-derived discipline (spec 3.3): if standardsTrace[] is
|
|
274
|
+
// set, every ADR contribution MUST carry a non-null
|
|
275
|
+
// standardsTraceClause. The loader does NOT cross-check clause
|
|
276
|
+
// severity to kind (per amendment A2); the discipline is prose.
|
|
277
|
+
const stError = validateStandardsTrace(doc, metaPath);
|
|
278
|
+
if (stError) return stError;
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Validate providesRoles[] shape (spec 2.1). Optional; when present
|
|
284
|
+
* must be a non-empty array of lower camelCase strings on the pattern
|
|
285
|
+
* ^[a-z][a-zA-Z0-9]*$.
|
|
286
|
+
*/
|
|
287
|
+
function validateProvidesRoles(doc, metaPath) {
|
|
288
|
+
if (doc.providesRoles === undefined) return null;
|
|
289
|
+
if (!Array.isArray(doc.providesRoles) || doc.providesRoles.length === 0) {
|
|
290
|
+
return rcfError({ kind: 'validation', message: 'blueprint.json: providesRoles must be a non-empty array when set', filePath: metaPath });
|
|
291
|
+
}
|
|
292
|
+
for (let i = 0; i < doc.providesRoles.length; i += 1) {
|
|
293
|
+
const role = doc.providesRoles[i];
|
|
294
|
+
if (typeof role !== 'string' || !ROLE_NAME_RE.test(role)) {
|
|
295
|
+
return rcfError({
|
|
296
|
+
kind: 'validation',
|
|
297
|
+
message: `blueprint.json: providesRoles[${i}] '${role}' is not lower camelCase (^[a-z][a-zA-Z0-9]*$).`,
|
|
298
|
+
filePath: metaPath,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Validate suggestedCompanions[] shape (spec 2.1). Optional; when
|
|
307
|
+
* present must be a non-empty array of `{role, reason}` objects; role
|
|
308
|
+
* is a lower camelCase string; reason is a non-empty string with no
|
|
309
|
+
* em-dashes and no emojis (banned-tells baseline applies to operator-
|
|
310
|
+
* facing prose).
|
|
311
|
+
*/
|
|
312
|
+
function validateSuggestedCompanions(doc, metaPath) {
|
|
313
|
+
if (doc.suggestedCompanions === undefined) return null;
|
|
314
|
+
if (!Array.isArray(doc.suggestedCompanions) || doc.suggestedCompanions.length === 0) {
|
|
315
|
+
return rcfError({ kind: 'validation', message: 'blueprint.json: suggestedCompanions must be a non-empty array when set', filePath: metaPath });
|
|
316
|
+
}
|
|
317
|
+
for (let i = 0; i < doc.suggestedCompanions.length; i += 1) {
|
|
318
|
+
const entry = doc.suggestedCompanions[i];
|
|
319
|
+
if (typeof entry !== 'object' || entry === null) {
|
|
320
|
+
return rcfError({ kind: 'validation', message: `blueprint.json: suggestedCompanions[${i}] must be an object with role and reason`, filePath: metaPath });
|
|
321
|
+
}
|
|
322
|
+
if (typeof entry.role !== 'string' || !ROLE_NAME_RE.test(entry.role)) {
|
|
323
|
+
return rcfError({
|
|
324
|
+
kind: 'validation',
|
|
325
|
+
message: `blueprint.json: suggestedCompanions[${i}].role '${entry.role}' is not lower camelCase (^[a-z][a-zA-Z0-9]*$).`,
|
|
326
|
+
filePath: metaPath,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
if (typeof entry.reason !== 'string' || entry.reason.trim().length === 0) {
|
|
330
|
+
return rcfError({
|
|
331
|
+
kind: 'validation',
|
|
332
|
+
message: `blueprint.json: suggestedCompanions[${i}].reason for role '${entry.role}' must be a non-empty string.`,
|
|
333
|
+
filePath: metaPath,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
if (EM_DASH.test(entry.reason)) {
|
|
337
|
+
return rcfError({
|
|
338
|
+
kind: 'validation',
|
|
339
|
+
message: `blueprint.json: suggestedCompanions[${i}].reason for role '${entry.role}' contains an em-dash; use a comma, a full stop, or a colon.`,
|
|
340
|
+
filePath: metaPath,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
if (EMOJI_RE.test(entry.reason)) {
|
|
344
|
+
return rcfError({
|
|
345
|
+
kind: 'validation',
|
|
346
|
+
message: `blueprint.json: suggestedCompanions[${i}].reason for role '${entry.role}' contains an emoji; operator-facing prose must be plain text.`,
|
|
347
|
+
filePath: metaPath,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Paired-ADR gate for providesRoles (spec 2.1). A blueprint that
|
|
356
|
+
* declares a role MUST also carry a scope:global ADR whose topic
|
|
357
|
+
* string equals the role name. The check runs after per-contribution
|
|
358
|
+
* shape validation so the more specific per-contribution message
|
|
359
|
+
* fires first on a mis-authored file.
|
|
360
|
+
*/
|
|
361
|
+
function validateProvidesRolesPairedAdrs(doc, metaPath) {
|
|
362
|
+
if (!Array.isArray(doc.providesRoles) || doc.providesRoles.length === 0) return null;
|
|
363
|
+
const globalTopics = new Set(
|
|
364
|
+
(doc.contributions ?? [])
|
|
365
|
+
.filter((c) => c.kind === 'adr' && c.scope === 'global' && typeof c.topic === 'string')
|
|
366
|
+
.map((c) => c.topic),
|
|
367
|
+
);
|
|
368
|
+
for (const role of doc.providesRoles) {
|
|
369
|
+
if (!globalTopics.has(role)) {
|
|
370
|
+
return rcfError({
|
|
371
|
+
kind: 'validation',
|
|
372
|
+
message: `blueprint.json: providesRoles[] names '${role}' but no scope:global ADR carries topic '${role}'.`,
|
|
373
|
+
filePath: metaPath,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Standards-derived-discipline gate (spec 3.3). If standardsTrace[]
|
|
382
|
+
* is declared, every ADR contribution MUST carry a non-null
|
|
383
|
+
* standardsTraceClause. No cross-check on severity-to-kind mapping
|
|
384
|
+
* (amendment A2 Baz 2026-09-04T12:20:31Z): the discipline is prose in
|
|
385
|
+
* blueprint-authoring.md section 8a, not code.
|
|
386
|
+
*/
|
|
387
|
+
function validateStandardsTrace(doc, metaPath) {
|
|
388
|
+
if (doc.standardsTrace === undefined) return null;
|
|
389
|
+
if (!Array.isArray(doc.standardsTrace)) {
|
|
390
|
+
return rcfError({ kind: 'validation', message: 'blueprint.json: standardsTrace must be an array when set', filePath: metaPath });
|
|
391
|
+
}
|
|
392
|
+
for (let i = 0; i < doc.standardsTrace.length; i += 1) {
|
|
393
|
+
const entry = doc.standardsTrace[i];
|
|
394
|
+
if (typeof entry !== 'object' || entry === null) {
|
|
395
|
+
return rcfError({ kind: 'validation', message: `blueprint.json: standardsTrace[${i}] must be an object with id and version`, filePath: metaPath });
|
|
396
|
+
}
|
|
397
|
+
if (typeof entry.id !== 'string' || entry.id.trim().length === 0) {
|
|
398
|
+
return rcfError({ kind: 'validation', message: `blueprint.json: standardsTrace[${i}].id must be a non-empty string`, filePath: metaPath });
|
|
399
|
+
}
|
|
400
|
+
if (typeof entry.version !== 'string' || entry.version.trim().length === 0) {
|
|
401
|
+
return rcfError({ kind: 'validation', message: `blueprint.json: standardsTrace[${i}].version must be a non-empty string`, filePath: metaPath });
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const slug = doc.slug;
|
|
405
|
+
for (const c of doc.contributions ?? []) {
|
|
406
|
+
if (c.kind !== 'adr') continue;
|
|
407
|
+
if (typeof c.standardsTraceClause !== 'string' || c.standardsTraceClause.trim().length === 0) {
|
|
408
|
+
return rcfError({
|
|
409
|
+
kind: 'validation',
|
|
410
|
+
message: `blueprint '${slug}' declares standardsTrace but ADR contribution '${c.id}' has no standardsTraceClause; every ADR must reference a standard clause or the sentinel 'generic enterprise practice'.`,
|
|
411
|
+
filePath: metaPath,
|
|
412
|
+
});
|
|
413
|
+
}
|
|
172
414
|
}
|
|
173
415
|
return null;
|
|
174
416
|
}
|
package/src/cli/blueprint.js
CHANGED
|
@@ -19,11 +19,20 @@ import {
|
|
|
19
19
|
enrichRowsWithCategories,
|
|
20
20
|
groupRowsByCategory,
|
|
21
21
|
listBlueprints,
|
|
22
|
+
loadBlueprint,
|
|
22
23
|
removeBlueprint,
|
|
23
24
|
removeResolution,
|
|
24
25
|
renderDiff,
|
|
25
26
|
resolveBlueprintSource,
|
|
26
27
|
supersedeBlueprintTopic,
|
|
28
|
+
readCompanionsFile,
|
|
29
|
+
setCompanionPin,
|
|
30
|
+
unsetCompanionPin,
|
|
31
|
+
resolveCompanions,
|
|
32
|
+
renderCompanionLines,
|
|
33
|
+
renderAmbiguousLibraryRefusal,
|
|
34
|
+
enumerateShelfProviders,
|
|
35
|
+
enumerateLibraryProviders,
|
|
27
36
|
} from '../blueprint/index.js';
|
|
28
37
|
import { conflictReportJson, renderConflictReport } from '../blueprint/conflicts.js';
|
|
29
38
|
import { handleLibraryVerb, LIBRARY_HELP } from './blueprint-library.js';
|
|
@@ -81,6 +90,26 @@ Verbs:
|
|
|
81
90
|
names an ADR present on the tree but is no
|
|
82
91
|
longer on any resolutions[] entry, prints
|
|
83
92
|
"nothing to remove" and exits 0.
|
|
93
|
+
companions <slug> Print the resolved companion set for the
|
|
94
|
+
named applied service blueprint. Reads its
|
|
95
|
+
source blueprint.json's suggestedCompanions[],
|
|
96
|
+
walks the deterministic tier ladder (applied
|
|
97
|
+
> registered library > core shelf, with
|
|
98
|
+
rcf/companions.json pin overriding library +
|
|
99
|
+
shelf), and prints one line per role. Refuses
|
|
100
|
+
exit 3 when two or more registered libraries
|
|
101
|
+
provide the same role with no pin. \`--json\`
|
|
102
|
+
emits a machine-readable envelope.
|
|
103
|
+
companions set <role> <slug>
|
|
104
|
+
Write a project-level pin to rcf/companions.json
|
|
105
|
+
under roles.<role>.provider. <slug> accepts
|
|
106
|
+
\`<libraryPrefix>:<slug>\` for a library provider
|
|
107
|
+
or a bare kebab slug for a shelf provider.
|
|
108
|
+
Refuses exit 2 when the named provider does
|
|
109
|
+
not declare providesRoles containing <role>.
|
|
110
|
+
companions unset <role>
|
|
111
|
+
Remove a pin from rcf/companions.json. Refuses
|
|
112
|
+
exit 2 when no pin exists for <role>.
|
|
84
113
|
library <verb> Manage external blueprint libraries. Sub-verbs:
|
|
85
114
|
add, list, remove, refresh. See
|
|
86
115
|
'rcf define blueprint library --help' for the
|
|
@@ -102,6 +131,21 @@ Options:
|
|
|
102
131
|
--reason <text> (supersede, add --resolve) Optional operator
|
|
103
132
|
note attached to the manifest.resolutions[]
|
|
104
133
|
record.
|
|
134
|
+
--companion <role>=<slug>
|
|
135
|
+
(add only, repeatable) Pin a companion
|
|
136
|
+
provider at apply time; writes the pin to
|
|
137
|
+
rcf/companions.json (schemaVersion 1, current
|
|
138
|
+
pin per role) with pinnedAt. <slug> is
|
|
139
|
+
\`<libraryPrefix>:<slug>\` for a library
|
|
140
|
+
provider or a bare kebab slug for a shelf
|
|
141
|
+
provider. Refuses exit 2 when the named
|
|
142
|
+
provider does not declare providesRoles
|
|
143
|
+
containing <role>.
|
|
144
|
+
--no-companion-suggestions
|
|
145
|
+
(add only) Suppress both the --companion pin
|
|
146
|
+
phase and the post-apply resolved suggestion
|
|
147
|
+
block; every other apply behaviour is
|
|
148
|
+
unchanged.
|
|
105
149
|
--json (add only) Emit the result (or conflict
|
|
106
150
|
report) as a machine-readable JSON object.
|
|
107
151
|
Exit code is unchanged (0 on apply, 3 on
|
|
@@ -137,6 +181,9 @@ const OPTION_SPEC = {
|
|
|
137
181
|
'dry-run': { type: 'boolean' },
|
|
138
182
|
quiet: { type: 'boolean' },
|
|
139
183
|
help: { type: 'boolean' },
|
|
184
|
+
// Companion-suggestion mechanism (core-companions spec section 2).
|
|
185
|
+
companion: { type: 'string', multiple: true },
|
|
186
|
+
'no-companion-suggestions': { type: 'boolean' },
|
|
140
187
|
};
|
|
141
188
|
|
|
142
189
|
/**
|
|
@@ -220,6 +267,25 @@ export async function main(argv, deps = {}) {
|
|
|
220
267
|
stderr.write(`[error] blueprint add: ${resolveDeclarations.error}\n`);
|
|
221
268
|
return 2;
|
|
222
269
|
}
|
|
270
|
+
// Pre-flight --companion selectors (core-companions spec section
|
|
271
|
+
// 5). Validate BEFORE the apply so a bad selector refuses without
|
|
272
|
+
// side effects (the pin write and the applied contributions both
|
|
273
|
+
// stay untouched). --no-companion-suggestions suppresses the
|
|
274
|
+
// pre-flight too.
|
|
275
|
+
const companionSelectorsPre = parseCompanionOptions(parsed.values.companion);
|
|
276
|
+
if (companionSelectorsPre.error) {
|
|
277
|
+
stderr.write(`[error] blueprint add: ${companionSelectorsPre.error}\n`);
|
|
278
|
+
return 2;
|
|
279
|
+
}
|
|
280
|
+
if (!parsed.values['no-companion-suggestions']) {
|
|
281
|
+
for (const sel of companionSelectorsPre.value) {
|
|
282
|
+
const providerCheck = await validateCompanionProvider({ selector: sel, projectRoot, tree });
|
|
283
|
+
if (providerCheck.error) {
|
|
284
|
+
stderr.write(`[error] blueprint add: ${providerCheck.error}\n`);
|
|
285
|
+
return 2;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
223
289
|
const result = await applyBlueprint({
|
|
224
290
|
projectRoot, tree, source,
|
|
225
291
|
namespaceOverride: parsed.values.namespace,
|
|
@@ -288,6 +354,57 @@ export async function main(argv, deps = {}) {
|
|
|
288
354
|
if (!parsed.values.quiet) {
|
|
289
355
|
stdout.write(`[blueprint] applied '${result.slug}' at ${result.version} (${result.contributions.length} contribution(s)).\n`);
|
|
290
356
|
}
|
|
357
|
+
// Companion-suggestion mechanism (spec 2.6). Runs AFTER the apply
|
|
358
|
+
// writes so a failed apply never produces a suggestion block.
|
|
359
|
+
// Composed of two phases: (a) --companion selectors record pins to
|
|
360
|
+
// rcf/companions.json (spec 2.4); (b) print the resolved
|
|
361
|
+
// suggestion block using the current tree + pins + libraries + shelf.
|
|
362
|
+
// Both phases are suppressed by --no-companion-suggestions.
|
|
363
|
+
if (!parsed.values['no-companion-suggestions']) {
|
|
364
|
+
// Re-walk the tree so applied writes are visible to the resolver.
|
|
365
|
+
const { tree: postTree } = await walkTree({ projectRoot });
|
|
366
|
+
// Phase (a): --companion selectors. Pre-flight validated the
|
|
367
|
+
// shape and provider gate before the apply; here we only write
|
|
368
|
+
// the pins.
|
|
369
|
+
for (const sel of companionSelectorsPre.value) {
|
|
370
|
+
const pinRes = await setCompanionPin({ projectRoot, role: sel.role, provider: sel.provider, now });
|
|
371
|
+
if (pinRes.kind) {
|
|
372
|
+
stderr.write(`[error] blueprint add: ${pinRes.message}\n`);
|
|
373
|
+
return 2;
|
|
374
|
+
}
|
|
375
|
+
if (pinRes.previousProvider && pinRes.previousProvider !== sel.provider) {
|
|
376
|
+
stderr.write(`[blueprint] companion pin for role '${sel.role}' updated: '${pinRes.previousProvider}' -> '${sel.provider}'\n`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
// Phase (b): print the resolved suggestion block for the just-
|
|
380
|
+
// applied service blueprint. Skipped if the applied blueprint
|
|
381
|
+
// declared no suggestedCompanions[].
|
|
382
|
+
if (Array.isArray(result.suggestedCompanions) && result.suggestedCompanions.length > 0) {
|
|
383
|
+
const pins = await readCompanionsFile(projectRoot);
|
|
384
|
+
const pinsClean = pins && pins.kind ? null : pins;
|
|
385
|
+
const resolved = await resolveCompanions({
|
|
386
|
+
projectRoot,
|
|
387
|
+
tree: postTree,
|
|
388
|
+
suggestedCompanions: result.suggestedCompanions,
|
|
389
|
+
pins: pinsClean,
|
|
390
|
+
});
|
|
391
|
+
// Surface ambiguous refusal as a hard exit-3 (spec 2.4).
|
|
392
|
+
const ambiguous = resolved.find((r) => r.origin === 'ambiguousLibraries');
|
|
393
|
+
if (ambiguous) {
|
|
394
|
+
stderr.write(renderAmbiguousLibraryRefusal({
|
|
395
|
+
role: ambiguous.role,
|
|
396
|
+
providers: ambiguous.ambiguousProviders,
|
|
397
|
+
serviceSlug: result.slug,
|
|
398
|
+
}));
|
|
399
|
+
return 3;
|
|
400
|
+
}
|
|
401
|
+
if (!parsed.values.quiet) {
|
|
402
|
+
stdout.write(`\nSuggested companions this blueprint recommends alongside it:\n`);
|
|
403
|
+
stdout.write(`${renderCompanionLines(resolved)}\n`);
|
|
404
|
+
stdout.write(`Apply either with: rcf define blueprint add <slug>\n`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
291
408
|
return 0;
|
|
292
409
|
}
|
|
293
410
|
|
|
@@ -405,11 +522,203 @@ export async function main(argv, deps = {}) {
|
|
|
405
522
|
return 0;
|
|
406
523
|
}
|
|
407
524
|
|
|
525
|
+
if (verb === 'companions') {
|
|
526
|
+
return handleCompanionsVerb({
|
|
527
|
+
rest,
|
|
528
|
+
parsed,
|
|
529
|
+
projectRoot,
|
|
530
|
+
tree,
|
|
531
|
+
now,
|
|
532
|
+
stdout,
|
|
533
|
+
stderr,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
408
537
|
stderr.write(`[error] blueprint: unknown verb '${verb}'\n`);
|
|
409
538
|
stderr.write(HELP);
|
|
410
539
|
return 2;
|
|
411
540
|
}
|
|
412
541
|
|
|
542
|
+
/**
|
|
543
|
+
* Parse `--companion <role>=<providerSlug>` occurrences. Slug accepts
|
|
544
|
+
* either `<libraryPrefix>:<slug>` for a library-qualified provider or a
|
|
545
|
+
* bare kebab slug for a shelf provider. Returns { value: [{role,
|
|
546
|
+
* provider}] } on success or { error: string }.
|
|
547
|
+
*/
|
|
548
|
+
function parseCompanionOptions(rawList) {
|
|
549
|
+
if (!Array.isArray(rawList) || rawList.length === 0) return { value: [] };
|
|
550
|
+
const out = [];
|
|
551
|
+
for (const raw of rawList) {
|
|
552
|
+
if (typeof raw !== 'string' || raw.length === 0) {
|
|
553
|
+
return { error: `--companion expects <role>=<slug>, got '${raw}'` };
|
|
554
|
+
}
|
|
555
|
+
const eq = raw.indexOf('=');
|
|
556
|
+
if (eq === -1) {
|
|
557
|
+
return { error: `--companion expects <role>=<slug>, got '${raw}' (missing '=')` };
|
|
558
|
+
}
|
|
559
|
+
const role = raw.slice(0, eq);
|
|
560
|
+
const provider = raw.slice(eq + 1);
|
|
561
|
+
if (!/^[a-z][a-zA-Z0-9]*$/.test(role)) {
|
|
562
|
+
return { error: `--companion role '${role}' is not lower camelCase (^[a-z][a-zA-Z0-9]*$).` };
|
|
563
|
+
}
|
|
564
|
+
if (provider.length === 0) {
|
|
565
|
+
return { error: `--companion provider slug is empty for role '${role}'` };
|
|
566
|
+
}
|
|
567
|
+
out.push({ role, provider });
|
|
568
|
+
}
|
|
569
|
+
return { value: out };
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Validate a --companion selector: locate the named provider blueprint
|
|
574
|
+
* (library-qualified or shelf), load it, and refuse when its
|
|
575
|
+
* `providesRoles[]` does not contain the requested role. Message shape
|
|
576
|
+
* per spec 5.
|
|
577
|
+
*/
|
|
578
|
+
async function validateCompanionProvider({ selector, projectRoot, tree }) {
|
|
579
|
+
const { role, provider } = selector;
|
|
580
|
+
// Applied?
|
|
581
|
+
const applied = (tree?.manifest?.blueprints ?? []).find((bp) => bp.slug === provider);
|
|
582
|
+
if (applied && typeof applied.source === 'string' && !applied.source.includes(':')) {
|
|
583
|
+
const bp = await loadBlueprint(applied.source);
|
|
584
|
+
if (bp.kind) return { error: `--companion ${role}=${provider}: source blueprint failed to load: ${bp.message}` };
|
|
585
|
+
if (Array.isArray(bp.providesRoles) && bp.providesRoles.includes(role)) return { ok: true };
|
|
586
|
+
return { error: `--companion ${role}=${provider}: blueprint '${provider}' does not declare providesRoles containing '${role}'.` };
|
|
587
|
+
}
|
|
588
|
+
// Library-qualified?
|
|
589
|
+
if (provider.includes(':')) {
|
|
590
|
+
const libraries = await enumerateLibraryProviders({ projectRoot, role });
|
|
591
|
+
if (libraries.some((l) => `${l.libraryPrefix}:${l.slug}` === provider)) return { ok: true };
|
|
592
|
+
// Additional diagnostic: check if the library exists but the blueprint does not declare the role
|
|
593
|
+
return { error: `--companion ${role}=${provider}: blueprint '${provider}' does not declare providesRoles containing '${role}'.` };
|
|
594
|
+
}
|
|
595
|
+
// Shelf?
|
|
596
|
+
const shelfProviders = await enumerateShelfProviders(role);
|
|
597
|
+
if (shelfProviders.includes(provider)) return { ok: true };
|
|
598
|
+
return { error: `--companion ${role}=${provider}: blueprint '${provider}' does not declare providesRoles containing '${role}'.` };
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* `rcf define blueprint companions <slug>|set|unset` sub-verb
|
|
603
|
+
* dispatcher (spec 2.5). Read-only against the blueprint tree; set /
|
|
604
|
+
* unset only touch rcf/companions.json.
|
|
605
|
+
*/
|
|
606
|
+
async function handleCompanionsVerb({ rest, parsed, projectRoot, tree, now, stdout, stderr }) {
|
|
607
|
+
const asJson = parsed.values.json === true;
|
|
608
|
+
const quiet = parsed.values.quiet === true;
|
|
609
|
+
if (rest.length === 0) {
|
|
610
|
+
stderr.write('[error] blueprint companions: missing <slug> or sub-verb (set|unset)\n');
|
|
611
|
+
return 2;
|
|
612
|
+
}
|
|
613
|
+
const head = rest[0];
|
|
614
|
+
// set / unset sub-verbs.
|
|
615
|
+
if (head === 'set') {
|
|
616
|
+
if (rest.length < 3) {
|
|
617
|
+
stderr.write('[error] blueprint companions set: expected <role> <slug>\n');
|
|
618
|
+
return 2;
|
|
619
|
+
}
|
|
620
|
+
const role = rest[1];
|
|
621
|
+
const provider = rest[2];
|
|
622
|
+
const validation = await validateCompanionProvider({ selector: { role, provider }, projectRoot, tree });
|
|
623
|
+
if (validation.error) {
|
|
624
|
+
stderr.write(`[error] blueprint companions set: ${validation.error}\n`);
|
|
625
|
+
return 2;
|
|
626
|
+
}
|
|
627
|
+
const res = await setCompanionPin({ projectRoot, role, provider, now });
|
|
628
|
+
if (res.kind) {
|
|
629
|
+
stderr.write(`[error] blueprint companions set: ${res.message}\n`);
|
|
630
|
+
return 2;
|
|
631
|
+
}
|
|
632
|
+
if (asJson) {
|
|
633
|
+
stdout.write(`${JSON.stringify({ ok: true, role, provider, previousProvider: res.previousProvider, pinnedAt: now.toISOString() })}\n`);
|
|
634
|
+
} else if (!quiet) {
|
|
635
|
+
if (res.previousProvider && res.previousProvider !== provider) {
|
|
636
|
+
stdout.write(`[blueprint] companion pin for role '${role}' updated: '${res.previousProvider}' -> '${provider}'\n`);
|
|
637
|
+
} else {
|
|
638
|
+
stdout.write(`[blueprint] companion pin for role '${role}' set to '${provider}'\n`);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return 0;
|
|
642
|
+
}
|
|
643
|
+
if (head === 'unset') {
|
|
644
|
+
if (rest.length < 2) {
|
|
645
|
+
stderr.write('[error] blueprint companions unset: expected <role>\n');
|
|
646
|
+
return 2;
|
|
647
|
+
}
|
|
648
|
+
const role = rest[1];
|
|
649
|
+
const res = await unsetCompanionPin({ projectRoot, role });
|
|
650
|
+
if (res.kind) {
|
|
651
|
+
stderr.write(`[error] blueprint companions unset: ${res.message}\n`);
|
|
652
|
+
return 2;
|
|
653
|
+
}
|
|
654
|
+
if (asJson) {
|
|
655
|
+
stdout.write(`${JSON.stringify({ ok: true, role, removed: true })}\n`);
|
|
656
|
+
} else if (!quiet) {
|
|
657
|
+
stdout.write(`[blueprint] companion pin for role '${role}' removed\n`);
|
|
658
|
+
}
|
|
659
|
+
return 0;
|
|
660
|
+
}
|
|
661
|
+
// <slug> form: print the resolved companion set for an applied
|
|
662
|
+
// service blueprint (spec 2.5 first form).
|
|
663
|
+
const slug = head;
|
|
664
|
+
const applied = (tree?.manifest?.blueprints ?? []).find((bp) => bp.slug === slug);
|
|
665
|
+
if (!applied) {
|
|
666
|
+
stderr.write(`[error] blueprint companions: no applied blueprint with slug '${slug}' on this project.\n`);
|
|
667
|
+
return 2;
|
|
668
|
+
}
|
|
669
|
+
if (typeof applied.source !== 'string' || applied.source.length === 0) {
|
|
670
|
+
stderr.write(`[error] blueprint companions: applied blueprint '${slug}' has no readable source.\n`);
|
|
671
|
+
return 2;
|
|
672
|
+
}
|
|
673
|
+
if (applied.source.includes(':') && !applied.source.startsWith('/')) {
|
|
674
|
+
stderr.write(`[error] blueprint companions: applied blueprint '${slug}' has a library-qualified source '${applied.source}' the companions reader cannot resolve directly; refresh the library and retry, or supply the source path.\n`);
|
|
675
|
+
return 2;
|
|
676
|
+
}
|
|
677
|
+
const bp = await loadBlueprint(applied.source);
|
|
678
|
+
if (bp.kind) {
|
|
679
|
+
stderr.write(`[error] blueprint companions: applied blueprint '${slug}' source failed to load: ${bp.message}\n`);
|
|
680
|
+
return 2;
|
|
681
|
+
}
|
|
682
|
+
if (!Array.isArray(bp.suggestedCompanions) || bp.suggestedCompanions.length === 0) {
|
|
683
|
+
stderr.write(`[error] blueprint companions: applied blueprint '${slug}' declares no suggestedCompanions[].\n`);
|
|
684
|
+
return 2;
|
|
685
|
+
}
|
|
686
|
+
const pins = await readCompanionsFile(projectRoot);
|
|
687
|
+
const pinsClean = pins && pins.kind ? null : pins;
|
|
688
|
+
const resolved = await resolveCompanions({
|
|
689
|
+
projectRoot,
|
|
690
|
+
tree,
|
|
691
|
+
suggestedCompanions: bp.suggestedCompanions,
|
|
692
|
+
pins: pinsClean,
|
|
693
|
+
});
|
|
694
|
+
const ambiguous = resolved.find((r) => r.origin === 'ambiguousLibraries');
|
|
695
|
+
if (ambiguous && !asJson) {
|
|
696
|
+
stderr.write(renderAmbiguousLibraryRefusal({
|
|
697
|
+
role: ambiguous.role,
|
|
698
|
+
providers: ambiguous.ambiguousProviders,
|
|
699
|
+
serviceSlug: slug,
|
|
700
|
+
}));
|
|
701
|
+
return 3;
|
|
702
|
+
}
|
|
703
|
+
if (asJson) {
|
|
704
|
+
stdout.write(`${JSON.stringify({
|
|
705
|
+
slug,
|
|
706
|
+
suggestions: resolved.map((r) => ({
|
|
707
|
+
role: r.role,
|
|
708
|
+
reason: r.reason,
|
|
709
|
+
provider: r.provider,
|
|
710
|
+
origin: r.origin,
|
|
711
|
+
notes: r.notes,
|
|
712
|
+
...(r.ambiguousProviders ? { ambiguousProviders: r.ambiguousProviders } : {}),
|
|
713
|
+
})),
|
|
714
|
+
}, null, 2)}\n`);
|
|
715
|
+
return ambiguous ? 3 : 0;
|
|
716
|
+
}
|
|
717
|
+
stdout.write(`${slug} suggests companions:\n`);
|
|
718
|
+
stdout.write(`${renderCompanionLines(resolved)}\n`);
|
|
719
|
+
return 0;
|
|
720
|
+
}
|
|
721
|
+
|
|
413
722
|
/**
|
|
414
723
|
* Parse `--resolve <topic>=project:<ADR-id>` occurrences into a list
|
|
415
724
|
* of declarations, attaching an optional shared `reason` (from the
|
package/src/cli/validate.js
CHANGED
|
@@ -6,6 +6,7 @@ import { parseArgs } from 'node:util';
|
|
|
6
6
|
|
|
7
7
|
import { formatErrors } from '#core/errors';
|
|
8
8
|
import { checkCodeNodeResolution, walkTree } from '#core/store';
|
|
9
|
+
import { validateCompanionPinsResolvable } from '../blueprint/companions.js';
|
|
9
10
|
import { findProjectRoot } from '../view/index.js';
|
|
10
11
|
|
|
11
12
|
const OPTION_SPEC = {
|
|
@@ -126,6 +127,12 @@ export async function main(argv, deps = {}) {
|
|
|
126
127
|
const staleErrors = await checkCodeNodeResolution({ projectRoot, tree });
|
|
127
128
|
errors.push(...staleErrors);
|
|
128
129
|
}
|
|
130
|
+
// Core-companions spec section 5: rcf/companions.json refusals ride
|
|
131
|
+
// the same exit path as schema / referential-integrity errors. An
|
|
132
|
+
// absent companions.json is not itself an error; a malformed file or
|
|
133
|
+
// a pin that names no known provider is exit 3.
|
|
134
|
+
const companionsErrors = await validateCompanionPinsResolvable({ projectRoot, tree });
|
|
135
|
+
errors.push(...companionsErrors);
|
|
129
136
|
if (flags.json) {
|
|
130
137
|
const issues = errors.map((e) => ({
|
|
131
138
|
id: e.documentId ?? null,
|