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
@@ -0,0 +1,341 @@
1
+ // Project-config external-library registry, at
2
+ // `rcf/blueprint-libraries.json`. Written and read only by the
3
+ // `rcf define blueprint library` verb family; the manifest walker does
4
+ // not touch it (per spec 4.1 rationale: adding a required cross-cutting
5
+ // section into `manifest.json` ripples through every downstream
6
+ // validator, so the registry sits as a discrete file next to the
7
+ // manifest, exactly like `rcf/.identity/` sits next to it).
8
+ //
9
+ // Phase 2b (this file) covers registry CRUD + band-overlap gates.
10
+ // Phase 2c layers the network fetchers on top and populates the
11
+ // `resolvedSha` / `tarballSha256` provenance fields; a `local` source
12
+ // bypasses both and carries a `local` provenance tag.
13
+ //
14
+ // Spec: external-blueprint-libraries-spec-2026-08-31.md sections 4, 7, 8.3.
15
+
16
+ import { existsSync } from 'node:fs';
17
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
18
+ import { dirname, join } from 'node:path';
19
+ import { fileURLToPath } from 'node:url';
20
+
21
+ import { rcfError } from '../core/errors/index.js';
22
+
23
+ export const REGISTRY_VERSION = 1;
24
+ export const REGISTRY_PATH = 'rcf/blueprint-libraries.json';
25
+
26
+ const here = dirname(fileURLToPath(import.meta.url));
27
+ // packages/rcf-lite/src/blueprint -> packages/rcf-lite -> data/
28
+ const CORE_BAND_RESERVATIONS_PATH = join(here, '..', '..', 'data', 'core-band-reservations.json');
29
+
30
+ /**
31
+ * @typedef {object} RegistryEntry
32
+ * @property {string} libraryPrefix
33
+ * @property {'local' | 'git' | 'tarball'} sourceKind
34
+ * @property {string} sourceRef
35
+ * @property {string} [resolvedSha]
36
+ * @property {string} displayName
37
+ * @property {{ id: string, displayName: string, contact?: string }} publisher
38
+ * @property {string} libraryRef
39
+ * @property {{ ac: { start: number, end: number }, suffixBlocks?: Array<{ kind: string, start: number, end: number }> }} bands
40
+ * @property {Array<{ slug: string, path: string }>} [blueprints]
41
+ * Snapshot of the library's `blueprints[]` at add-time. The resolver
42
+ * consults this to locate a blueprint by slug without re-reading
43
+ * `library.json` on every `blueprint add`. Absent snapshot falls back
44
+ * to the conventional `blueprints/<slug>/` layout (see shelf-resolver
45
+ * findBlueprintEntry).
46
+ * @property {string} addedAt RFC 3339
47
+ * @property {'operator'} reviewedBy
48
+ * @property {{ tier: 'local' | 'git' | 'tarball', shaVerifiedAt?: string, tarballSha256?: string }} provenance
49
+ * @property {string} cachePath
50
+ */
51
+
52
+ /**
53
+ * @typedef {object} LibraryRegistry
54
+ * @property {number} registryVersion
55
+ * @property {RegistryEntry[]} libraries
56
+ */
57
+
58
+ /**
59
+ * Read the registry from a project. Absent file is not an error: a
60
+ * project that has never added a library returns an empty registry.
61
+ *
62
+ * @param {string} projectRoot - absolute path
63
+ * @returns {Promise<LibraryRegistry | import('../core/errors/index.js').RcfError>}
64
+ */
65
+ export async function readLibraryRegistry(projectRoot) {
66
+ const path = join(projectRoot, REGISTRY_PATH);
67
+ if (!existsSync(path)) {
68
+ return { registryVersion: REGISTRY_VERSION, libraries: [] };
69
+ }
70
+ let raw;
71
+ try {
72
+ raw = await readFile(path, 'utf8');
73
+ } catch (err) {
74
+ return rcfError({ kind: 'ioFailure', message: `library registry: read failed: ${err.message}`, filePath: path });
75
+ }
76
+ let doc;
77
+ try {
78
+ doc = JSON.parse(raw);
79
+ } catch (err) {
80
+ return rcfError({ kind: 'parseFailure', message: `library registry: JSON parse failed: ${err.message}`, filePath: path });
81
+ }
82
+ const err = validateRegistryShape(doc, path);
83
+ if (err) return err;
84
+ return doc;
85
+ }
86
+
87
+ /**
88
+ * Persist a registry to disk. Overwrites the whole file (registry
89
+ * writes are always full-file for legibility; the file is small).
90
+ *
91
+ * @param {string} projectRoot
92
+ * @param {LibraryRegistry} registry
93
+ * @param {object} [opts]
94
+ * @param {boolean} [opts.dryRun]
95
+ * @returns {Promise<{ written: boolean, path: string } | import('../core/errors/index.js').RcfError>}
96
+ */
97
+ export async function writeLibraryRegistry(projectRoot, registry, opts = {}) {
98
+ const path = join(projectRoot, REGISTRY_PATH);
99
+ const err = validateRegistryShape(registry, path);
100
+ if (err) return err;
101
+ if (opts.dryRun === true) return { written: false, path };
102
+ try {
103
+ await mkdir(dirname(path), { recursive: true });
104
+ await writeFile(path, `${JSON.stringify(registry, null, 2)}\n`, 'utf8');
105
+ } catch (writeErr) {
106
+ return rcfError({ kind: 'ioFailure', message: `library registry: write failed: ${writeErr.message}`, filePath: path });
107
+ }
108
+ return { written: true, path };
109
+ }
110
+
111
+ /**
112
+ * @param {LibraryRegistry} registry
113
+ * @param {string} libraryPrefix
114
+ * @returns {RegistryEntry | undefined}
115
+ */
116
+ export function findLibrary(registry, libraryPrefix) {
117
+ if (!registry || !Array.isArray(registry.libraries)) return undefined;
118
+ return registry.libraries.find((l) => l.libraryPrefix === libraryPrefix);
119
+ }
120
+
121
+ /**
122
+ * Load the core-shelf band reservations shipped alongside rcf-lite.
123
+ * Read-through with cache would be fine, but the file is tiny so we
124
+ * re-read on demand; keeps tests hermetic against a stale singleton.
125
+ *
126
+ * @param {string} [dataFilePath] - override for tests
127
+ * @returns {Promise<{ ac: Array<{ blueprint: string, start: number, end: number }>, suffixBlocks: Array<{ blueprint: string, kind: string, start: number, end: number }> }>}
128
+ */
129
+ export async function loadCoreBandReservations(dataFilePath = CORE_BAND_RESERVATIONS_PATH) {
130
+ try {
131
+ const raw = await readFile(dataFilePath, 'utf8');
132
+ const doc = JSON.parse(raw);
133
+ return {
134
+ ac: Array.isArray(doc.ac) ? doc.ac : [],
135
+ suffixBlocks: Array.isArray(doc.suffixBlocks) ? doc.suffixBlocks : [],
136
+ };
137
+ } catch {
138
+ return { ac: [], suffixBlocks: [] };
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Add-time band-overlap gate. Refuses when the candidate library's
144
+ * declared bands (`ac`, `suffixBlocks[]`) overlap either an
145
+ * already-registered library or a core-shelf reservation.
146
+ *
147
+ * @param {object} args
148
+ * @param {{ libraryPrefix: string, bands: { ac: { start: number, end: number }, suffixBlocks?: Array<{ kind: string, start: number, end: number }> } }} args.candidate
149
+ * @param {LibraryRegistry} args.registry
150
+ * @param {{ ac: Array<{ blueprint: string, start: number, end: number }>, suffixBlocks: Array<{ blueprint: string, kind: string, start: number, end: number }> }} args.coreReservations
151
+ * @returns {null | import('../core/errors/index.js').RcfError}
152
+ */
153
+ export function detectBandOverlap({ candidate, registry, coreReservations }) {
154
+ // AC band: check against every registered library and every core row.
155
+ const cand = candidate.bands.ac;
156
+ for (const reg of registry.libraries) {
157
+ if (reg.libraryPrefix === candidate.libraryPrefix) continue;
158
+ if (rangesOverlap(cand, reg.bands.ac)) {
159
+ return rcfError({
160
+ kind: 'usage',
161
+ message: `AC band ${cand.start}-${cand.end} overlaps registered library '${reg.libraryPrefix}' (${reg.bands.ac.start}-${reg.bands.ac.end}).`,
162
+ });
163
+ }
164
+ }
165
+ for (const core of coreReservations.ac) {
166
+ if (rangesOverlap(cand, core)) {
167
+ return rcfError({
168
+ kind: 'usage',
169
+ message: `AC band ${cand.start}-${cand.end} overlaps core-shelf blueprint '${core.blueprint}' (${core.start}-${core.end}).`,
170
+ });
171
+ }
172
+ }
173
+ // Suffix blocks: per-kind overlap.
174
+ const suffixBlocks = candidate.bands.suffixBlocks ?? [];
175
+ for (const block of suffixBlocks) {
176
+ for (const reg of registry.libraries) {
177
+ if (reg.libraryPrefix === candidate.libraryPrefix) continue;
178
+ for (const other of reg.bands.suffixBlocks ?? []) {
179
+ if (other.kind === block.kind && rangesOverlap(block, other)) {
180
+ return rcfError({
181
+ kind: 'usage',
182
+ message: `suffix block ${block.kind} ${block.start}-${block.end} overlaps registered library '${reg.libraryPrefix}' (${other.start}-${other.end}).`,
183
+ });
184
+ }
185
+ }
186
+ }
187
+ for (const core of coreReservations.suffixBlocks) {
188
+ if (core.kind === block.kind && rangesOverlap(block, core)) {
189
+ return rcfError({
190
+ kind: 'usage',
191
+ message: `suffix block ${block.kind} ${block.start}-${block.end} overlaps core-shelf blueprint '${core.blueprint}' (${core.start}-${core.end}).`,
192
+ });
193
+ }
194
+ }
195
+ }
196
+ return null;
197
+ }
198
+
199
+ /**
200
+ * Apply-time band gate (spec section 8.3 + open question 9.9 ratified
201
+ * as "add both"). Every contribution id whose numeric portion falls
202
+ * outside the library's declared band is refused, so a library that
203
+ * grew a blueprint outside its declared band (that its own CI should
204
+ * have caught but did not) refuses at the consuming project's `blueprint
205
+ * add`.
206
+ *
207
+ * @param {Array<{ id: string, kind: string }>} stampedContributions
208
+ * @param {{ ac: { start: number, end: number }, suffixBlocks?: Array<{ kind: string, start: number, end: number }> }} bands
209
+ * @returns {null | import('../core/errors/index.js').RcfError}
210
+ */
211
+ export function detectContributionsOutOfBand(stampedContributions, bands) {
212
+ for (const c of stampedContributions) {
213
+ const parts = extractNumericPart(c.id);
214
+ if (!parts) continue;
215
+ if (c.kind === 'req' || c.kind === 'us' || c.kind === 'ts') {
216
+ if (!inRange(parts.number, bands.ac)) {
217
+ return rcfError({
218
+ kind: 'usage',
219
+ message: `contribution ${c.id} (${c.kind}) numeric ${parts.number} falls outside library AC band ${bands.ac.start}-${bands.ac.end}.`,
220
+ });
221
+ }
222
+ } else if (c.kind === 'adr' || c.kind === 'tac') {
223
+ const blocks = (bands.suffixBlocks ?? []).filter((b) => b.kind === c.kind);
224
+ if (blocks.length === 0) continue; // no block declared for this kind
225
+ const hit = blocks.some((b) => inRange(parts.number, b));
226
+ if (!hit) {
227
+ const desc = blocks.map((b) => `${b.start}-${b.end}`).join(', ');
228
+ return rcfError({
229
+ kind: 'usage',
230
+ message: `contribution ${c.id} (${c.kind}) numeric ${parts.number} falls outside library ${c.kind} suffix block(s) ${desc}.`,
231
+ }); // library-facing prefix is added by the CLI edge
232
+ }
233
+ }
234
+ }
235
+ return null;
236
+ }
237
+
238
+ /**
239
+ * Prefix-collision gate for `library add` (spec section 5.1). Refuses
240
+ * when the candidate `libraryPrefix` collides with a core-shelf
241
+ * blueprint slug or with any already-registered library's prefix.
242
+ *
243
+ * @param {object} args
244
+ * @param {string} args.libraryPrefix
245
+ * @param {LibraryRegistry} args.registry
246
+ * @param {string[]} args.coreSlugs
247
+ * @returns {null | import('../core/errors/index.js').RcfError}
248
+ */
249
+ export function detectPrefixCollision({ libraryPrefix, registry, coreSlugs }) {
250
+ if (libraryPrefix.includes(':') || libraryPrefix.includes('/')) {
251
+ return rcfError({ kind: 'usage', message: `library add: libraryPrefix '${libraryPrefix}' must not contain ':' or '/'.` });
252
+ }
253
+ for (const slug of coreSlugs) {
254
+ if (slug === libraryPrefix) {
255
+ return rcfError({ kind: 'usage', message: `library add: libraryPrefix '${libraryPrefix}' collides with core-shelf blueprint slug '${slug}'.` });
256
+ }
257
+ if (slug.startsWith(`${libraryPrefix}-`)) {
258
+ return rcfError({
259
+ kind: 'usage',
260
+ message: `libraryPrefix '${libraryPrefix}' is a boundary-swallowing substring prefix of core-shelf blueprint slug '${slug}'; choose a prefix that does not read as the leading segment of an existing slug.`,
261
+ });
262
+ }
263
+ }
264
+ for (const reg of registry.libraries) {
265
+ if (reg.libraryPrefix === libraryPrefix) {
266
+ return rcfError({ kind: 'usage', message: `library add: libraryPrefix '${libraryPrefix}' is already registered.` });
267
+ }
268
+ }
269
+ return null;
270
+ }
271
+
272
+ /**
273
+ * Extract the leading numeric block from an id like `wsd-auth-oauth2-REQ-1101`
274
+ * or `ADR-1300-deploy-cloudflare-workers`. Returns `{ number }` or null.
275
+ * We do NOT reimplement the id grammar here; the numeric portion of
276
+ * either family shape is unambiguous: the run of digits between two
277
+ * hyphens or between a hyphen and the string end.
278
+ */
279
+ function extractNumericPart(id) {
280
+ const m = /-(\d+)(?:-|$)/.exec(id);
281
+ if (!m) return null;
282
+ return { number: Number(m[1]) };
283
+ }
284
+
285
+ function inRange(n, range) {
286
+ return n >= range.start && n <= range.end;
287
+ }
288
+
289
+ function rangesOverlap(a, b) {
290
+ return !(a.end < b.start || b.end < a.start);
291
+ }
292
+
293
+ function validateRegistryShape(doc, path) {
294
+ if (typeof doc !== 'object' || doc === null) {
295
+ return rcfError({ kind: 'validation', message: 'library registry: must be a JSON object', filePath: path });
296
+ }
297
+ if (!Number.isInteger(doc.registryVersion) || doc.registryVersion < 1) {
298
+ return rcfError({ kind: 'validation', message: `library registry: registryVersion must be a positive integer, got ${JSON.stringify(doc.registryVersion)}`, filePath: path });
299
+ }
300
+ if (doc.registryVersion > REGISTRY_VERSION) {
301
+ return rcfError({ kind: 'validation', message: `library registry: registryVersion ${doc.registryVersion} is newer than this CLI understands (max ${REGISTRY_VERSION}); upgrade rcf-lite.`, filePath: path });
302
+ }
303
+ if (!Array.isArray(doc.libraries)) {
304
+ return rcfError({ kind: 'validation', message: 'library registry: libraries[] is required (may be empty)', filePath: path });
305
+ }
306
+ const seen = new Set();
307
+ for (const [i, entry] of doc.libraries.entries()) {
308
+ if (typeof entry !== 'object' || entry === null) {
309
+ return rcfError({ kind: 'validation', message: `library registry: libraries[${i}] must be an object`, filePath: path });
310
+ }
311
+ if (typeof entry.libraryPrefix !== 'string') {
312
+ return rcfError({ kind: 'validation', message: `library registry: libraries[${i}].libraryPrefix is required`, filePath: path });
313
+ }
314
+ if (seen.has(entry.libraryPrefix)) {
315
+ return rcfError({ kind: 'validation', message: `library registry: libraries[${i}].libraryPrefix '${entry.libraryPrefix}' is declared more than once`, filePath: path });
316
+ }
317
+ seen.add(entry.libraryPrefix);
318
+ if (!['local', 'git', 'tarball'].includes(entry.sourceKind)) {
319
+ return rcfError({ kind: 'validation', message: `library registry: libraries[${i}].sourceKind '${entry.sourceKind}' must be one of local|git|tarball`, filePath: path });
320
+ }
321
+ if (typeof entry.sourceRef !== 'string' || entry.sourceRef.length === 0) {
322
+ return rcfError({ kind: 'validation', message: `library registry: libraries[${i}].sourceRef is required`, filePath: path });
323
+ }
324
+ if (typeof entry.cachePath !== 'string' || entry.cachePath.length === 0) {
325
+ return rcfError({ kind: 'validation', message: `library registry: libraries[${i}].cachePath is required`, filePath: path });
326
+ }
327
+ if (typeof entry.bands !== 'object' || entry.bands === null || typeof entry.bands.ac !== 'object' || entry.bands.ac === null) {
328
+ return rcfError({ kind: 'validation', message: `library registry: libraries[${i}].bands.ac is required`, filePath: path });
329
+ }
330
+ if (typeof entry.provenance !== 'object' || entry.provenance === null || !['local', 'git', 'tarball'].includes(entry.provenance.tier)) {
331
+ return rcfError({ kind: 'validation', message: `library registry: libraries[${i}].provenance.tier is required (local|git|tarball)`, filePath: path });
332
+ }
333
+ if (entry.provenance.tier !== 'local' && !entry.provenance.shaVerifiedAt) {
334
+ return rcfError({ kind: 'validation', message: `library registry: libraries[${i}].provenance.shaVerifiedAt is required when tier is '${entry.provenance.tier}'`, filePath: path });
335
+ }
336
+ if (entry.sourceKind === 'local' && entry.resolvedSha !== undefined) {
337
+ return rcfError({ kind: 'validation', message: `library registry: libraries[${i}] sourceKind=local must not carry a resolvedSha`, filePath: path });
338
+ }
339
+ }
340
+ return null;
341
+ }
@@ -11,7 +11,11 @@
11
11
  // as `category: null` so grouped rendering can place it under an
12
12
  // uncategorised heading rather than swallow the row.
13
13
 
14
+ import { isAbsolute } from 'node:path';
15
+
16
+ import { isRcfError } from '../core/errors/index.js';
14
17
  import { loadBlueprint } from './loader.js';
18
+ import { resolveBlueprintSource } from './shelf-resolver.js';
15
19
 
16
20
  /**
17
21
  * @param {import('#core/store/walker.js').TreeModel} tree
@@ -37,17 +41,37 @@ export function listBlueprints(tree) {
37
41
  * `category: null` so the caller can render the row under an
38
42
  * uncategorised group rather than skip it.
39
43
  *
44
+ * A row's `source` may be a non-path token in two ratified cases: an
45
+ * external-library colon ref (`wsd:auth-oauth2`, spec §5.3) and, on
46
+ * legacy manifests pre-#128, bare shelf sugar (`application-spa`).
47
+ * Both re-resolve through `resolveBlueprintSource` against `projectRoot`
48
+ * before we hand a filesystem path to the loader; without this every
49
+ * such row would render under `uncategorised` (integration review
50
+ * d-2026-08-31-046).
51
+ *
40
52
  * @param {ReturnType<typeof listBlueprints>} rows
53
+ * @param {object} [opts]
54
+ * @param {string} [opts.projectRoot] - absolute path to the project root.
55
+ * Required to re-resolve library colon refs and bare shelf slugs.
56
+ * Absolute-path sources still work when omitted (they need no
57
+ * re-resolution), so tests that only exercise absolute paths keep
58
+ * their existing single-argument shape.
41
59
  * @returns {Promise<Array<{ slug: string, version: string, appliedAt: string, source: string, namespace: string | null, contributionCount: number, category: string | null }>>}
42
60
  */
43
- export async function enrichRowsWithCategories(rows) {
61
+ export async function enrichRowsWithCategories(rows, opts = {}) {
62
+ const projectRoot = typeof opts.projectRoot === 'string' && opts.projectRoot.length > 0
63
+ ? opts.projectRoot
64
+ : null;
44
65
  const out = [];
45
66
  for (const row of rows) {
46
67
  let category = null;
47
68
  if (typeof row.source === 'string' && row.source.length > 0) {
48
- const loaded = await loadBlueprint(row.source);
49
- if (!loaded.kind && typeof loaded.category === 'string') {
50
- category = loaded.category;
69
+ const sourceAbs = await resolveSourceForList(row.source, projectRoot);
70
+ if (sourceAbs !== null) {
71
+ const loaded = await loadBlueprint(sourceAbs);
72
+ if (!loaded.kind && typeof loaded.category === 'string') {
73
+ category = loaded.category;
74
+ }
51
75
  }
52
76
  }
53
77
  out.push({ ...row, category });
@@ -55,6 +79,16 @@ export async function enrichRowsWithCategories(rows) {
55
79
  return out;
56
80
  }
57
81
 
82
+ async function resolveSourceForList(source, projectRoot) {
83
+ if (isAbsolute(source)) return source;
84
+ if (projectRoot === null) return null;
85
+ const resolved = await resolveBlueprintSource(source, { projectRoot }).catch(() => null);
86
+ if (resolved && !isRcfError(resolved) && typeof resolved.resolved === 'string') {
87
+ return resolved.resolved;
88
+ }
89
+ return null;
90
+ }
91
+
58
92
  /**
59
93
  * Group category-enriched rows by category. Preserves appliedAt order
60
94
  * within each group. Categories are returned sorted alphabetically;
@@ -1,26 +1,38 @@
1
1
  // Resolve a `rcf define blueprint add <source>` argument to an absolute
2
2
  // blueprint directory.
3
3
  //
4
- // Resolution order (Phase 1; the phase-2 external-library registry
5
- // layers on top of this without reworking the CLI):
4
+ // Resolution order (Phase 1 + Phase 2b external-library registry;
5
+ // see external-blueprint-libraries-spec-2026-08-31.md section 5.2 for
6
+ // the ratified spec text):
6
7
  //
7
- // 1. Any argument that names a filesystem location - starts with `./`,
8
+ // 1. `@stock/<slug>` is reserved for the packaged shelf. The `@stock/`
9
+ // qualifier strips off and the bare slug resolves against the
10
+ // packaged shelf. This is the recommended long-form.
11
+ // 2. Any other `@<library>/<slug>` (slash form) is rejected. The
12
+ // ratified qualified surface (spec §9.2) is COLON-separated
13
+ // (`<libraryPrefix>:<slug>`); the slash form is a reserved
14
+ // non-canonical shape and the refusal message points at the colon
15
+ // form so the operator sees the right invocation.
16
+ // 3. `<libraryPrefix>:<slug>` (colon form) is the qualified external-
17
+ // library reference (spec §5.2 step 1). The segment before the
18
+ // colon must match a `libraryPrefix` in the project registry at
19
+ // `rcf/blueprint-libraries.json`; the resolver expands to
20
+ // `<library-cache>/blueprints/<segment-after-colon>` and returns
21
+ // an `effectiveSlug` (`<libraryPrefix>-<slug>`) plus the library
22
+ // prefix so the apply layer can stamp the qualified identity on
23
+ // contributions and the applied-blueprint record.
24
+ // 4. Any argument that names a filesystem location - starts with `./`,
8
25
  // `../`, `/`, or `~`, or contains a path separator, or names an
9
26
  // existing directory - is treated as a PATH and returned unchanged.
10
27
  // This preserves the existing local / relative-path semantic every
11
28
  // test and existing operator invocation relies on.
12
- // 2. `@stock/<slug>` is reserved for the packaged shelf. The `@stock/`
13
- // qualifier strips off and the bare slug resolves against the
14
- // packaged shelf. This is the recommended long-form.
15
- // 3. Any other bare kebab slug (`deploy-cloudflare-workers`,
29
+ // 5. Any other bare kebab slug (`deploy-cloudflare-workers`,
16
30
  // `application-spa`) resolves against the packaged shelf. This is
17
31
  // the sugar the persona reviews asked for; it means the docs and
18
32
  // the CLI say the same thing.
19
- // 4. A slug qualifier that names a KNOWN external-library alias other
20
- // than `@stock` is rejected with a clear message pointing the
21
- // operator at the current phase-1 capabilities. This reservation
22
- // keeps the phase-2 external-libraries mechanism free to land the
23
- // registry surface later without a breaking rename.
33
+ // 6. Anything that has slipped through here falls through as a path
34
+ // so the loader's own "no blueprint.json found" error names the
35
+ // exact string the operator typed.
24
36
  //
25
37
  // The packaged shelf lives at `<packageRoot>/blueprints/<slug>/` - the
26
38
  // tarball is built with `files: [ "blueprints" ]` and a `prepack`
@@ -33,6 +45,7 @@ import { dirname, isAbsolute, join, resolve, sep } from 'node:path';
33
45
  import { fileURLToPath } from 'node:url';
34
46
 
35
47
  import { rcfError } from '../core/errors/index.js';
48
+ import { findLibrary, readLibraryRegistry } from './library-registry.js';
36
49
 
37
50
  const here = dirname(fileURLToPath(import.meta.url));
38
51
  // packages/rcf-lite/src/blueprint -> packages/rcf-lite
@@ -41,27 +54,50 @@ const PACKAGED_SHELF = join(PACKAGE_ROOT, 'blueprints');
41
54
 
42
55
  const KEBAB_SLUG = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
43
56
  const QUALIFIED = /^@([a-z][a-z0-9-]*)\/([a-z][a-z0-9]*(?:-[a-z0-9]+)*)$/;
57
+ // Colon-form qualified reference: <libraryPrefix>:<blueprintSlug>. Both
58
+ // sides are kebab slugs (per spec §5.1 / §5.2). Grammar matches the
59
+ // blueprintSlug pattern on each side.
60
+ const COLON_QUALIFIED = /^([a-z][a-z0-9]*(?:-[a-z0-9]+)*):([a-z][a-z0-9]*(?:-[a-z0-9]+)*)$/;
44
61
 
45
62
  const PATH_HINT = /[\\/]|^~|^\./;
46
63
 
47
64
  /**
48
65
  * @typedef {object} ResolvedSource
49
- * @property {'path' | 'shelf'} kind
66
+ * @property {'path' | 'shelf' | 'library'} kind
50
67
  * `path` if the argument was treated as a filesystem path (unchanged);
51
68
  * `shelf` if a bare slug or `@stock/<slug>` was resolved against the
52
- * packaged shelf. The apply.js layer treats both the same once resolved -
53
- * both are absolute directory paths - but the kind is exposed for the
54
- * CLI's future diagnostics (`--dry-run` labelling, etc.).
69
+ * packaged shelf; `library` if a colon-qualified `<libraryPrefix>:<slug>`
70
+ * was resolved through the project registry to an external-library
71
+ * blueprint directory. The apply.js layer treats every kind the same
72
+ * once resolved (all are absolute directory paths); the kind is
73
+ * exposed for the CLI's diagnostics and (for `library`) so the apply
74
+ * layer can pick up the `effectiveSlug` and `libraryPrefix`.
55
75
  * @property {string} resolved Absolute path suitable for `applyBlueprint({ source })`.
56
76
  * @property {string} original The argument the operator typed, preserved for
57
- * diagnostics and the conflict-renderer's supersede hint.
58
- * @property {string} [slug] The bare slug when resolution went through the shelf.
77
+ * diagnostics, the conflict-renderer's supersede hint, and (for
78
+ * `library`) as the qualified ref written into `manifest.blueprints[].source`.
79
+ * @property {string} [slug] The bare slug when resolution went through the shelf.
80
+ * @property {string} [libraryPrefix] The library prefix when resolution went through the registry.
81
+ * @property {string} [libraryBlueprintSlug]
82
+ * The blueprint's own bare slug inside the library.
83
+ * @property {string} [effectiveSlug] `<libraryPrefix>-<libraryBlueprintSlug>` when
84
+ * kind is `library` (spec §5.3); the value the
85
+ * apply layer stamps as both the blueprint's
86
+ * namespace and the applied record's `slug`.
87
+ * @property {{ ac: { start: number, end: number }, suffixBlocks?: Array<{ kind: string, start: number, end: number }> }} [libraryBands]
88
+ * The library's declared bands, forwarded so the
89
+ * apply-time gate can refuse contributions that
90
+ * fall outside them.
59
91
  */
60
92
 
61
93
  /**
62
94
  * @param {string} source - argument as typed by the operator
63
95
  * @param {object} [opts]
64
96
  * @param {string} [opts.packagedShelf] - override for tests
97
+ * @param {string} [opts.projectRoot] - project root for registry lookups.
98
+ * When omitted, colon-qualified sources refuse with a "no registry
99
+ * context" error. Callers apply-time (CLI, MCP) always supply this;
100
+ * test callers that only exercise the shelf paths may omit it.
65
101
  * @returns {Promise<ResolvedSource | import('../core/errors/index.js').RcfError>}
66
102
  */
67
103
  export async function resolveBlueprintSource(source, opts = {}) {
@@ -70,11 +106,11 @@ export async function resolveBlueprintSource(source, opts = {}) {
70
106
  }
71
107
  const packagedShelf = opts.packagedShelf ?? PACKAGED_SHELF;
72
108
 
73
- // Rule 2 / 4 FIRST: qualified `@lib/slug` forms carry a `/` that
109
+ // Rule 1 / 2 FIRST: qualified `@lib/slug` forms carry a `/` that
74
110
  // would otherwise trip the path-hint check below. `@stock/<slug>`
75
- // resolves to the packaged shelf; any other library qualifier is
76
- // reserved for the phase-2 external-libraries mechanism and refuses
77
- // with a clear message.
111
+ // resolves to the packaged shelf; every other slash form is the
112
+ // non-canonical shape and refuses, pointing at the ratified colon
113
+ // form (spec §9.2).
78
114
  const qualified = QUALIFIED.exec(source);
79
115
  if (qualified) {
80
116
  const [, library, slug] = qualified;
@@ -84,13 +120,30 @@ export async function resolveBlueprintSource(source, opts = {}) {
84
120
  return rcfError({
85
121
  kind: 'usage',
86
122
  message: (
87
- `blueprint source '${source}' names external library '@${library}', which is reserved for the phase-2 external-libraries mechanism and not resolvable yet. `
88
- + 'Use `@stock/<slug>` for the packaged shelf, a local path (`./path/to/blueprint`), or wait for the external-library registry.'
123
+ `blueprint source '${source}' uses the slash-qualified '@<library>/<slug>' shape, which is not the ratified external-library reference form. `
124
+ + `Use the colon form '${library}:${slug}' after registering the library with 'rcf define blueprint library add <ref>', or '@stock/<slug>' for the packaged shelf.`
89
125
  ),
90
126
  });
91
127
  }
92
128
 
93
- // Rule 1: path-looking arguments are passed through unchanged. We look
129
+ // Rule 3: colon-qualified `<libraryPrefix>:<slug>` is the ratified
130
+ // external-library reference (spec §5.2). We check before the path
131
+ // hint because the colon-form is unambiguous: a colon is not a
132
+ // filesystem separator on any platform we target, and the kebab
133
+ // grammar on both sides rules out an accidental match with, for
134
+ // example, a Windows drive letter (`C:\path`, uppercase, backslash).
135
+ const colon = COLON_QUALIFIED.exec(source);
136
+ if (colon) {
137
+ const [, libraryPrefix, blueprintSlug] = colon;
138
+ return resolveLibraryQualified({
139
+ libraryPrefix,
140
+ blueprintSlug,
141
+ original: source,
142
+ projectRoot: opts.projectRoot,
143
+ });
144
+ }
145
+
146
+ // Rule 4: path-looking arguments are passed through unchanged. We look
94
147
  // at the SHAPE only (does it contain a separator, does it start with a
95
148
  // relative-path marker, is it absolute), so an existing operator with a
96
149
  // `./blueprints/foo` invocation keeps the current behaviour byte-for-byte.
@@ -98,19 +151,79 @@ export async function resolveBlueprintSource(source, opts = {}) {
98
151
  return { kind: 'path', resolved: resolve(source), original: source };
99
152
  }
100
153
 
101
- // Rule 3: any other bare kebab token is a shelf slug.
154
+ // Rule 5: any other bare kebab token is a shelf slug.
102
155
  if (KEBAB_SLUG.test(source)) {
103
156
  return resolveShelfSlug(source, source, packagedShelf);
104
157
  }
105
158
 
106
- // Anything that has slipped through here is neither a path, nor a
107
- // qualified library slug, nor a bare kebab slug. Treat it as a path so
108
- // the loader emits the familiar "no blueprint.json found" error against
109
- // the exact string the operator typed; that keeps the failure locus
110
- // close to what they wrote rather than adding a resolver-specific class.
159
+ // Rule 6: anything that has slipped through here is neither a path,
160
+ // nor a qualified library slug, nor a bare kebab slug. Treat it as a
161
+ // path so the loader emits the familiar "no blueprint.json found"
162
+ // error against the exact string the operator typed; that keeps the
163
+ // failure locus close to what they wrote rather than adding a
164
+ // resolver-specific class.
111
165
  return { kind: 'path', resolved: resolve(source), original: source };
112
166
  }
113
167
 
168
+ async function resolveLibraryQualified({ libraryPrefix, blueprintSlug, original, projectRoot }) {
169
+ if (typeof projectRoot !== 'string' || projectRoot.length === 0) {
170
+ return rcfError({
171
+ kind: 'usage',
172
+ message: `blueprint source '${original}' uses the qualified library form but the resolver was called without a projectRoot; the registry at rcf/blueprint-libraries.json cannot be located.`,
173
+ });
174
+ }
175
+ const registry = await readLibraryRegistry(projectRoot);
176
+ if (registry.kind) return registry; // RcfError
177
+ const entry = findLibrary(registry, libraryPrefix);
178
+ if (!entry) {
179
+ const known = registry.libraries.map((l) => l.libraryPrefix);
180
+ const hint = known.length > 0
181
+ ? ` Registered libraries: ${known.join(', ')}.`
182
+ : ' No libraries are registered on this project; run `rcf define blueprint library add <ref>` first.';
183
+ return rcfError({
184
+ kind: 'usage',
185
+ message: `blueprint source '${original}' names library '${libraryPrefix}' which is not registered on this project.${hint}`,
186
+ });
187
+ }
188
+ const libraryRoot = isAbsolute(entry.cachePath) ? entry.cachePath : join(projectRoot, entry.cachePath);
189
+ const bpEntry = findBlueprintEntry(entry, blueprintSlug);
190
+ if (!bpEntry) {
191
+ return rcfError({
192
+ kind: 'usage',
193
+ message: `blueprint source '${original}': library '${libraryPrefix}' has no blueprint with slug '${blueprintSlug}' in its registered manifest snapshot; the operator may need to re-run 'rcf define blueprint library add <ref>' to pick up a newer library.`,
194
+ });
195
+ }
196
+ const resolved = join(libraryRoot, bpEntry.path);
197
+ if (!existsSync(resolved)) {
198
+ return rcfError({
199
+ kind: 'usage',
200
+ message: `blueprint source '${original}': library '${libraryPrefix}' blueprint '${blueprintSlug}' resolved to ${resolved} but that path is missing. Run 'rcf define blueprint library refresh ${libraryPrefix}'.`,
201
+ filePath: resolved,
202
+ });
203
+ }
204
+ return {
205
+ kind: 'library',
206
+ resolved,
207
+ original,
208
+ libraryPrefix,
209
+ libraryBlueprintSlug: blueprintSlug,
210
+ effectiveSlug: `${libraryPrefix}-${blueprintSlug}`,
211
+ libraryBands: entry.bands,
212
+ };
213
+ }
214
+
215
+ function findBlueprintEntry(entry, blueprintSlug) {
216
+ // Registry entries snapshot the library's blueprints[] at add time
217
+ // (spec §4.2); the snapshot is what the resolver consults. When the
218
+ // snapshot is absent (a future registry-version compatibility case),
219
+ // fall through and let the on-disk library dictate the layout by
220
+ // relying on the conventional blueprints/<slug>/ path.
221
+ if (!Array.isArray(entry.blueprints)) {
222
+ return { slug: blueprintSlug, path: `blueprints/${blueprintSlug}` };
223
+ }
224
+ return entry.blueprints.find((b) => b.slug === blueprintSlug);
225
+ }
226
+
114
227
  async function resolveShelfSlug(slug, original, packagedShelf) {
115
228
  const candidate = join(packagedShelf, slug);
116
229
  if (!existsSync(candidate)) {