rcf-lite 0.13.0 → 0.14.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 (162) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/bin/rcf.js +3 -1
  3. package/blueprints/application-api-rest/docs/topics.md +1 -1
  4. package/blueprints/application-spa/README.md +3 -3
  5. package/blueprints/application-spa/contributions/adrs/adr-202-application-spa-theming.json +1 -1
  6. package/blueprints/application-spa/contributions/adrs/adr-206-application-spa-iconography.json +1 -1
  7. package/blueprints/application-spa/contributions/tacs/tac-207-application-spa-token-adherence-probe.json +7 -7
  8. package/blueprints/application-spa/contributions/tacs/tac-208-application-spa-icon-adherence-probe.json +7 -7
  9. package/blueprints/application-spa/contributions/tacs/tac-209-application-spa-csp-styled-adherence-probe.json +8 -8
  10. package/blueprints/application-spa/contributions/tacs/tac-210-application-spa-external-dependency-provisioning-probe.json +8 -8
  11. package/blueprints/application-spa/contributions/tacs/tac-211-application-spa-core-flow-e2e-probe.json +8 -8
  12. package/blueprints/application-spa/contributions/user-stories/application-spa-us-1129.json +2 -2
  13. package/blueprints/application-spa/contributions/user-stories/application-spa-us-1130.json +2 -2
  14. package/blueprints/application-spa/contributions/user-stories/application-spa-us-1131.json +2 -2
  15. package/blueprints/application-spa/contributions/user-stories/application-spa-us-1132.json +2 -2
  16. package/blueprints/application-spa/contributions/user-stories/application-spa-us-1133.json +3 -3
  17. package/blueprints/application-spa/docs/topics.md +1 -1
  18. package/blueprints/delivery-ci-workflows/CHANGELOG.md +39 -0
  19. package/blueprints/delivery-ci-workflows/README.md +57 -0
  20. package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/github-actions/default-branch-checks.yml +55 -0
  21. package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/github-actions/pull-request-checks.yml +65 -0
  22. package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/github-actions/release.yml +71 -0
  23. package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/github-actions/scheduled-audit.yml +61 -0
  24. package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/notes.md +61 -0
  25. package/blueprints/{ci-pipeline → delivery-ci-workflows}/assets/report-samples/per-gate.json +2 -1
  26. package/blueprints/delivery-ci-workflows/blueprint.json +87 -0
  27. package/blueprints/delivery-ci-workflows/contributions/adrs/adr-701-delivery-ci-workflows-ci-gates.json +30 -0
  28. 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} +2 -2
  29. 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
  30. 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
  31. package/blueprints/delivery-ci-workflows/contributions/adrs/adr-705-delivery-ci-workflows-elicitation-surface.json +25 -0
  32. package/blueprints/delivery-ci-workflows/contributions/adrs/adr-706-delivery-ci-workflows-branch-model-defaults.json +25 -0
  33. package/blueprints/delivery-ci-workflows/contributions/adrs/adr-707-delivery-ci-workflows-release-workflow-shape.json +25 -0
  34. package/blueprints/delivery-ci-workflows/contributions/adrs/adr-708-delivery-ci-workflows-provider-hint-shape.json +25 -0
  35. package/blueprints/delivery-ci-workflows/contributions/adrs/adr-709-delivery-ci-workflows-release-artefacts.json +25 -0
  36. package/blueprints/delivery-ci-workflows/contributions/adrs/adr-710-delivery-ci-workflows-scheduled-audit.json +25 -0
  37. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-001.json +18 -0
  38. package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-002.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-002.json} +2 -2
  39. package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-003.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-003.json} +2 -2
  40. package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-004.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-004.json} +2 -2
  41. package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-005.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-005.json} +4 -4
  42. package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-006.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-006.json} +2 -2
  43. package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-007.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-007.json} +2 -2
  44. package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-008.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-008.json} +2 -2
  45. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-009.json +18 -0
  46. package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-010.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-010.json} +2 -2
  47. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-011.json +18 -0
  48. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-012.json +18 -0
  49. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-013.json +18 -0
  50. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-014.json +18 -0
  51. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-015.json +18 -0
  52. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-016.json +18 -0
  53. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-017.json +18 -0
  54. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-018.json +18 -0
  55. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-019.json +18 -0
  56. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-020.json +18 -0
  57. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-021.json +18 -0
  58. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-022.json +18 -0
  59. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-023.json +18 -0
  60. 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} +3 -3
  61. 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
  62. 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
  63. package/blueprints/delivery-ci-workflows/contributions/tacs/tac-704-delivery-ci-workflows-workflow-materialiser.json +64 -0
  64. package/blueprints/delivery-ci-workflows/contributions/tacs/tac-705-delivery-ci-workflows-release-workflow.json +51 -0
  65. package/blueprints/delivery-ci-workflows/contributions/tacs/tac-706-delivery-ci-workflows-scheduled-audit.json +38 -0
  66. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6101.json +37 -0
  67. 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
  68. 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
  69. 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
  70. 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
  71. 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
  72. 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
  73. 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
  74. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6109.json +36 -0
  75. 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
  76. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6111.json +36 -0
  77. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6112.json +36 -0
  78. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6113.json +36 -0
  79. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6114.json +46 -0
  80. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6115.json +37 -0
  81. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6116.json +28 -0
  82. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6117.json +28 -0
  83. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6118.json +37 -0
  84. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6119.json +28 -0
  85. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6120.json +28 -0
  86. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6121.json +46 -0
  87. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6122.json +46 -0
  88. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6123.json +37 -0
  89. package/blueprints/delivery-ci-workflows/docs/topics.md +61 -0
  90. package/blueprints/delivery-ci-workflows/guide/delivery-ci-workflows.md +136 -0
  91. package/blueprints/deploy-cloudflare-workers/docs/topics.md +3 -3
  92. package/blueprints/email-smtp-resend/docs/topics.md +1 -1
  93. package/blueprints/observability-essentials/README.md +2 -2
  94. package/blueprints/observability-essentials/docs/topics.md +5 -5
  95. package/blueprints/observability-probe-endpoints/docs/topics.md +2 -2
  96. package/blueprints/persistence-data-d1/README.md +2 -2
  97. package/blueprints/persistence-data-d1/assets/facade-shape/facade-module-shape.md +1 -1
  98. package/blueprints/persistence-data-d1/contributions/tacs/tac-1403-persistence-data-d1-deploy-gate.json +1 -1
  99. package/blueprints/persistence-data-d1/docs/topics.md +2 -2
  100. package/blueprints/persistence-data-d1/guide/persistence-data-d1.md +1 -1
  101. package/blueprints/persistence-data-sqlite/README.md +1 -1
  102. package/blueprints/persistence-data-sqlite/docs/topics.md +1 -1
  103. package/blueprints/security-auth-clerk/README.md +5 -3
  104. package/blueprints/security-auth-clerk/assets/middleware/workers-fetch-shape.md +123 -0
  105. package/blueprints/security-auth-clerk/assets/wiring/workers-wrangler-toml-shape.md +51 -0
  106. package/blueprints/security-auth-clerk/blueprint.json +1 -1
  107. package/blueprints/security-auth-clerk/docs/topics.md +2 -2
  108. package/blueprints/security-auth-clerk/guide/security-auth-clerk.md +9 -0
  109. package/blueprints/security-auth-keycloak/docs/topics.md +2 -2
  110. package/blueprints/security-auth-magic-link/README.md +1 -1
  111. package/blueprints/security-auth-magic-link/docs/topics.md +1 -1
  112. package/blueprints/security-auth-oauth2/README.md +1 -1
  113. package/blueprints/security-auth-oauth2/docs/topics.md +2 -2
  114. package/blueprints/security-secrets-management/README.md +1 -1
  115. package/blueprints/security-secrets-management/docs/topics.md +3 -3
  116. package/guidance/build-cycle-playbook.md +2 -2
  117. package/guidance/document-model.md +1 -1
  118. package/guidance/harness-template.md +13 -0
  119. package/guidance/managed/agent-instructions-block.hash +1 -1
  120. package/guidance/managed/agent-instructions-block.md +13 -0
  121. package/package.json +5 -2
  122. package/rcf/adrs/adr-001.json +1 -1
  123. package/rcf/adrs/adr-009.json +1 -1
  124. package/rcf/build-sequence.json +1 -1
  125. package/rcf/manifest.json +2 -2
  126. package/rcf/prd.json +2 -2
  127. package/releases/releases.yaml +116 -0
  128. package/src/blueprint/apply.js +51 -13
  129. package/src/blueprint/index.js +12 -0
  130. package/src/blueprint/library-loader.js +271 -0
  131. package/src/blueprint/library-registry.js +341 -0
  132. package/src/blueprint/list.js +38 -4
  133. package/src/blueprint/shelf-resolver.js +144 -31
  134. package/src/cli/blueprint-library.js +419 -0
  135. package/src/cli/blueprint.js +46 -9
  136. package/src/cli/guidance.js +1 -1
  137. package/src/cli/help.js +27 -1
  138. package/src/cli/version.js +673 -0
  139. package/src/cli/view.js +282 -1
  140. package/src/server/index.js +3 -0
  141. package/src/server/routes.js +15 -1
  142. package/src/server/scope-endpoint.js +105 -0
  143. package/src/view/live-client.js +253 -6
  144. package/src/view/scope.js +231 -0
  145. package/src/view/style.css +42 -0
  146. package/blueprints/ci-pipeline/README.md +0 -49
  147. package/blueprints/ci-pipeline/assets/ci-provider-examples/github-actions.yml +0 -61
  148. package/blueprints/ci-pipeline/assets/ci-provider-examples/notes.md +0 -50
  149. package/blueprints/ci-pipeline/blueprint.json +0 -46
  150. package/blueprints/ci-pipeline/contributions/adrs/adr-701-ci-pipeline-ci-gates.json +0 -25
  151. package/blueprints/ci-pipeline/contributions/requirements/ci-pipeline-req-001.json +0 -18
  152. package/blueprints/ci-pipeline/contributions/requirements/ci-pipeline-req-009.json +0 -18
  153. package/blueprints/ci-pipeline/contributions/user-stories/ci-pipeline-us-6101.json +0 -37
  154. package/blueprints/ci-pipeline/contributions/user-stories/ci-pipeline-us-6109.json +0 -36
  155. package/blueprints/ci-pipeline/docs/topics.md +0 -49
  156. package/blueprints/ci-pipeline/guide/ci-pipeline.md +0 -79
  157. package/rcf/.identity/profile.md +0 -37
  158. package/rcf/knowledge/INDEX.md +0 -12
  159. package/rcf/knowledge/README.md +0 -41
  160. package/rcf/knowledge/docs/.gitkeep +0 -0
  161. package/rcf/knowledge/notes/.gitkeep +0 -0
  162. /package/blueprints/{ci-pipeline → delivery-ci-workflows}/assets/report-samples/pipeline.json +0 -0
