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.
Files changed (166) hide show
  1. package/CHANGELOG.md +48 -1
  2. package/bin/rcf.js +3 -1
  3. package/bin/view-supervisor-child.mjs +0 -0
  4. package/blueprints/application-api-rest/docs/topics.md +1 -1
  5. package/blueprints/application-spa/README.md +3 -3
  6. package/blueprints/application-spa/contributions/adrs/adr-202-application-spa-theming.json +1 -1
  7. package/blueprints/application-spa/contributions/adrs/adr-206-application-spa-iconography.json +1 -1
  8. package/blueprints/application-spa/contributions/tacs/tac-207-application-spa-token-adherence-probe.json +7 -7
  9. package/blueprints/application-spa/contributions/tacs/tac-208-application-spa-icon-adherence-probe.json +7 -7
  10. package/blueprints/application-spa/contributions/tacs/tac-209-application-spa-csp-styled-adherence-probe.json +8 -8
  11. package/blueprints/application-spa/contributions/tacs/tac-210-application-spa-external-dependency-provisioning-probe.json +8 -8
  12. package/blueprints/application-spa/contributions/tacs/tac-211-application-spa-core-flow-e2e-probe.json +8 -8
  13. package/blueprints/application-spa/contributions/user-stories/application-spa-us-1129.json +2 -2
  14. package/blueprints/application-spa/contributions/user-stories/application-spa-us-1130.json +2 -2
  15. package/blueprints/application-spa/contributions/user-stories/application-spa-us-1131.json +2 -2
  16. package/blueprints/application-spa/contributions/user-stories/application-spa-us-1132.json +2 -2
  17. package/blueprints/application-spa/contributions/user-stories/application-spa-us-1133.json +3 -3
  18. package/blueprints/application-spa/docs/topics.md +1 -1
  19. package/blueprints/delivery-ci-workflows/CHANGELOG.md +39 -0
  20. package/blueprints/delivery-ci-workflows/README.md +61 -0
  21. package/blueprints/delivery-ci-workflows/assets/bootstrap/README.md +26 -0
  22. package/blueprints/delivery-ci-workflows/assets/bootstrap/adr-bootstrap-coverage-supersession.template.json +28 -0
  23. package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/github-actions/default-branch-checks.yml +61 -0
  24. package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/github-actions/pull-request-checks.yml +74 -0
  25. package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/github-actions/release.yml +75 -0
  26. package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/github-actions/scheduled-audit.yml +65 -0
  27. package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/notes.md +61 -0
  28. package/blueprints/{ci-pipeline → delivery-ci-workflows}/assets/report-samples/per-gate.json +3 -2
  29. package/blueprints/delivery-ci-workflows/blueprint.json +87 -0
  30. package/blueprints/delivery-ci-workflows/contributions/adrs/adr-701-delivery-ci-workflows-ci-gates.json +30 -0
  31. 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
  32. 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
  33. 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
  34. package/blueprints/delivery-ci-workflows/contributions/adrs/adr-705-delivery-ci-workflows-elicitation-surface.json +25 -0
  35. package/blueprints/delivery-ci-workflows/contributions/adrs/adr-706-delivery-ci-workflows-branch-model-defaults.json +25 -0
  36. package/blueprints/delivery-ci-workflows/contributions/adrs/adr-707-delivery-ci-workflows-release-workflow-shape.json +25 -0
  37. package/blueprints/delivery-ci-workflows/contributions/adrs/adr-708-delivery-ci-workflows-provider-hint-shape.json +25 -0
  38. package/blueprints/delivery-ci-workflows/contributions/adrs/adr-709-delivery-ci-workflows-release-artefacts.json +25 -0
  39. package/blueprints/delivery-ci-workflows/contributions/adrs/adr-710-delivery-ci-workflows-scheduled-audit.json +25 -0
  40. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-001.json +18 -0
  41. package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-002.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-002.json} +2 -2
  42. package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-003.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-003.json} +2 -2
  43. package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-004.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-004.json} +2 -2
  44. package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-005.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-005.json} +4 -4
  45. package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-006.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-006.json} +2 -2
  46. package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-007.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-007.json} +2 -2
  47. package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-008.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-008.json} +2 -2
  48. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-009.json +18 -0
  49. package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-010.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-010.json} +2 -2
  50. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-011.json +18 -0
  51. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-012.json +18 -0
  52. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-013.json +18 -0
  53. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-014.json +18 -0
  54. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-015.json +18 -0
  55. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-016.json +18 -0
  56. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-017.json +18 -0
  57. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-018.json +18 -0
  58. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-019.json +18 -0
  59. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-020.json +18 -0
  60. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-021.json +18 -0
  61. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-022.json +18 -0
  62. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-023.json +18 -0
  63. 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
  64. 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
  65. 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
  66. package/blueprints/delivery-ci-workflows/contributions/tacs/tac-704-delivery-ci-workflows-workflow-materialiser.json +67 -0
  67. package/blueprints/delivery-ci-workflows/contributions/tacs/tac-705-delivery-ci-workflows-release-workflow.json +51 -0
  68. package/blueprints/delivery-ci-workflows/contributions/tacs/tac-706-delivery-ci-workflows-scheduled-audit.json +38 -0
  69. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6101.json +37 -0
  70. 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
  71. 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
  72. 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
  73. 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
  74. 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
  75. 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
  76. 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
  77. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6109.json +36 -0
  78. 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
  79. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6111.json +36 -0
  80. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6112.json +36 -0
  81. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6113.json +36 -0
  82. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6114.json +46 -0
  83. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6115.json +37 -0
  84. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6116.json +28 -0
  85. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6117.json +28 -0
  86. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6118.json +37 -0
  87. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6119.json +28 -0
  88. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6120.json +28 -0
  89. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6121.json +46 -0
  90. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6122.json +46 -0
  91. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6123.json +37 -0
  92. package/blueprints/delivery-ci-workflows/docs/topics.md +61 -0
  93. package/blueprints/delivery-ci-workflows/guide/delivery-ci-workflows.md +174 -0
  94. package/blueprints/deploy-cloudflare-workers/docs/topics.md +3 -3
  95. package/blueprints/email-smtp-resend/docs/topics.md +1 -1
  96. package/blueprints/observability-essentials/README.md +2 -2
  97. package/blueprints/observability-essentials/docs/topics.md +5 -5
  98. package/blueprints/observability-probe-endpoints/docs/topics.md +2 -2
  99. package/blueprints/persistence-data-d1/README.md +2 -2
  100. package/blueprints/persistence-data-d1/assets/facade-shape/facade-module-shape.md +1 -1
  101. package/blueprints/persistence-data-d1/contributions/tacs/tac-1403-persistence-data-d1-deploy-gate.json +1 -1
  102. package/blueprints/persistence-data-d1/docs/topics.md +2 -2
  103. package/blueprints/persistence-data-d1/guide/persistence-data-d1.md +1 -1
  104. package/blueprints/persistence-data-sqlite/README.md +1 -1
  105. package/blueprints/persistence-data-sqlite/docs/topics.md +1 -1
  106. package/blueprints/security-auth-clerk/README.md +5 -3
  107. package/blueprints/security-auth-clerk/assets/middleware/workers-fetch-shape.md +123 -0
  108. package/blueprints/security-auth-clerk/assets/wiring/workers-wrangler-toml-shape.md +51 -0
  109. package/blueprints/security-auth-clerk/blueprint.json +1 -1
  110. package/blueprints/security-auth-clerk/docs/topics.md +2 -2
  111. package/blueprints/security-auth-clerk/guide/security-auth-clerk.md +9 -0
  112. package/blueprints/security-auth-keycloak/docs/topics.md +2 -2
  113. package/blueprints/security-auth-magic-link/README.md +1 -1
  114. package/blueprints/security-auth-magic-link/docs/topics.md +1 -1
  115. package/blueprints/security-auth-oauth2/README.md +1 -1
  116. package/blueprints/security-auth-oauth2/docs/topics.md +2 -2
  117. package/blueprints/security-secrets-management/README.md +1 -1
  118. package/blueprints/security-secrets-management/docs/topics.md +3 -3
  119. package/guidance/build-cycle-playbook.md +2 -2
  120. package/guidance/document-model.md +1 -1
  121. package/guidance/harness-template.md +13 -0
  122. package/guidance/managed/agent-instructions-block.hash +1 -1
  123. package/guidance/managed/agent-instructions-block.md +13 -0
  124. package/package.json +15 -14
  125. package/rcf/adrs/adr-001.json +1 -1
  126. package/rcf/adrs/adr-009.json +1 -1
  127. package/rcf/build-sequence.json +1 -1
  128. package/rcf/manifest.json +2 -2
  129. package/rcf/prd.json +2 -2
  130. package/releases/releases.yaml +126 -0
  131. package/src/blueprint/apply.js +60 -13
  132. package/src/blueprint/index.js +12 -0
  133. package/src/blueprint/library-loader.js +292 -0
  134. package/src/blueprint/library-registry.js +341 -0
  135. package/src/blueprint/list.js +38 -4
  136. package/src/blueprint/shelf-resolver.js +144 -31
  137. package/src/blueprint/supersede.js +56 -13
  138. package/src/cli/blueprint-library.js +447 -0
  139. package/src/cli/blueprint.js +50 -9
  140. package/src/cli/guidance.js +1 -1
  141. package/src/cli/help.js +27 -1
  142. package/src/cli/version.js +673 -0
  143. package/src/cli/view.js +282 -1
  144. package/src/server/index.js +3 -0
  145. package/src/server/routes.js +15 -1
  146. package/src/server/scope-endpoint.js +105 -0
  147. package/src/view/live-client.js +253 -6
  148. package/src/view/scope.js +231 -0
  149. package/src/view/style.css +42 -0
  150. package/blueprints/ci-pipeline/README.md +0 -49
  151. package/blueprints/ci-pipeline/assets/ci-provider-examples/github-actions.yml +0 -61
  152. package/blueprints/ci-pipeline/assets/ci-provider-examples/notes.md +0 -50
  153. package/blueprints/ci-pipeline/blueprint.json +0 -46
  154. package/blueprints/ci-pipeline/contributions/adrs/adr-701-ci-pipeline-ci-gates.json +0 -25
  155. package/blueprints/ci-pipeline/contributions/requirements/ci-pipeline-req-001.json +0 -18
  156. package/blueprints/ci-pipeline/contributions/requirements/ci-pipeline-req-009.json +0 -18
  157. package/blueprints/ci-pipeline/contributions/user-stories/ci-pipeline-us-6101.json +0 -37
  158. package/blueprints/ci-pipeline/contributions/user-stories/ci-pipeline-us-6109.json +0 -36
  159. package/blueprints/ci-pipeline/docs/topics.md +0 -49
  160. package/blueprints/ci-pipeline/guide/ci-pipeline.md +0 -79
  161. package/rcf/.identity/profile.md +0 -37
  162. package/rcf/knowledge/INDEX.md +0 -12
  163. package/rcf/knowledge/README.md +0 -41
  164. package/rcf/knowledge/docs/.gitkeep +0 -0
  165. package/rcf/knowledge/notes/.gitkeep +0 -0
  166. /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 loads the incoming
18
- // blueprint from disk, finds its scope:global ADR on <topic>, stamps
19
- // the id into the incoming blueprint's namespace, and uses that
20
- // {slug, adrId} as the second side of supersedes[]. Round-2 shipped
21
- // with the writer requiring 2 already-applied ADRs, which meant option
22
- // 3 as printed exited 2 in the refused-add state; the escalation the
23
- // worker adapted around was that AC-1002-5 could not pass as written.
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 loaded = await loadBlueprint(incomingSource);
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 '${loaded.slug}' declares no scope:global ADR on topic '${topic}'.`,
190
+ message: `--incoming ${incomingSource}: blueprint '${effectiveSlug}' declares no scope:global ADR on topic '${topic}'.`,
148
191
  });
149
192
  }
150
- const stamped = stampId(matched.id, loaded.slug);
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: loaded.slug, adrId: stamped.id, path: `rcf/adrs/${stamped.id.toLowerCase()}.json` };
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
+ }