rcf-lite 0.17.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 +60 -0
- package/blueprints/application-api-rest/README.md +5 -1
- 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/contributions/requirements/application-api-rest-req-006.json +4 -4
- package/blueprints/application-api-rest/contributions/tacs/tac-306-application-api-rest-operability.json +9 -8
- package/blueprints/application-api-rest/contributions/user-stories/application-api-rest-us-2103.json +3 -3
- package/blueprints/application-api-rest/contributions/user-stories/application-api-rest-us-2108.json +27 -27
- package/blueprints/application-api-rest/docs/topics.md +6 -4
- package/blueprints/application-api-rest/guide/application-api-rest.md +5 -1
- 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 +4 -2
- package/blueprints/delivery-ci-workflows/docs/topics.md +2 -2
- package/blueprints/deploy-cloudflare-workers/docs/topics.md +2 -2
- package/blueprints/email-smtp-resend/docs/topics.md +2 -2
- package/blueprints/observability-essentials/README.md +6 -2
- package/blueprints/observability-essentials/blueprint.json +133 -33
- package/blueprints/observability-essentials/contributions/adrs/adr-801-observability-essentials-health-probes.json +4 -4
- package/blueprints/observability-essentials/contributions/adrs/adr-802-observability-essentials-readiness-semantics.json +4 -4
- package/blueprints/observability-essentials/contributions/requirements/observability-essentials-req-001.json +4 -4
- package/blueprints/observability-essentials/contributions/requirements/observability-essentials-req-002.json +4 -4
- package/blueprints/observability-essentials/contributions/tacs/tac-801-observability-essentials-liveness-probe.json +10 -9
- package/blueprints/observability-essentials/contributions/tacs/tac-802-observability-essentials-readiness-probe.json +17 -11
- package/blueprints/observability-essentials/contributions/user-stories/observability-essentials-us-7101.json +12 -3
- package/blueprints/observability-essentials/contributions/user-stories/observability-essentials-us-7102.json +12 -3
- package/blueprints/observability-essentials/docs/topics.md +15 -8
- package/blueprints/observability-essentials/guide/observability-essentials.md +9 -3
- 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/blueprints/observability-probe-endpoints/README.md +5 -1
- package/blueprints/observability-probe-endpoints/blueprint.json +116 -24
- package/blueprints/observability-probe-endpoints/contributions/adrs/adr-1503-observability-probe-endpoints-kubernetes-default.json +5 -5
- package/blueprints/observability-probe-endpoints/contributions/user-stories/observability-probe-endpoints-us-14102.json +11 -2
- package/blueprints/observability-probe-endpoints/contributions/user-stories/observability-probe-endpoints-us-14107.json +11 -2
- package/blueprints/observability-probe-endpoints/docs/topics.md +6 -6
- package/blueprints/observability-probe-endpoints/guide/observability-probe-endpoints.md +10 -0
- package/blueprints/persistence-data-d1/docs/topics.md +2 -2
- package/blueprints/persistence-data-sqlite/docs/topics.md +2 -2
- package/blueprints/security-auth-clerk/docs/topics.md +2 -2
- package/blueprints/security-auth-keycloak/docs/topics.md +2 -2
- package/blueprints/security-auth-magic-link/docs/topics.md +2 -2
- package/blueprints/security-auth-oauth2/docs/topics.md +2 -2
- package/blueprints/security-secrets-management/docs/topics.md +2 -2
- 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-074.json +19 -0
- package/rcf/code-nodes/cn-075.json +15 -0
- package/rcf/code-nodes/cn-076.json +14 -0
- 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-024.json +24 -0
- package/rcf/fbs/fbs-025.json +25 -0
- package/rcf/fbs/fbs-026.json +27 -0
- package/rcf/fbs/fbs-027.json +25 -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-012.json +22 -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-034.json +32 -0
- package/rcf/test-suites/ts-035.json +23 -0
- package/rcf/test-suites/ts-036.json +65 -0
- package/rcf/test-suites/ts-037.json +55 -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-1201.json +34 -0
- package/rcf/user-stories/us-1202.json +25 -0
- package/rcf/user-stories/us-1203.json +43 -0
- package/rcf/user-stories/us-1204.json +25 -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 +21 -1
- package/src/blueprint/apply.js +8 -0
- package/src/blueprint/companions.js +485 -0
- package/src/blueprint/index.js +17 -0
- package/src/blueprint/loader.js +243 -1
- package/src/blueprint/remove-resolution.js +104 -0
- package/src/cli/blueprint.js +350 -0
- package/src/cli/doctor.js +76 -1
- package/src/cli/validate.js +7 -0
package/src/cli/blueprint.js
CHANGED
|
@@ -19,10 +19,20 @@ import {
|
|
|
19
19
|
enrichRowsWithCategories,
|
|
20
20
|
groupRowsByCategory,
|
|
21
21
|
listBlueprints,
|
|
22
|
+
loadBlueprint,
|
|
22
23
|
removeBlueprint,
|
|
24
|
+
removeResolution,
|
|
23
25
|
renderDiff,
|
|
24
26
|
resolveBlueprintSource,
|
|
25
27
|
supersedeBlueprintTopic,
|
|
28
|
+
readCompanionsFile,
|
|
29
|
+
setCompanionPin,
|
|
30
|
+
unsetCompanionPin,
|
|
31
|
+
resolveCompanions,
|
|
32
|
+
renderCompanionLines,
|
|
33
|
+
renderAmbiguousLibraryRefusal,
|
|
34
|
+
enumerateShelfProviders,
|
|
35
|
+
enumerateLibraryProviders,
|
|
26
36
|
} from '../blueprint/index.js';
|
|
27
37
|
import { conflictReportJson, renderConflictReport } from '../blueprint/conflicts.js';
|
|
28
38
|
import { handleLibraryVerb, LIBRARY_HELP } from './blueprint-library.js';
|
|
@@ -66,6 +76,40 @@ Verbs:
|
|
|
66
76
|
diff <topic> Side-by-side view of every applied blueprint's
|
|
67
77
|
scope:global ADR on <topic>: id, path, title,
|
|
68
78
|
status, decision. Read-only.
|
|
79
|
+
remove-resolution <adr-id>
|
|
80
|
+
Remove a single manifest.resolutions[] entry by
|
|
81
|
+
its resolvedByAdrId (the id the doctor's
|
|
82
|
+
probe-path-owner check names when a historical
|
|
83
|
+
resolution has become redundant after a
|
|
84
|
+
blueprint upgrade). Writes nothing else: the
|
|
85
|
+
project-level ADR file at rcf/adrs/<adr-id>.json
|
|
86
|
+
is left in place as historical context. Refuses
|
|
87
|
+
exit 2 when <adr-id> is malformed or names no
|
|
88
|
+
ADR on the project tree. Idempotent on re-run:
|
|
89
|
+
when <adr-id> is a well-formed ADR id that
|
|
90
|
+
names an ADR present on the tree but is no
|
|
91
|
+
longer on any resolutions[] entry, prints
|
|
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>.
|
|
69
113
|
library <verb> Manage external blueprint libraries. Sub-verbs:
|
|
70
114
|
add, list, remove, refresh. See
|
|
71
115
|
'rcf define blueprint library --help' for the
|
|
@@ -87,6 +131,21 @@ Options:
|
|
|
87
131
|
--reason <text> (supersede, add --resolve) Optional operator
|
|
88
132
|
note attached to the manifest.resolutions[]
|
|
89
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.
|
|
90
149
|
--json (add only) Emit the result (or conflict
|
|
91
150
|
report) as a machine-readable JSON object.
|
|
92
151
|
Exit code is unchanged (0 on apply, 3 on
|
|
@@ -122,6 +181,9 @@ const OPTION_SPEC = {
|
|
|
122
181
|
'dry-run': { type: 'boolean' },
|
|
123
182
|
quiet: { type: 'boolean' },
|
|
124
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' },
|
|
125
187
|
};
|
|
126
188
|
|
|
127
189
|
/**
|
|
@@ -205,6 +267,25 @@ export async function main(argv, deps = {}) {
|
|
|
205
267
|
stderr.write(`[error] blueprint add: ${resolveDeclarations.error}\n`);
|
|
206
268
|
return 2;
|
|
207
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
|
+
}
|
|
208
289
|
const result = await applyBlueprint({
|
|
209
290
|
projectRoot, tree, source,
|
|
210
291
|
namespaceOverride: parsed.values.namespace,
|
|
@@ -273,6 +354,57 @@ export async function main(argv, deps = {}) {
|
|
|
273
354
|
if (!parsed.values.quiet) {
|
|
274
355
|
stdout.write(`[blueprint] applied '${result.slug}' at ${result.version} (${result.contributions.length} contribution(s)).\n`);
|
|
275
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
|
+
}
|
|
276
408
|
return 0;
|
|
277
409
|
}
|
|
278
410
|
|
|
@@ -364,11 +496,229 @@ export async function main(argv, deps = {}) {
|
|
|
364
496
|
return 0;
|
|
365
497
|
}
|
|
366
498
|
|
|
499
|
+
if (verb === 'remove-resolution') {
|
|
500
|
+
if (rest.length === 0) {
|
|
501
|
+
stderr.write('[error] blueprint remove-resolution: missing <adr-id>\n');
|
|
502
|
+
return 2;
|
|
503
|
+
}
|
|
504
|
+
const adrId = rest[0];
|
|
505
|
+
const result = await removeResolution({
|
|
506
|
+
projectRoot, tree, resolvedByAdrId: adrId,
|
|
507
|
+
dryRun: parsed.values['dry-run'] === true,
|
|
508
|
+
});
|
|
509
|
+
if (isRcfError(result)) {
|
|
510
|
+
stderr.write(`[error] blueprint remove-resolution: ${result.message}\n`);
|
|
511
|
+
return 2;
|
|
512
|
+
}
|
|
513
|
+
if (result.alreadyAbsent) {
|
|
514
|
+
if (!parsed.values.quiet) stdout.write(`[blueprint] nothing to remove: '${adrId}' is not on manifest.resolutions[].\n`);
|
|
515
|
+
return 0;
|
|
516
|
+
}
|
|
517
|
+
if (!parsed.values.quiet) {
|
|
518
|
+
const topicSuffix = result.topic ? ` (topic '${result.topic}')` : '';
|
|
519
|
+
const idSuffix = result.resolutionId ? `; dropped resolution ${result.resolutionId}` : '';
|
|
520
|
+
stdout.write(`[blueprint] removed resolution for '${adrId}'${topicSuffix}${idSuffix}.\n`);
|
|
521
|
+
}
|
|
522
|
+
return 0;
|
|
523
|
+
}
|
|
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
|
+
|
|
367
537
|
stderr.write(`[error] blueprint: unknown verb '${verb}'\n`);
|
|
368
538
|
stderr.write(HELP);
|
|
369
539
|
return 2;
|
|
370
540
|
}
|
|
371
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
|
+
|
|
372
722
|
/**
|
|
373
723
|
* Parse `--resolve <topic>=project:<ADR-id>` occurrences into a list
|
|
374
724
|
* of declarations, attaching an optional shared `reason` (from the
|
package/src/cli/doctor.js
CHANGED
|
@@ -76,6 +76,7 @@ const KNOWN_CHECKS = /** @type {const} */ ([
|
|
|
76
76
|
'browser-present',
|
|
77
77
|
'playwright-mcp-reachable',
|
|
78
78
|
'playwright-mcp-redundant',
|
|
79
|
+
'probe-path-owner',
|
|
79
80
|
]);
|
|
80
81
|
|
|
81
82
|
/** The four Playwright-related checks doctor runs conditionally for
|
|
@@ -102,7 +103,7 @@ Options:
|
|
|
102
103
|
Values: agent-instructions, gitignore,
|
|
103
104
|
knowledge, identity, playwright-present,
|
|
104
105
|
browser-present, playwright-mcp-reachable,
|
|
105
|
-
playwright-mcp-redundant.
|
|
106
|
+
playwright-mcp-redundant, probe-path-owner.
|
|
106
107
|
--json Emit machine-readable envelope: { ok, drift, writes, notices }.
|
|
107
108
|
--quiet Only summary line + first 3 drift items.
|
|
108
109
|
--force Accept a legacy-markers --fix on hand-edited
|
|
@@ -247,6 +248,11 @@ export async function main(argv, deps = {}) {
|
|
|
247
248
|
// API-only project even under an explicit --check ask.
|
|
248
249
|
if (!ctx.browserFacing) continue;
|
|
249
250
|
result = await runPlaywrightMcpRedundantCheck(ctx);
|
|
251
|
+
} else if (check === 'probe-path-owner') {
|
|
252
|
+
// Fires whenever more than one applied blueprint teaches probe paths.
|
|
253
|
+
// Runs on every project (not gated on browser-facing). Spec:
|
|
254
|
+
// projects/rcf-lite-wsd/specs/rcf-lite-probe-path-alignment-spec-2026-09-04.md section 9.
|
|
255
|
+
result = await runProbePathOwnerCheck(ctx);
|
|
250
256
|
} else continue;
|
|
251
257
|
for (const d of result.drift) drift.push({ check, ...d });
|
|
252
258
|
for (const w of result.writes) writes.push(w);
|
|
@@ -705,6 +711,75 @@ async function runPlaywrightMcpRedundantCheck(ctx) {
|
|
|
705
711
|
return { drift, writes: [] };
|
|
706
712
|
}
|
|
707
713
|
|
|
714
|
+
/* ------------------------------------------------------------------ */
|
|
715
|
+
/* Check: probe-path-owner (probe-path alignment spec section 9) */
|
|
716
|
+
/* ------------------------------------------------------------------ */
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Fires when more than one applied blueprint teaches probe paths. Reads
|
|
720
|
+
* `rcf/manifest.json` for the applied-blueprint records; counts how many
|
|
721
|
+
* carry a scope:global ADR on `healthProbes` (the shelf-wide probe-path
|
|
722
|
+
* ownership topic). More than one is drift: names the blueprints and the
|
|
723
|
+
* remedy line naming the one that would have to drop its opinion.
|
|
724
|
+
*
|
|
725
|
+
* Also names any resolutions[] entry the alignment removed as redundant
|
|
726
|
+
* (a project that applied essentials v1.x + probe-endpoints v1.0.0 and
|
|
727
|
+
* resolved the two topics via supersede will have a resolution entry
|
|
728
|
+
* pointing at both blueprint ADRs; after re-applying to essentials v2.0.0
|
|
729
|
+
* the essentials side no longer claims the topic and the resolution is
|
|
730
|
+
* redundant historical context the operator can remove).
|
|
731
|
+
*/
|
|
732
|
+
async function runProbePathOwnerCheck(ctx) {
|
|
733
|
+
const drift = [];
|
|
734
|
+
const manifestPath = join(ctx.projectRoot, 'rcf', 'manifest.json');
|
|
735
|
+
let manifest = null;
|
|
736
|
+
try {
|
|
737
|
+
manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
|
|
738
|
+
} catch { return { drift, writes: [] }; }
|
|
739
|
+
const applied = Array.isArray(manifest?.blueprints) ? manifest.blueprints : [];
|
|
740
|
+
const claimants = { healthProbes: [], readinessSemantics: [] };
|
|
741
|
+
for (const b of applied) {
|
|
742
|
+
for (const c of b?.contributions ?? []) {
|
|
743
|
+
if (c?.kind === 'adr' && c?.scope === 'global' && (c.topic === 'healthProbes' || c.topic === 'readinessSemantics')) {
|
|
744
|
+
claimants[c.topic].push({ slug: b.slug, adrId: c.id });
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
for (const topic of ['healthProbes', 'readinessSemantics']) {
|
|
749
|
+
if (claimants[topic].length > 1) {
|
|
750
|
+
const slugs = claimants[topic].map((x) => x.slug).join(', ');
|
|
751
|
+
const owner = claimants[topic].find((x) => x.slug === 'observability-probe-endpoints');
|
|
752
|
+
const remedy = owner
|
|
753
|
+
? `${claimants[topic].filter((x) => x.slug !== 'observability-probe-endpoints').map((x) => x.slug).join(', ')} would have to drop its scope:global claim on ${topic} (observability-probe-endpoints is the shelf canonical owner from probe-endpoints v1.0.0)`
|
|
754
|
+
: `one of ${slugs} would have to drop its scope:global claim on ${topic}`;
|
|
755
|
+
drift.push({
|
|
756
|
+
item: `multiple-${topic}-owners`,
|
|
757
|
+
file: 'rcf/manifest.json',
|
|
758
|
+
message: `more than one applied blueprint teaches probe paths on topic '${topic}': ${slugs}. ${remedy}. Spec: projects/rcf-lite-wsd/specs/rcf-lite-probe-path-alignment-spec-2026-09-04.md section 9.`,
|
|
759
|
+
refusedByFix: true,
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
// Redundant resolutions: a resolutions[] entry that names both
|
|
764
|
+
// `healthProbes` or `readinessSemantics` where the current claimant
|
|
765
|
+
// count is 1 (or 0) is redundant historical context.
|
|
766
|
+
const resolutions = Array.isArray(manifest?.resolutions) ? manifest.resolutions : [];
|
|
767
|
+
for (const r of resolutions) {
|
|
768
|
+
if (r?.topic === 'healthProbes' || r?.topic === 'readinessSemantics') {
|
|
769
|
+
if (claimants[r.topic].length <= 1) {
|
|
770
|
+
drift.push({
|
|
771
|
+
item: `redundant-${r.topic}-resolution`,
|
|
772
|
+
file: 'rcf/manifest.json',
|
|
773
|
+
message: `resolution '${r?.resolvedByAdrId ?? '(unnamed)'}' on topic '${r.topic}' is now redundant historical context: only ${claimants[r.topic].length} applied blueprint claims the topic after the probe-path alignment. Consider \`rcf define blueprint remove-resolution ${r?.resolvedByAdrId ?? '<id>'}\` or leave the resolution ADR as historical.`,
|
|
774
|
+
refusedByFix: true,
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
return { drift, writes: [] };
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
|
|
708
783
|
/**
|
|
709
784
|
* Default reader for the project-root .mcp.json body. Returns the parsed
|
|
710
785
|
* object, or null on missing / unparseable (doctor treats an unparseable
|
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,
|