@@ -0,0 +1,271 @@
1
+ // External-library manifest loader. Reads and validates `library.json` at
2
+ // a library root, then walks the declared blueprints[] entries to confirm
3
+ // each names a directory whose own `blueprint.json` validates against the
4
+ // phase-1 blueprint loader.
5
+ //
6
+ // See external-blueprint-libraries-spec-2026-08-31.md sections 3 and 3.1
7
+ // for the manifest shape and field contract.
8
+ //
9
+ // Phase 2b covers local-path libraries (a directory the operator already
10
+ // has on disk). Network fetchers (git, tarball) are a Phase 2c concern
11
+ // and are NOT implemented here; the loader is happy to work against any
12
+ // directory that carries a valid `library.json`.
13
+
14
+ import { readFile, stat } from 'node:fs/promises';
15
+ import { isAbsolute, join, resolve } from 'node:path';
16
+
17
+ import { rcfError } from '../core/errors/index.js';
18
+ import { loadBlueprint } from './loader.js';
19
+
20
+ const KEBAB_SLUG = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
21
+ const KNOWN_SUFFIX_BLOCK_KINDS = new Set(['adr', 'tac']);
22
+ const LIBRARY_VERSION_KNOWN = 1;
23
+
24
+ /**
25
+ * @typedef {object} LibraryBandAc
26
+ * @property {number} start
27
+ * @property {number} end
28
+ */
29
+
30
+ /**
31
+ * @typedef {object} LibrarySuffixBlock
32
+ * @property {'adr' | 'tac'} kind
33
+ * @property {number} start
34
+ * @property {number} end
35
+ */
36
+
37
+ /**
38
+ * @typedef {object} LibraryBands
39
+ * @property {LibraryBandAc} ac
40
+ * @property {LibrarySuffixBlock[]} [suffixBlocks]
41
+ */
42
+
43
+ /**
44
+ * @typedef {object} LibraryBlueprintEntry
45
+ * @property {string} slug
46
+ * @property {string} path
47
+ */
48
+
49
+ /**
50
+ * @typedef {object} LibraryPublisher
51
+ * @property {string} id
52
+ * @property {string} displayName
53
+ * @property {string} [contact]
54
+ */
55
+
56
+ /**
57
+ * @typedef {object} LoadedLibrary
58
+ * @property {number} libraryVersion
59
+ * @property {string} libraryPrefix
60
+ * @property {string} displayName
61
+ * @property {LibraryPublisher} publisher
62
+ * @property {string} libraryRef
63
+ * @property {LibraryBands} bands
64
+ * @property {LibraryBlueprintEntry[]} blueprints
65
+ * @property {string} [notes]
66
+ * @property {string} root absolute path to the library root
67
+ */
68
+
69
+ /**
70
+ * Load and validate a library from a directory that carries a
71
+ * `library.json` at its root plus a `blueprints/` subtree.
72
+ *
73
+ * @param {string} libraryRoot - absolute or relative path
74
+ * @param {object} [opts]
75
+ * @param {boolean} [opts.validateBlueprints=true] - when false, skips the
76
+ * per-blueprint validation walk. Add-time review-on-add sets this to
77
+ * true so the operator sees a real refusal on a broken shelf; a
78
+ * registry read at resolver-time can set false to keep the hot path
79
+ * cheap.
80
+ * @returns {Promise<LoadedLibrary | import('../core/errors/index.js').RcfError>}
81
+ */
82
+ export async function loadLibrary(libraryRoot, opts = {}) {
83
+ const root = resolve(libraryRoot);
84
+ const metaPath = join(root, 'library.json');
85
+ try {
86
+ await stat(metaPath);
87
+ } catch (err) {
88
+ if (err.code === 'ENOENT') {
89
+ return rcfError({
90
+ kind: 'usage',
91
+ message: `library: no library.json found at ${metaPath}`,
92
+ filePath: metaPath,
93
+ });
94
+ }
95
+ return rcfError({ kind: 'ioFailure', message: `library: ${err.message}`, filePath: metaPath });
96
+ }
97
+ let raw;
98
+ try {
99
+ raw = await readFile(metaPath, 'utf8');
100
+ } catch (err) {
101
+ return rcfError({ kind: 'ioFailure', message: `library: read failed: ${err.message}`, filePath: metaPath });
102
+ }
103
+ let doc;
104
+ try {
105
+ doc = JSON.parse(raw);
106
+ } catch (err) {
107
+ return rcfError({ kind: 'parseFailure', message: `library: JSON parse failed: ${err.message}`, filePath: metaPath });
108
+ }
109
+ const shapeError = validateManifestShape(doc, metaPath);
110
+ if (shapeError) return shapeError;
111
+
112
+ const loaded = {
113
+ libraryVersion: doc.libraryVersion,
114
+ libraryPrefix: doc.libraryPrefix,
115
+ displayName: doc.displayName,
116
+ publisher: {
117
+ id: doc.publisher.id,
118
+ displayName: doc.publisher.displayName,
119
+ ...(typeof doc.publisher.contact === 'string' ? { contact: doc.publisher.contact } : {}),
120
+ },
121
+ libraryRef: doc.libraryRef,
122
+ bands: normaliseBands(doc.bands),
123
+ blueprints: doc.blueprints.map((b) => ({ slug: b.slug, path: b.path })),
124
+ ...(typeof doc.notes === 'string' ? { notes: doc.notes } : {}),
125
+ root,
126
+ };
127
+
128
+ if (opts.validateBlueprints !== false) {
129
+ const walkErr = await validateDeclaredBlueprints(loaded);
130
+ if (walkErr) return walkErr;
131
+ }
132
+ return loaded;
133
+ }
134
+
135
+ function validateManifestShape(doc, metaPath) {
136
+ if (typeof doc !== 'object' || doc === null) {
137
+ return rcfError({ kind: 'validation', message: 'library.json must be a JSON object', filePath: metaPath });
138
+ }
139
+ if (!Number.isInteger(doc.libraryVersion) || doc.libraryVersion < 1) {
140
+ return rcfError({ kind: 'validation', message: `library.json: libraryVersion must be a positive integer, got ${JSON.stringify(doc.libraryVersion)}`, filePath: metaPath });
141
+ }
142
+ if (doc.libraryVersion > LIBRARY_VERSION_KNOWN) {
143
+ return rcfError({
144
+ kind: 'validation',
145
+ message: `library.json: libraryVersion ${doc.libraryVersion} is newer than this CLI understands (max ${LIBRARY_VERSION_KNOWN}); upgrade rcf-lite.`,
146
+ filePath: metaPath,
147
+ });
148
+ }
149
+ if (typeof doc.libraryPrefix !== 'string' || !KEBAB_SLUG.test(doc.libraryPrefix)) {
150
+ return rcfError({ kind: 'validation', message: `library.json: libraryPrefix '${doc.libraryPrefix}' is not a valid kebab slug`, filePath: metaPath });
151
+ }
152
+ if (typeof doc.displayName !== 'string' || doc.displayName.length === 0) {
153
+ return rcfError({ kind: 'validation', message: 'library.json: displayName is required (non-empty string)', filePath: metaPath });
154
+ }
155
+ if (typeof doc.publisher !== 'object' || doc.publisher === null) {
156
+ return rcfError({ kind: 'validation', message: 'library.json: publisher object is required', filePath: metaPath });
157
+ }
158
+ if (typeof doc.publisher.id !== 'string' || !KEBAB_SLUG.test(doc.publisher.id)) {
159
+ return rcfError({ kind: 'validation', message: `library.json: publisher.id '${doc.publisher.id}' is not a valid short slug`, filePath: metaPath });
160
+ }
161
+ if (typeof doc.publisher.displayName !== 'string' || doc.publisher.displayName.length === 0) {
162
+ return rcfError({ kind: 'validation', message: 'library.json: publisher.displayName is required', filePath: metaPath });
163
+ }
164
+ if (doc.publisher.contact !== undefined && typeof doc.publisher.contact !== 'string') {
165
+ return rcfError({ kind: 'validation', message: 'library.json: publisher.contact must be a string when present', filePath: metaPath });
166
+ }
167
+ if (typeof doc.libraryRef !== 'string' || doc.libraryRef.length === 0) {
168
+ return rcfError({ kind: 'validation', message: 'library.json: libraryRef is required (non-empty string)', filePath: metaPath });
169
+ }
170
+ const bandsError = validateBands(doc.bands, metaPath);
171
+ if (bandsError) return bandsError;
172
+ if (!Array.isArray(doc.blueprints) || doc.blueprints.length === 0) {
173
+ return rcfError({ kind: 'validation', message: 'library.json: blueprints[] is required and must not be empty', filePath: metaPath });
174
+ }
175
+ const seenSlugs = new Set();
176
+ for (const [i, entry] of doc.blueprints.entries()) {
177
+ if (typeof entry !== 'object' || entry === null) {
178
+ return rcfError({ kind: 'validation', message: `library.json: blueprints[${i}] must be an object`, filePath: metaPath });
179
+ }
180
+ if (typeof entry.slug !== 'string' || !KEBAB_SLUG.test(entry.slug)) {
181
+ return rcfError({ kind: 'validation', message: `library.json: blueprints[${i}].slug '${entry.slug}' is not a valid kebab slug`, filePath: metaPath });
182
+ }
183
+ if (seenSlugs.has(entry.slug)) {
184
+ return rcfError({ kind: 'validation', message: `library.json: blueprints[${i}].slug '${entry.slug}' is declared more than once`, filePath: metaPath });
185
+ }
186
+ seenSlugs.add(entry.slug);
187
+ if (typeof entry.path !== 'string' || entry.path.length === 0) {
188
+ return rcfError({ kind: 'validation', message: `library.json: blueprints[${i}].path is required (non-empty string)`, filePath: metaPath });
189
+ }
190
+ if (isAbsolute(entry.path)) {
191
+ return rcfError({ kind: 'validation', message: `library.json: blueprints[${i}].path '${entry.path}' must be relative to the library root`, filePath: metaPath });
192
+ }
193
+ if (entry.path.split(/[\\/]/).some((s) => s === '..')) {
194
+ return rcfError({ kind: 'validation', message: `library.json: blueprints[${i}].path '${entry.path}' contains a '..' segment (parent-directory traversal is refused)`, filePath: metaPath });
195
+ }
196
+ }
197
+ if (doc.notes !== undefined && typeof doc.notes !== 'string') {
198
+ return rcfError({ kind: 'validation', message: 'library.json: notes must be a string when present', filePath: metaPath });
199
+ }
200
+ return null;
201
+ }
202
+
203
+ function validateBands(bands, metaPath) {
204
+ if (typeof bands !== 'object' || bands === null) {
205
+ return rcfError({ kind: 'validation', message: 'library.json: bands object is required', filePath: metaPath });
206
+ }
207
+ if (typeof bands.ac !== 'object' || bands.ac === null) {
208
+ return rcfError({ kind: 'validation', message: 'library.json: bands.ac object is required with { start, end }', filePath: metaPath });
209
+ }
210
+ const err = validateBandRange('bands.ac', bands.ac, metaPath);
211
+ if (err) return err;
212
+ if (bands.suffixBlocks !== undefined) {
213
+ if (!Array.isArray(bands.suffixBlocks)) {
214
+ return rcfError({ kind: 'validation', message: 'library.json: bands.suffixBlocks must be an array when present', filePath: metaPath });
215
+ }
216
+ for (const [i, block] of bands.suffixBlocks.entries()) {
217
+ if (typeof block !== 'object' || block === null) {
218
+ return rcfError({ kind: 'validation', message: `library.json: bands.suffixBlocks[${i}] must be an object`, filePath: metaPath });
219
+ }
220
+ if (typeof block.kind !== 'string' || !KNOWN_SUFFIX_BLOCK_KINDS.has(block.kind)) {
221
+ return rcfError({ kind: 'validation', message: `library.json: bands.suffixBlocks[${i}].kind '${block.kind}' must be one of ${[...KNOWN_SUFFIX_BLOCK_KINDS].join(', ')}`, filePath: metaPath });
222
+ }
223
+ const subErr = validateBandRange(`bands.suffixBlocks[${i}]`, block, metaPath);
224
+ if (subErr) return subErr;
225
+ }
226
+ }
227
+ return null;
228
+ }
229
+
230
+ function validateBandRange(label, range, metaPath) {
231
+ if (!Number.isInteger(range.start) || !Number.isInteger(range.end)) {
232
+ return rcfError({ kind: 'validation', message: `library.json: ${label}.start and ${label}.end must be integers`, filePath: metaPath });
233
+ }
234
+ if (range.start < 1 || range.end > 99999) {
235
+ return rcfError({ kind: 'validation', message: `library.json: ${label} out of allowed 1..99999 range (got ${range.start}..${range.end})`, filePath: metaPath });
236
+ }
237
+ if (range.start > range.end) {
238
+ return rcfError({ kind: 'validation', message: `library.json: ${label}.start (${range.start}) must be <= ${label}.end (${range.end})`, filePath: metaPath });
239
+ }
240
+ return null;
241
+ }
242
+
243
+ function normaliseBands(bands) {
244
+ const out = { ac: { start: bands.ac.start, end: bands.ac.end } };
245
+ if (Array.isArray(bands.suffixBlocks)) {
246
+ out.suffixBlocks = bands.suffixBlocks.map((b) => ({ kind: b.kind, start: b.start, end: b.end }));
247
+ }
248
+ return out;
249
+ }
250
+
251
+ async function validateDeclaredBlueprints(library) {
252
+ for (const entry of library.blueprints) {
253
+ const bpRoot = join(library.root, entry.path);
254
+ const loaded = await loadBlueprint(bpRoot);
255
+ if (loaded.kind) {
256
+ return rcfError({
257
+ kind: 'validation',
258
+ message: `library.json: blueprint '${entry.slug}' at '${entry.path}' failed to load: ${loaded.message}`,
259
+ filePath: bpRoot,
260
+ });
261
+ }
262
+ if (loaded.slug !== entry.slug) {
263
+ return rcfError({
264
+ kind: 'validation',
265
+ message: `library.json: blueprint '${entry.slug}' at '${entry.path}' declares its own slug '${loaded.slug}' on disk (library manifest and blueprint.json must agree)`,
266
+ filePath: bpRoot,
267
+ });
268
+ }
269
+ }
270
+ return null;
271
+ }
@@ -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;