rcf-lite 0.13.0 → 0.15.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 +48 -1
- package/bin/rcf.js +3 -1
- package/bin/view-supervisor-child.mjs +0 -0
- package/blueprints/application-api-rest/docs/topics.md +1 -1
- package/blueprints/application-spa/README.md +3 -3
- package/blueprints/application-spa/contributions/adrs/adr-202-application-spa-theming.json +1 -1
- package/blueprints/application-spa/contributions/adrs/adr-206-application-spa-iconography.json +1 -1
- package/blueprints/application-spa/contributions/tacs/tac-207-application-spa-token-adherence-probe.json +7 -7
- package/blueprints/application-spa/contributions/tacs/tac-208-application-spa-icon-adherence-probe.json +7 -7
- package/blueprints/application-spa/contributions/tacs/tac-209-application-spa-csp-styled-adherence-probe.json +8 -8
- package/blueprints/application-spa/contributions/tacs/tac-210-application-spa-external-dependency-provisioning-probe.json +8 -8
- package/blueprints/application-spa/contributions/tacs/tac-211-application-spa-core-flow-e2e-probe.json +8 -8
- package/blueprints/application-spa/contributions/user-stories/application-spa-us-1129.json +2 -2
- package/blueprints/application-spa/contributions/user-stories/application-spa-us-1130.json +2 -2
- package/blueprints/application-spa/contributions/user-stories/application-spa-us-1131.json +2 -2
- package/blueprints/application-spa/contributions/user-stories/application-spa-us-1132.json +2 -2
- package/blueprints/application-spa/contributions/user-stories/application-spa-us-1133.json +3 -3
- package/blueprints/application-spa/docs/topics.md +1 -1
- package/blueprints/delivery-ci-workflows/CHANGELOG.md +39 -0
- package/blueprints/delivery-ci-workflows/README.md +61 -0
- package/blueprints/delivery-ci-workflows/assets/bootstrap/README.md +26 -0
- package/blueprints/delivery-ci-workflows/assets/bootstrap/adr-bootstrap-coverage-supersession.template.json +28 -0
- package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/github-actions/default-branch-checks.yml +61 -0
- package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/github-actions/pull-request-checks.yml +74 -0
- package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/github-actions/release.yml +75 -0
- package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/github-actions/scheduled-audit.yml +65 -0
- package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/notes.md +61 -0
- package/blueprints/{ci-pipeline → delivery-ci-workflows}/assets/report-samples/per-gate.json +3 -2
- package/blueprints/delivery-ci-workflows/blueprint.json +87 -0
- package/blueprints/delivery-ci-workflows/contributions/adrs/adr-701-delivery-ci-workflows-ci-gates.json +30 -0
- package/blueprints/{ci-pipeline/contributions/adrs/adr-702-ci-pipeline-strict-coverage-gate.json → delivery-ci-workflows/contributions/adrs/adr-702-delivery-ci-workflows-strict-coverage-gate.json} +4 -4
- package/blueprints/{ci-pipeline/contributions/adrs/adr-703-ci-pipeline-node-only-runner.json → delivery-ci-workflows/contributions/adrs/adr-703-delivery-ci-workflows-node-only-runner.json} +1 -1
- package/blueprints/{ci-pipeline/contributions/adrs/adr-704-ci-pipeline-report-shape.json → delivery-ci-workflows/contributions/adrs/adr-704-delivery-ci-workflows-report-shape.json} +3 -3
- package/blueprints/delivery-ci-workflows/contributions/adrs/adr-705-delivery-ci-workflows-elicitation-surface.json +25 -0
- package/blueprints/delivery-ci-workflows/contributions/adrs/adr-706-delivery-ci-workflows-branch-model-defaults.json +25 -0
- package/blueprints/delivery-ci-workflows/contributions/adrs/adr-707-delivery-ci-workflows-release-workflow-shape.json +25 -0
- package/blueprints/delivery-ci-workflows/contributions/adrs/adr-708-delivery-ci-workflows-provider-hint-shape.json +25 -0
- package/blueprints/delivery-ci-workflows/contributions/adrs/adr-709-delivery-ci-workflows-release-artefacts.json +25 -0
- package/blueprints/delivery-ci-workflows/contributions/adrs/adr-710-delivery-ci-workflows-scheduled-audit.json +25 -0
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-001.json +18 -0
- package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-002.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-002.json} +2 -2
- package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-003.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-003.json} +2 -2
- package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-004.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-004.json} +2 -2
- package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-005.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-005.json} +4 -4
- package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-006.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-006.json} +2 -2
- package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-007.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-007.json} +2 -2
- package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-008.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-008.json} +2 -2
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-009.json +18 -0
- package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-010.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-010.json} +2 -2
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-011.json +18 -0
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-012.json +18 -0
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-013.json +18 -0
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-014.json +18 -0
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-015.json +18 -0
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-016.json +18 -0
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-017.json +18 -0
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-018.json +18 -0
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-019.json +18 -0
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-020.json +18 -0
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-021.json +18 -0
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-022.json +18 -0
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-023.json +18 -0
- package/blueprints/{ci-pipeline/contributions/tacs/tac-701-ci-pipeline-gate-runner.json → delivery-ci-workflows/contributions/tacs/tac-701-delivery-ci-workflows-gate-runner.json} +5 -5
- package/blueprints/{ci-pipeline/contributions/tacs/tac-702-ci-pipeline-gate-report.json → delivery-ci-workflows/contributions/tacs/tac-702-delivery-ci-workflows-gate-report.json} +1 -1
- package/blueprints/{ci-pipeline/contributions/tacs/tac-703-ci-pipeline-aggregate-report.json → delivery-ci-workflows/contributions/tacs/tac-703-delivery-ci-workflows-aggregate-report.json} +2 -2
- package/blueprints/delivery-ci-workflows/contributions/tacs/tac-704-delivery-ci-workflows-workflow-materialiser.json +67 -0
- package/blueprints/delivery-ci-workflows/contributions/tacs/tac-705-delivery-ci-workflows-release-workflow.json +51 -0
- package/blueprints/delivery-ci-workflows/contributions/tacs/tac-706-delivery-ci-workflows-scheduled-audit.json +38 -0
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6101.json +37 -0
- package/blueprints/{ci-pipeline/contributions/user-stories/ci-pipeline-us-6102.json → delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6102.json} +3 -3
- package/blueprints/{ci-pipeline/contributions/user-stories/ci-pipeline-us-6103.json → delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6103.json} +4 -4
- package/blueprints/{ci-pipeline/contributions/user-stories/ci-pipeline-us-6104.json → delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6104.json} +4 -4
- package/blueprints/{ci-pipeline/contributions/user-stories/ci-pipeline-us-6105.json → delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6105.json} +6 -6
- package/blueprints/{ci-pipeline/contributions/user-stories/ci-pipeline-us-6106.json → delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6106.json} +3 -3
- package/blueprints/{ci-pipeline/contributions/user-stories/ci-pipeline-us-6107.json → delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6107.json} +5 -5
- package/blueprints/{ci-pipeline/contributions/user-stories/ci-pipeline-us-6108.json → delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6108.json} +4 -4
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6109.json +36 -0
- package/blueprints/{ci-pipeline/contributions/user-stories/ci-pipeline-us-6110.json → delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6110.json} +3 -3
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6111.json +36 -0
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6112.json +36 -0
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6113.json +36 -0
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6114.json +46 -0
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6115.json +37 -0
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6116.json +28 -0
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6117.json +28 -0
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6118.json +37 -0
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6119.json +28 -0
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6120.json +28 -0
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6121.json +46 -0
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6122.json +46 -0
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6123.json +37 -0
- package/blueprints/delivery-ci-workflows/docs/topics.md +61 -0
- package/blueprints/delivery-ci-workflows/guide/delivery-ci-workflows.md +174 -0
- package/blueprints/deploy-cloudflare-workers/docs/topics.md +3 -3
- package/blueprints/email-smtp-resend/docs/topics.md +1 -1
- package/blueprints/observability-essentials/README.md +2 -2
- package/blueprints/observability-essentials/docs/topics.md +5 -5
- package/blueprints/observability-probe-endpoints/docs/topics.md +2 -2
- package/blueprints/persistence-data-d1/README.md +2 -2
- package/blueprints/persistence-data-d1/assets/facade-shape/facade-module-shape.md +1 -1
- package/blueprints/persistence-data-d1/contributions/tacs/tac-1403-persistence-data-d1-deploy-gate.json +1 -1
- package/blueprints/persistence-data-d1/docs/topics.md +2 -2
- package/blueprints/persistence-data-d1/guide/persistence-data-d1.md +1 -1
- package/blueprints/persistence-data-sqlite/README.md +1 -1
- package/blueprints/persistence-data-sqlite/docs/topics.md +1 -1
- package/blueprints/security-auth-clerk/README.md +5 -3
- package/blueprints/security-auth-clerk/assets/middleware/workers-fetch-shape.md +123 -0
- package/blueprints/security-auth-clerk/assets/wiring/workers-wrangler-toml-shape.md +51 -0
- package/blueprints/security-auth-clerk/blueprint.json +1 -1
- package/blueprints/security-auth-clerk/docs/topics.md +2 -2
- package/blueprints/security-auth-clerk/guide/security-auth-clerk.md +9 -0
- package/blueprints/security-auth-keycloak/docs/topics.md +2 -2
- package/blueprints/security-auth-magic-link/README.md +1 -1
- package/blueprints/security-auth-magic-link/docs/topics.md +1 -1
- package/blueprints/security-auth-oauth2/README.md +1 -1
- package/blueprints/security-auth-oauth2/docs/topics.md +2 -2
- package/blueprints/security-secrets-management/README.md +1 -1
- package/blueprints/security-secrets-management/docs/topics.md +3 -3
- package/guidance/build-cycle-playbook.md +2 -2
- package/guidance/document-model.md +1 -1
- package/guidance/harness-template.md +13 -0
- package/guidance/managed/agent-instructions-block.hash +1 -1
- package/guidance/managed/agent-instructions-block.md +13 -0
- package/package.json +15 -14
- package/rcf/adrs/adr-001.json +1 -1
- package/rcf/adrs/adr-009.json +1 -1
- package/rcf/build-sequence.json +1 -1
- package/rcf/manifest.json +2 -2
- package/rcf/prd.json +2 -2
- package/releases/releases.yaml +126 -0
- package/src/blueprint/apply.js +60 -13
- package/src/blueprint/index.js +12 -0
- package/src/blueprint/library-loader.js +292 -0
- package/src/blueprint/library-registry.js +341 -0
- package/src/blueprint/list.js +38 -4
- package/src/blueprint/shelf-resolver.js +144 -31
- package/src/blueprint/supersede.js +56 -13
- package/src/cli/blueprint-library.js +447 -0
- package/src/cli/blueprint.js +50 -9
- package/src/cli/guidance.js +1 -1
- package/src/cli/help.js +27 -1
- package/src/cli/version.js +673 -0
- package/src/cli/view.js +282 -1
- package/src/server/index.js +3 -0
- package/src/server/routes.js +15 -1
- package/src/server/scope-endpoint.js +105 -0
- package/src/view/live-client.js +253 -6
- package/src/view/scope.js +231 -0
- package/src/view/style.css +42 -0
- package/blueprints/ci-pipeline/README.md +0 -49
- package/blueprints/ci-pipeline/assets/ci-provider-examples/github-actions.yml +0 -61
- package/blueprints/ci-pipeline/assets/ci-provider-examples/notes.md +0 -50
- package/blueprints/ci-pipeline/blueprint.json +0 -46
- package/blueprints/ci-pipeline/contributions/adrs/adr-701-ci-pipeline-ci-gates.json +0 -25
- package/blueprints/ci-pipeline/contributions/requirements/ci-pipeline-req-001.json +0 -18
- package/blueprints/ci-pipeline/contributions/requirements/ci-pipeline-req-009.json +0 -18
- package/blueprints/ci-pipeline/contributions/user-stories/ci-pipeline-us-6101.json +0 -37
- package/blueprints/ci-pipeline/contributions/user-stories/ci-pipeline-us-6109.json +0 -36
- package/blueprints/ci-pipeline/docs/topics.md +0 -49
- package/blueprints/ci-pipeline/guide/ci-pipeline.md +0 -79
- package/rcf/.identity/profile.md +0 -37
- package/rcf/knowledge/INDEX.md +0 -12
- package/rcf/knowledge/README.md +0 -41
- package/rcf/knowledge/docs/.gitkeep +0 -0
- package/rcf/knowledge/notes/.gitkeep +0 -0
- /package/blueprints/{ci-pipeline → delivery-ci-workflows}/assets/report-samples/pipeline.json +0 -0
|
@@ -14,13 +14,20 @@
|
|
|
14
14
|
// This is what makes option 3 as printed by the reshaped conflict
|
|
15
15
|
// message executable VERBATIM from the refused-add state: the operator
|
|
16
16
|
// runs `rcf define blueprint supersede <topic> --incoming <source>` immediately
|
|
17
|
-
// after the refused add, with zero prep; the verb
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
17
|
+
// after the refused add, with zero prep; the verb resolves the incoming
|
|
18
|
+
// source through `resolveBlueprintSource` (the SAME resolver `add`
|
|
19
|
+
// uses, so `@stock/<slug>`, bare kebab slugs, colon-qualified library
|
|
20
|
+
// refs, and filesystem paths are all accepted here just as they are on
|
|
21
|
+
// `add`), loads the incoming blueprint from disk, finds its
|
|
22
|
+
// scope:global ADR on <topic>, stamps the id into the incoming
|
|
23
|
+
// blueprint's namespace, and uses that {slug, adrId} as the second
|
|
24
|
+
// side of supersedes[]. Round-2 shipped with the writer requiring 2
|
|
25
|
+
// already-applied ADRs, which meant option 3 as printed exited 2 in
|
|
26
|
+
// the refused-add state; the escalation the worker adapted around was
|
|
27
|
+
// that AC-1002-5 could not pass as written. The `--incoming` argument
|
|
28
|
+
// was subsequently fed directly to `loadBlueprint`, which understands
|
|
29
|
+
// paths only — a newcomer who intuited the `@stock/<slug>` form from
|
|
30
|
+
// `add`'s help hit a refusal. Persona re-run 2026-08-31 arc-4, H2.
|
|
24
31
|
//
|
|
25
32
|
// Two side effects, both governed by dryRun:
|
|
26
33
|
// 1. Writes the project ADR file (JSON, minimally valid — the operator
|
|
@@ -46,11 +53,12 @@
|
|
|
46
53
|
import { mkdir, rename, stat, unlink, writeFile } from 'node:fs/promises';
|
|
47
54
|
import { dirname, join } from 'node:path';
|
|
48
55
|
|
|
49
|
-
import { rcfError } from '../core/errors/index.js';
|
|
56
|
+
import { isRcfError, rcfError } from '../core/errors/index.js';
|
|
50
57
|
import { loadBlueprint } from './loader.js';
|
|
51
58
|
import { updateManifest } from './manifest-writer.js';
|
|
52
59
|
import { stampId } from './namespace.js';
|
|
53
60
|
import { nextResolutionId } from './resolutions.js';
|
|
61
|
+
import { resolveBlueprintSource } from './shelf-resolver.js';
|
|
54
62
|
|
|
55
63
|
/**
|
|
56
64
|
* @typedef {object} SupersedeResult
|
|
@@ -79,9 +87,16 @@ import { nextResolutionId } from './resolutions.js';
|
|
|
79
87
|
* @param {Date} [args.now]
|
|
80
88
|
* @param {boolean} [args.dryRun]
|
|
81
89
|
* @param {string} [args.reason]
|
|
90
|
+
* @param {(source: string, opts: { projectRoot: string }) => Promise<import('./shelf-resolver.js').ResolvedSource | import('../core/errors/index.js').RcfError>} [args._resolveSource]
|
|
91
|
+
* Test-only injection point; production callers omit this and the
|
|
92
|
+
* shared `resolveBlueprintSource` runs. Mirrors the same DI on
|
|
93
|
+
* `view/scope.js` so a hermetic test can exercise the `@stock/` and
|
|
94
|
+
* colon-qualified branches without staging the packaged shelf or a
|
|
95
|
+
* library registry.
|
|
82
96
|
* @returns {Promise<SupersedeResult | import('../core/errors/index.js').RcfError>}
|
|
83
97
|
*/
|
|
84
|
-
export async function supersedeBlueprintTopic({ projectRoot, tree, topic, incomingSource, now = new Date(), dryRun = false, reason }) {
|
|
98
|
+
export async function supersedeBlueprintTopic({ projectRoot, tree, topic, incomingSource, now = new Date(), dryRun = false, reason, _resolveSource }) {
|
|
99
|
+
const resolveImpl = _resolveSource ?? resolveBlueprintSource;
|
|
85
100
|
if (typeof topic !== 'string' || topic.trim().length === 0) {
|
|
86
101
|
// Schema minLength:1 accepts whitespace-only; the writer refuses
|
|
87
102
|
// it up-front so a whitespace-only topic never lands on disk.
|
|
@@ -122,8 +137,27 @@ export async function supersedeBlueprintTopic({ projectRoot, tree, topic, incomi
|
|
|
122
137
|
// incomingSource is informational only (skipped silently unless it
|
|
123
138
|
// would add a distinct {slug, adrId} pair, in which case it is
|
|
124
139
|
// appended for a >= 3-blueprint scenario).
|
|
140
|
+
//
|
|
141
|
+
// The incoming source is routed through the SAME resolver `add` uses
|
|
142
|
+
// (`resolveBlueprintSource`), so `@stock/<slug>`, bare kebab slugs,
|
|
143
|
+
// colon-qualified library refs, and filesystem paths are all accepted
|
|
144
|
+
// here just as they are on `add`. Before this routing the verb fed
|
|
145
|
+
// the raw string directly to `loadBlueprint`, which understands paths
|
|
146
|
+
// only; a newcomer following the conflict card's own printed remedy
|
|
147
|
+
// with the ergonomic `@stock/<slug>` form (identical to what `add`
|
|
148
|
+
// accepted) hit a refusal. Persona re-run 2026-08-31 arc-4, H2.
|
|
125
149
|
if (typeof incomingSource === 'string' && incomingSource.length > 0) {
|
|
126
|
-
const
|
|
150
|
+
const resolved = await resolveImpl(incomingSource, { projectRoot });
|
|
151
|
+
if (isRcfError(resolved)) {
|
|
152
|
+
// Wrap the resolver's error under the --incoming banner so the
|
|
153
|
+
// operator sees which flag failed.
|
|
154
|
+
return rcfError({
|
|
155
|
+
kind: resolved.kind,
|
|
156
|
+
message: `--incoming ${incomingSource}: ${resolved.message}`,
|
|
157
|
+
filePath: resolved.filePath,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
const loaded = await loadBlueprint(resolved.resolved);
|
|
127
161
|
if (loaded.kind) {
|
|
128
162
|
// Preserve the loader's own rcfError but drop any 'blueprint: '
|
|
129
163
|
// narrator prefix so the CLI's `[error] blueprint supersede: `
|
|
@@ -134,6 +168,15 @@ export async function supersedeBlueprintTopic({ projectRoot, tree, topic, incomi
|
|
|
134
168
|
filePath: loaded.filePath,
|
|
135
169
|
});
|
|
136
170
|
}
|
|
171
|
+
// For library-qualified sources the applied identity is rewired
|
|
172
|
+
// under the library prefix (`<libraryPrefix>-<blueprintSlug>`), so
|
|
173
|
+
// the supersedes[] entry must reference that effective slug and
|
|
174
|
+
// stamp the ADR id under it — matching what `apply.js` writes to
|
|
175
|
+
// `manifest.blueprints[].slug`. For shelf / path sources the
|
|
176
|
+
// blueprint's own slug applies.
|
|
177
|
+
const effectiveSlug = resolved.kind === 'library'
|
|
178
|
+
? resolved.effectiveSlug
|
|
179
|
+
: loaded.slug;
|
|
137
180
|
let matched = null;
|
|
138
181
|
for (const c of loaded.contributions ?? []) {
|
|
139
182
|
if (c.kind === 'adr' && c.scope === 'global' && c.topic === topic) {
|
|
@@ -144,14 +187,14 @@ export async function supersedeBlueprintTopic({ projectRoot, tree, topic, incomi
|
|
|
144
187
|
if (!matched) {
|
|
145
188
|
return rcfError({
|
|
146
189
|
kind: 'usage',
|
|
147
|
-
message: `--incoming ${incomingSource}: blueprint '${
|
|
190
|
+
message: `--incoming ${incomingSource}: blueprint '${effectiveSlug}' declares no scope:global ADR on topic '${topic}'.`,
|
|
148
191
|
});
|
|
149
192
|
}
|
|
150
|
-
const stamped = stampId(matched.id,
|
|
193
|
+
const stamped = stampId(matched.id, effectiveSlug);
|
|
151
194
|
if ('error' in stamped) {
|
|
152
195
|
return rcfError({ kind: 'validation', message: `--incoming ${incomingSource}: ${stamped.error}` });
|
|
153
196
|
}
|
|
154
|
-
const incomingPair = { slug:
|
|
197
|
+
const incomingPair = { slug: effectiveSlug, adrId: stamped.id, path: `rcf/adrs/${stamped.id.toLowerCase()}.json` };
|
|
155
198
|
// Dedupe against the applied side: an incoming blueprint that is
|
|
156
199
|
// also currently applied (unusual — refused-add state means it is
|
|
157
200
|
// NOT applied) would otherwise be double-listed.
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
// `rcf define blueprint library <verb>` CLI.
|
|
2
|
+
//
|
|
3
|
+
// Verbs (Phase 2b):
|
|
4
|
+
// add register an external library on this project (local source in
|
|
5
|
+
// 2b; network fetchers land in 2c)
|
|
6
|
+
// list list registered libraries
|
|
7
|
+
// remove unregister a library (refuses when any applied blueprint on
|
|
8
|
+
// the project came through the library)
|
|
9
|
+
// refresh re-validate an already-registered library's on-disk shape
|
|
10
|
+
//
|
|
11
|
+
// Spec: external-blueprint-libraries-spec-2026-08-31.md sections 4, 8.
|
|
12
|
+
|
|
13
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
14
|
+
import { stat } from 'node:fs/promises';
|
|
15
|
+
import { createInterface } from 'node:readline';
|
|
16
|
+
|
|
17
|
+
import { isRcfError } from '#core/errors';
|
|
18
|
+
import { findProjectRoot } from '../view/index.js';
|
|
19
|
+
import { loadLibrary } from '../blueprint/library-loader.js';
|
|
20
|
+
import {
|
|
21
|
+
detectBandOverlap,
|
|
22
|
+
detectPrefixCollision,
|
|
23
|
+
findLibrary,
|
|
24
|
+
loadCoreBandReservations,
|
|
25
|
+
readLibraryRegistry,
|
|
26
|
+
writeLibraryRegistry,
|
|
27
|
+
} from '../blueprint/library-registry.js';
|
|
28
|
+
import { knownShelfSlugs, packagedShelfPath } from '../blueprint/shelf-resolver.js';
|
|
29
|
+
import { walkTree } from '#core/store';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Union of core-shelf blueprint slugs used for the prefix-collision
|
|
33
|
+
* gate. The packaged-shelf directory is copied into the tarball at
|
|
34
|
+
* prepack time (`files: ['blueprints']`), so a source checkout does
|
|
35
|
+
* not have `packages/rcf-lite/blueprints/` on disk; falling back to
|
|
36
|
+
* the shipped `data/core-band-reservations.json` keeps the gate
|
|
37
|
+
* effective in both shapes.
|
|
38
|
+
*/
|
|
39
|
+
async function collectCoreSlugs() {
|
|
40
|
+
const fromShelf = await knownShelfSlugs(packagedShelfPath()).catch(() => []);
|
|
41
|
+
const reservations = await loadCoreBandReservations();
|
|
42
|
+
const slugs = new Set(fromShelf);
|
|
43
|
+
for (const row of reservations.ac ?? []) if (row.blueprint) slugs.add(row.blueprint);
|
|
44
|
+
for (const row of reservations.suffixBlocks ?? []) if (row.blueprint) slugs.add(row.blueprint);
|
|
45
|
+
return [...slugs];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const LIBRARY_HELP = `Usage: rcf define blueprint library <verb> [options]
|
|
49
|
+
|
|
50
|
+
Verbs:
|
|
51
|
+
add <ref> Register an external library on this project.
|
|
52
|
+
<ref> is either a local absolute or relative path
|
|
53
|
+
to a library root (a directory containing
|
|
54
|
+
library.json). Fetches metadata, runs review-on-
|
|
55
|
+
add, and writes an entry to
|
|
56
|
+
rcf/blueprint-libraries.json.
|
|
57
|
+
|
|
58
|
+
Phase 2b covers local sources; the git and
|
|
59
|
+
tarball fetchers land in Phase 2c. A private-repo
|
|
60
|
+
library today is registered via a local clone.
|
|
61
|
+
|
|
62
|
+
list [--json] List every registered library on this project
|
|
63
|
+
(prefix, source, publisher, blueprint count).
|
|
64
|
+
|
|
65
|
+
remove <prefix> Unregister a library. Refuses when any applied
|
|
66
|
+
blueprint on the project came through this
|
|
67
|
+
library.
|
|
68
|
+
|
|
69
|
+
refresh <prefix> Re-validate an already-registered library's
|
|
70
|
+
on-disk shape. Phase 2b re-reads the local
|
|
71
|
+
library.json and reports any drift from the
|
|
72
|
+
registry snapshot; phase 2c will re-fetch a git
|
|
73
|
+
or tarball source and verify the sha.
|
|
74
|
+
|
|
75
|
+
Options:
|
|
76
|
+
--prefix <slug> (add) Override the library's declared prefix.
|
|
77
|
+
Rarely needed; only useful when two libraries
|
|
78
|
+
collide on prefix locally.
|
|
79
|
+
--i-have-reviewed (add) Skip the interactive review-on-add prompt.
|
|
80
|
+
Required companion to --no-review when scripting;
|
|
81
|
+
the two-flag form keeps a library-add trust
|
|
82
|
+
decision loud (spec §9.7).
|
|
83
|
+
--no-review (add) Bypass the interactive prompt. Requires
|
|
84
|
+
--i-have-reviewed.
|
|
85
|
+
--json (add, list) Emit machine-readable JSON.
|
|
86
|
+
--dry-run Print intended writes without executing.
|
|
87
|
+
--quiet Suppress non-error stdout.
|
|
88
|
+
--help Print this help.
|
|
89
|
+
`;
|
|
90
|
+
|
|
91
|
+
const LIBRARY_OPTION_SPEC = {
|
|
92
|
+
prefix: { type: 'string' },
|
|
93
|
+
'i-have-reviewed': { type: 'boolean' },
|
|
94
|
+
'no-review': { type: 'boolean' },
|
|
95
|
+
json: { type: 'boolean' },
|
|
96
|
+
'dry-run': { type: 'boolean' },
|
|
97
|
+
quiet: { type: 'boolean' },
|
|
98
|
+
help: { type: 'boolean' },
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Handle `rcf define blueprint library <verb>`.
|
|
103
|
+
*
|
|
104
|
+
* @param {import('node:util').ParseArgsConfig} parsed - already-parsed argv (see cli/blueprint.js)
|
|
105
|
+
* @param {string[]} rest - positional args after `library`
|
|
106
|
+
* @param {object} deps
|
|
107
|
+
* @returns {Promise<number>}
|
|
108
|
+
*/
|
|
109
|
+
export async function handleLibraryVerb(parsed, rest, deps) {
|
|
110
|
+
const stdout = deps.stdout;
|
|
111
|
+
const stderr = deps.stderr;
|
|
112
|
+
const cwd = deps.cwd;
|
|
113
|
+
const now = deps.now ?? new Date();
|
|
114
|
+
const stdin = deps.stdin ?? process.stdin;
|
|
115
|
+
|
|
116
|
+
if (parsed.values.help || rest.length === 0) {
|
|
117
|
+
stdout.write(LIBRARY_HELP);
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
120
|
+
const verb = rest[0];
|
|
121
|
+
const args = rest.slice(1);
|
|
122
|
+
|
|
123
|
+
const projectRoot = await findProjectRoot(cwd);
|
|
124
|
+
if (!projectRoot) {
|
|
125
|
+
stderr.write('[error] no rcf/ tree found in this directory or any ancestor.\n');
|
|
126
|
+
return 2;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (verb === 'add') return handleAdd({ args, parsed, projectRoot, now, stdout, stderr, stdin });
|
|
130
|
+
if (verb === 'list') return handleList({ parsed, projectRoot, stdout, stderr });
|
|
131
|
+
if (verb === 'remove') return handleRemove({ args, parsed, projectRoot, stdout, stderr });
|
|
132
|
+
if (verb === 'refresh') return handleRefresh({ args, parsed, projectRoot, stdout, stderr });
|
|
133
|
+
|
|
134
|
+
stderr.write(`[error] blueprint library: unknown verb '${verb}'\n`);
|
|
135
|
+
stderr.write(LIBRARY_HELP);
|
|
136
|
+
return 2;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function handleAdd({ args, parsed, projectRoot, now, stdout, stderr, stdin }) {
|
|
140
|
+
if (args.length === 0) {
|
|
141
|
+
stderr.write('[error] blueprint library add: missing <ref>\n');
|
|
142
|
+
return 2;
|
|
143
|
+
}
|
|
144
|
+
const ref = args[0];
|
|
145
|
+
|
|
146
|
+
// Phase 2b: local sources only. Fetchers land in 2c.
|
|
147
|
+
const kind = classifySourceRef(ref);
|
|
148
|
+
if (kind !== 'local') {
|
|
149
|
+
stderr.write(
|
|
150
|
+
`[error] blueprint library add: source kind '${kind}' requires the network fetchers landing in Phase 2c. `
|
|
151
|
+
+ 'In 2b, register the library from a local path (a directory carrying library.json). '
|
|
152
|
+
+ 'For private git repos today, clone the repo locally and point add at the clone.\n',
|
|
153
|
+
);
|
|
154
|
+
return 2;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const localRoot = isAbsolute(ref) ? ref : resolve(projectRoot, ref);
|
|
158
|
+
let libStat;
|
|
159
|
+
try {
|
|
160
|
+
libStat = await stat(localRoot);
|
|
161
|
+
} catch (err) {
|
|
162
|
+
stderr.write(`[error] blueprint library add: path '${localRoot}' cannot be read: ${err.message}\n`);
|
|
163
|
+
return 2;
|
|
164
|
+
}
|
|
165
|
+
if (!libStat.isDirectory()) {
|
|
166
|
+
stderr.write(`[error] blueprint library add: path '${localRoot}' is not a directory.\n`);
|
|
167
|
+
return 2;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const library = await loadLibrary(localRoot, { validateBlueprints: true });
|
|
171
|
+
if (isRcfError(library)) {
|
|
172
|
+
stderr.write(`[error] blueprint library add: ${library.message}\n`);
|
|
173
|
+
return 2;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const libraryPrefix = typeof parsed.values.prefix === 'string' && parsed.values.prefix.length > 0
|
|
177
|
+
? parsed.values.prefix
|
|
178
|
+
: library.libraryPrefix;
|
|
179
|
+
|
|
180
|
+
const registry = await readLibraryRegistry(projectRoot);
|
|
181
|
+
if (isRcfError(registry)) {
|
|
182
|
+
stderr.write(`[error] blueprint library add: ${registry.message}\n`);
|
|
183
|
+
return 2;
|
|
184
|
+
}
|
|
185
|
+
if (findLibrary(registry, libraryPrefix)) {
|
|
186
|
+
stderr.write(`[error] blueprint library add: libraryPrefix '${libraryPrefix}' is already registered on this project. Use 'library refresh' or 'library remove' first.\n`);
|
|
187
|
+
return 2;
|
|
188
|
+
}
|
|
189
|
+
const coreSlugs = await collectCoreSlugs();
|
|
190
|
+
const prefixErr = detectPrefixCollision({ libraryPrefix, registry, coreSlugs });
|
|
191
|
+
if (prefixErr) {
|
|
192
|
+
stderr.write(`[error] blueprint library add: ${prefixErr.message}\n`);
|
|
193
|
+
return 2;
|
|
194
|
+
}
|
|
195
|
+
const coreReservations = await loadCoreBandReservations();
|
|
196
|
+
const bandErr = detectBandOverlap({
|
|
197
|
+
candidate: { libraryPrefix, bands: library.bands },
|
|
198
|
+
registry,
|
|
199
|
+
coreReservations,
|
|
200
|
+
});
|
|
201
|
+
if (bandErr) {
|
|
202
|
+
stderr.write(`[error] blueprint library add: ${bandErr.message}\n`);
|
|
203
|
+
return 2;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const wantsReview = parsed.values['no-review'] !== true;
|
|
207
|
+
const iHaveReviewed = parsed.values['i-have-reviewed'] === true;
|
|
208
|
+
if (parsed.values['no-review'] === true && !iHaveReviewed) {
|
|
209
|
+
stderr.write('[error] blueprint library add: --no-review requires --i-have-reviewed (spec §9.7: the two-flag form keeps the trust decision loud).\n');
|
|
210
|
+
return 2;
|
|
211
|
+
}
|
|
212
|
+
if (wantsReview) {
|
|
213
|
+
printReview({ stdout, ref, library, libraryPrefix, coreReservations });
|
|
214
|
+
if (!iHaveReviewed) {
|
|
215
|
+
const proceed = await prompt(stdin, stdout, 'Proceed with add? [y/N] ');
|
|
216
|
+
if (!/^y(es)?$/i.test((proceed ?? '').trim())) {
|
|
217
|
+
stdout.write('[blueprint library] aborted by operator; no registry entry written.\n');
|
|
218
|
+
return 0;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const entry = {
|
|
224
|
+
libraryPrefix,
|
|
225
|
+
sourceKind: 'local',
|
|
226
|
+
sourceRef: localRoot,
|
|
227
|
+
displayName: library.displayName,
|
|
228
|
+
publisher: { ...library.publisher },
|
|
229
|
+
libraryRef: library.libraryRef,
|
|
230
|
+
bands: library.bands,
|
|
231
|
+
blueprints: library.blueprints.map((b) => ({ slug: b.slug, path: b.path })),
|
|
232
|
+
addedAt: now.toISOString(),
|
|
233
|
+
reviewedBy: 'operator',
|
|
234
|
+
provenance: { tier: 'local' },
|
|
235
|
+
cachePath: localRoot,
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const nextRegistry = {
|
|
239
|
+
registryVersion: registry.registryVersion,
|
|
240
|
+
libraries: [...registry.libraries, entry],
|
|
241
|
+
};
|
|
242
|
+
const write = await writeLibraryRegistry(projectRoot, nextRegistry, { dryRun: parsed.values['dry-run'] === true });
|
|
243
|
+
if (isRcfError(write)) {
|
|
244
|
+
stderr.write(`[error] blueprint library add: ${write.message}\n`);
|
|
245
|
+
return 2;
|
|
246
|
+
}
|
|
247
|
+
if (parsed.values.json) {
|
|
248
|
+
stdout.write(`${JSON.stringify({
|
|
249
|
+
added: !parsed.values['dry-run'],
|
|
250
|
+
dryRun: parsed.values['dry-run'] === true,
|
|
251
|
+
libraryPrefix: entry.libraryPrefix,
|
|
252
|
+
blueprintCount: entry.blueprints.length,
|
|
253
|
+
registryPath: write.path,
|
|
254
|
+
})}\n`);
|
|
255
|
+
return 0;
|
|
256
|
+
}
|
|
257
|
+
if (!parsed.values.quiet) {
|
|
258
|
+
if (parsed.values['dry-run']) {
|
|
259
|
+
stdout.write(`[blueprint library] dry-run: would add '${libraryPrefix}' (${entry.blueprints.length} blueprint(s)) to ${write.path}.\n`);
|
|
260
|
+
} else {
|
|
261
|
+
stdout.write(`[blueprint library] added '${libraryPrefix}' (${entry.blueprints.length} blueprint(s)) to ${write.path}.\n`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return 0;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function handleList({ parsed, projectRoot, stdout, stderr }) {
|
|
268
|
+
const registry = await readLibraryRegistry(projectRoot);
|
|
269
|
+
if (isRcfError(registry)) {
|
|
270
|
+
stderr.write(`[error] blueprint library list: ${registry.message}\n`);
|
|
271
|
+
return 2;
|
|
272
|
+
}
|
|
273
|
+
if (parsed.values.json) {
|
|
274
|
+
stdout.write(`${JSON.stringify({
|
|
275
|
+
registryVersion: registry.registryVersion,
|
|
276
|
+
libraries: registry.libraries.map((l) => ({
|
|
277
|
+
libraryPrefix: l.libraryPrefix,
|
|
278
|
+
sourceKind: l.sourceKind,
|
|
279
|
+
sourceRef: l.sourceRef,
|
|
280
|
+
publisher: l.publisher,
|
|
281
|
+
libraryRef: l.libraryRef,
|
|
282
|
+
blueprintCount: Array.isArray(l.blueprints) ? l.blueprints.length : 0,
|
|
283
|
+
addedAt: l.addedAt,
|
|
284
|
+
})),
|
|
285
|
+
}, null, 2)}\n`);
|
|
286
|
+
return 0;
|
|
287
|
+
}
|
|
288
|
+
if (registry.libraries.length === 0) {
|
|
289
|
+
if (!parsed.values.quiet) stdout.write('[blueprint library] no libraries registered on this project.\n');
|
|
290
|
+
return 0;
|
|
291
|
+
}
|
|
292
|
+
for (const l of registry.libraries) {
|
|
293
|
+
const count = Array.isArray(l.blueprints) ? l.blueprints.length : 0;
|
|
294
|
+
stdout.write(`${l.libraryPrefix}\t${l.sourceKind}\t${l.publisher.displayName}\t${count} blueprint(s)\t${l.sourceRef}\n`);
|
|
295
|
+
}
|
|
296
|
+
return 0;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function handleRemove({ args, parsed, projectRoot, stdout, stderr }) {
|
|
300
|
+
if (args.length === 0) {
|
|
301
|
+
stderr.write('[error] blueprint library remove: missing <libraryPrefix>\n');
|
|
302
|
+
return 2;
|
|
303
|
+
}
|
|
304
|
+
const libraryPrefix = args[0];
|
|
305
|
+
const registry = await readLibraryRegistry(projectRoot);
|
|
306
|
+
if (isRcfError(registry)) {
|
|
307
|
+
stderr.write(`[error] blueprint library remove: ${registry.message}\n`);
|
|
308
|
+
return 2;
|
|
309
|
+
}
|
|
310
|
+
const entry = findLibrary(registry, libraryPrefix);
|
|
311
|
+
if (!entry) {
|
|
312
|
+
stderr.write(`[error] blueprint library remove: library '${libraryPrefix}' is not registered.\n`);
|
|
313
|
+
return 2;
|
|
314
|
+
}
|
|
315
|
+
// Refuse when any applied blueprint on the project came through the
|
|
316
|
+
// library. The ownership fact lives on the record itself as
|
|
317
|
+
// `libraryPrefix` (stamped at apply-time when the apply resolved
|
|
318
|
+
// through this registry). Records applied before the field shipped
|
|
319
|
+
// (@stravica-ai/rcf-schemas 0.5.1) carry no `libraryPrefix`; for
|
|
320
|
+
// those we fall back to the pre-field signal, `source` starting with
|
|
321
|
+
// `<prefix>:`. Preferring the record over the string match makes the
|
|
322
|
+
// ownership durable across registry edits: a library re-registered
|
|
323
|
+
// under a different prefix does not orphan the records applied under
|
|
324
|
+
// the previous prefix.
|
|
325
|
+
const { tree, errors } = await walkTree({ projectRoot });
|
|
326
|
+
if (errors.length > 0) {
|
|
327
|
+
for (const e of errors) stderr.write(`[tree] ${e.kind}: ${e.message}\n`);
|
|
328
|
+
return 2;
|
|
329
|
+
}
|
|
330
|
+
const referring = (tree.manifest?.blueprints ?? []).filter((b) => {
|
|
331
|
+
if (typeof b.libraryPrefix === 'string' && b.libraryPrefix.length > 0) {
|
|
332
|
+
return b.libraryPrefix === libraryPrefix;
|
|
333
|
+
}
|
|
334
|
+
return typeof b.source === 'string' && b.source.startsWith(`${libraryPrefix}:`);
|
|
335
|
+
});
|
|
336
|
+
if (referring.length > 0) {
|
|
337
|
+
stderr.write(`[error] blueprint library remove: ${referring.length} applied blueprint(s) came through '${libraryPrefix}':\n`);
|
|
338
|
+
for (const r of referring) stderr.write(` ${r.slug} <- ${r.source}\n`);
|
|
339
|
+
stderr.write('remove those first (rcf define blueprint remove <slug>) or explicitly force with an escalation flag (not in v1).\n');
|
|
340
|
+
return 3;
|
|
341
|
+
}
|
|
342
|
+
const nextRegistry = {
|
|
343
|
+
registryVersion: registry.registryVersion,
|
|
344
|
+
libraries: registry.libraries.filter((l) => l.libraryPrefix !== libraryPrefix),
|
|
345
|
+
};
|
|
346
|
+
const write = await writeLibraryRegistry(projectRoot, nextRegistry, { dryRun: parsed.values['dry-run'] === true });
|
|
347
|
+
if (isRcfError(write)) {
|
|
348
|
+
stderr.write(`[error] blueprint library remove: ${write.message}\n`);
|
|
349
|
+
return 2;
|
|
350
|
+
}
|
|
351
|
+
if (!parsed.values.quiet) {
|
|
352
|
+
stdout.write(`[blueprint library] removed '${libraryPrefix}' from ${write.path}.\n`);
|
|
353
|
+
}
|
|
354
|
+
return 0;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function handleRefresh({ args, parsed, projectRoot, stdout, stderr }) {
|
|
358
|
+
if (args.length === 0) {
|
|
359
|
+
stderr.write('[error] blueprint library refresh: missing <libraryPrefix>\n');
|
|
360
|
+
return 2;
|
|
361
|
+
}
|
|
362
|
+
const libraryPrefix = args[0];
|
|
363
|
+
const registry = await readLibraryRegistry(projectRoot);
|
|
364
|
+
if (isRcfError(registry)) {
|
|
365
|
+
stderr.write(`[error] blueprint library refresh: ${registry.message}\n`);
|
|
366
|
+
return 2;
|
|
367
|
+
}
|
|
368
|
+
const entry = findLibrary(registry, libraryPrefix);
|
|
369
|
+
if (!entry) {
|
|
370
|
+
stderr.write(`[error] blueprint library refresh: library '${libraryPrefix}' is not registered.\n`);
|
|
371
|
+
return 2;
|
|
372
|
+
}
|
|
373
|
+
if (entry.sourceKind !== 'local') {
|
|
374
|
+
stderr.write(`[error] blueprint library refresh: source kind '${entry.sourceKind}' requires the network fetchers landing in Phase 2c.\n`);
|
|
375
|
+
return 2;
|
|
376
|
+
}
|
|
377
|
+
const library = await loadLibrary(entry.cachePath, { validateBlueprints: true });
|
|
378
|
+
if (isRcfError(library)) {
|
|
379
|
+
stderr.write(`[error] blueprint library refresh: ${library.message}\n`);
|
|
380
|
+
return 2;
|
|
381
|
+
}
|
|
382
|
+
// Detect drift from the registry snapshot on load-bearing fields.
|
|
383
|
+
const drifted = [];
|
|
384
|
+
if (library.libraryPrefix !== entry.libraryPrefix) drifted.push(`libraryPrefix '${entry.libraryPrefix}' -> '${library.libraryPrefix}'`);
|
|
385
|
+
if (library.libraryRef !== entry.libraryRef) drifted.push(`libraryRef '${entry.libraryRef}' -> '${library.libraryRef}'`);
|
|
386
|
+
if (JSON.stringify(library.bands) !== JSON.stringify(entry.bands)) drifted.push('bands changed');
|
|
387
|
+
if (drifted.length > 0) {
|
|
388
|
+
stderr.write(`[blueprint library refresh] '${libraryPrefix}' has drifted from the registered snapshot:\n`);
|
|
389
|
+
for (const d of drifted) stderr.write(` ${d}\n`);
|
|
390
|
+
stderr.write("re-run 'rcf define blueprint library add <ref>' to pick up the newer library (spec §10.1: freshness is advisory, adoption is an operator act).\n");
|
|
391
|
+
return 3;
|
|
392
|
+
}
|
|
393
|
+
if (!parsed.values.quiet) {
|
|
394
|
+
stdout.write(`[blueprint library] '${libraryPrefix}' refresh clean: on-disk library matches the registry snapshot.\n`);
|
|
395
|
+
}
|
|
396
|
+
return 0;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function classifySourceRef(ref) {
|
|
400
|
+
if (ref.startsWith('git+') || ref.startsWith('git@') || ref.startsWith('https://') || ref.startsWith('http://') || ref.startsWith('ssh://')) return 'git';
|
|
401
|
+
if (ref.endsWith('.tar.gz') || ref.endsWith('.tgz') || ref.endsWith('.tar')) return 'tarball';
|
|
402
|
+
return 'local';
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function printReview({ stdout, ref, library, libraryPrefix, coreReservations }) {
|
|
406
|
+
const suffix = (library.bands.suffixBlocks ?? []).map((b) => `${b.kind} ${b.start}-${b.end}`).join(', ');
|
|
407
|
+
stdout.write(`\nREVIEW - you are about to add this library to the project registry.\n\n`);
|
|
408
|
+
stdout.write(` Library : ${library.displayName}\n`);
|
|
409
|
+
stdout.write(` Prefix : ${libraryPrefix}\n`);
|
|
410
|
+
stdout.write(` Publisher : ${library.publisher.displayName}${library.publisher.contact ? ` <${library.publisher.contact}>` : ''}\n`);
|
|
411
|
+
stdout.write(` Source : ${ref} (local)\n`);
|
|
412
|
+
stdout.write(` Library ref : ${library.libraryRef}\n`);
|
|
413
|
+
stdout.write(` AC band : ${library.bands.ac.start} - ${library.bands.ac.end}\n`);
|
|
414
|
+
if (suffix.length > 0) stdout.write(` Suffix blocks: ${suffix}\n`);
|
|
415
|
+
stdout.write(`\n Blueprints on this library:\n`);
|
|
416
|
+
for (const bp of library.blueprints) {
|
|
417
|
+
stdout.write(` ${libraryPrefix}:${bp.slug}\n`);
|
|
418
|
+
}
|
|
419
|
+
// Spec §8.1: surface the scope:global ADR topics each blueprint
|
|
420
|
+
// claims so the operator sees, at the review moment, which cross-
|
|
421
|
+
// library / cross-core trust-boundary interactions this add commits
|
|
422
|
+
// them to. Suppressed only when no blueprint on the library claims
|
|
423
|
+
// any global topic (the render would otherwise be a lonely header).
|
|
424
|
+
const withTopics = library.blueprints.filter((bp) => Array.isArray(bp.globalTopics) && bp.globalTopics.length > 0);
|
|
425
|
+
if (withTopics.length > 0) {
|
|
426
|
+
const qualifiedWidth = Math.max(...withTopics.map((bp) => `${libraryPrefix}:${bp.slug}`.length));
|
|
427
|
+
stdout.write(`\n Global topics these blueprints claim (may conflict with core or with other libraries):\n`);
|
|
428
|
+
for (const bp of withTopics) {
|
|
429
|
+
const qualified = `${libraryPrefix}:${bp.slug}`;
|
|
430
|
+
stdout.write(` ${qualified.padEnd(qualifiedWidth)} -> ${bp.globalTopics.join(', ')}\n`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
stdout.write(`\n Provenance : local (dev use)\n`);
|
|
434
|
+
stdout.write(` Band check : cross-checked ${coreReservations.ac.length} core AC row(s), ${coreReservations.suffixBlocks.length} core suffix block(s); no overlap.\n`);
|
|
435
|
+
stdout.write(` Prefix check : '${libraryPrefix}' does not collide with any core slug.\n`);
|
|
436
|
+
stdout.write(`\n`);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async function prompt(stdin, stdout, question) {
|
|
440
|
+
return new Promise((resolveP) => {
|
|
441
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
442
|
+
rl.question(question, (answer) => {
|
|
443
|
+
rl.close();
|
|
444
|
+
resolveP(answer);
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
}
|