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
@@ -15,6 +15,7 @@ import { readFile } from 'node:fs/promises';
15
15
  import { rcfError } from '../core/errors/index.js';
16
16
  import { subdirFor } from '#core/store';
17
17
  import { detectCrossBlueprintClaims, detectGlobalAdrConflicts } from './conflicts.js';
18
+ import { detectContributionsOutOfBand } from './library-registry.js';
18
19
  import { loadBlueprint } from './loader.js';
19
20
  import { updateManifest } from './manifest-writer.js';
20
21
  import { stampId } from './namespace.js';
@@ -40,6 +41,26 @@ import { nextResolutionId } from './resolutions.js';
40
41
  * string the operator can copy back. Defaults to `source` when omitted (so
41
42
  * existing callers keep today's byte-for-byte behaviour).
42
43
  * @param {string} [args.namespaceOverride] - non-default namespace slug
44
+ * @param {string} [args.effectiveSlug] - override for the blueprint's own slug
45
+ * when applied through an external-library qualified reference (spec §5.3):
46
+ * `<libraryPrefix>-<blueprintSlug>`. When set, this becomes BOTH the
47
+ * stamping namespace (so contributions inherit the library prefix) AND
48
+ * the value written into `manifest.blueprints[].slug` on the applied
49
+ * record, so `rcf define blueprint remove wsd-auth-oauth2` reads back
50
+ * cleanly. `namespaceOverride` still wins over `effectiveSlug` if both
51
+ * are set (operator explicitly chose a different namespace).
52
+ * @param {string} [args.libraryPrefix] - the registered library prefix the
53
+ * blueprint was resolved through (spec §5.3, §7.3). When set the applied
54
+ * record carries a `libraryPrefix` field so `rcf library remove`'s
55
+ * ownership check reads the ownership fact off the record itself
56
+ * rather than string-matching `source`. Absent for shelf and path
57
+ * applies. Requires @stravica-ai/rcf-schemas 0.5.1 or later
58
+ * (`appliedBlueprintRecord.libraryPrefix`, additive optional).
59
+ * @param {{ ac: { start: number, end: number }, suffixBlocks?: Array<{ kind: string, start: number, end: number }> }} [args.libraryBands]
60
+ * Declared bands from the resolved library. When set, every stamped
61
+ * contribution is band-gated before write; a contribution whose numeric
62
+ * portion falls outside the library's bands refuses at apply-time (spec
63
+ * §8.3, open question 9.9 ratified as "add both").
43
64
  * @param {Array<{ topic: string, resolvedByAdrId: string }>} [args.resolveDeclarations]
44
65
  * Operator-supplied conflict resolutions declared on the add
45
66
  * itself. One entry per resolved topic. For each, the resolution
@@ -58,25 +79,38 @@ import { nextResolutionId } from './resolutions.js';
58
79
  * prove the rollback runs. Never used in production.
59
80
  * @returns {Promise<ApplyResult | import('../core/errors/index.js').RcfError>}
60
81
  */
61
- export async function applyBlueprint({ projectRoot, tree, source, displaySource, namespaceOverride, resolveDeclarations, now = new Date(), dryRun = false, _copyFileForTest }) {
82
+ export async function applyBlueprint({ projectRoot, tree, source, displaySource, namespaceOverride, effectiveSlug, libraryPrefix, libraryBands, resolveDeclarations, now = new Date(), dryRun = false, _copyFileForTest }) {
62
83
  const hintSource = typeof displaySource === 'string' && displaySource.length > 0 ? displaySource : source;
63
84
  const blueprint = await loadBlueprint(source);
64
85
  if (blueprint.kind) return blueprint; // RcfError
65
- const namespace = namespaceOverride ?? blueprint.slug;
86
+ // Effective slug rewires the blueprint's identity under a library
87
+ // prefix (spec §5.3). It becomes both the stamping namespace and the
88
+ // slug written into `manifest.blueprints[].slug`. `namespaceOverride`
89
+ // still wins over `effectiveSlug` (operator's explicit --namespace on
90
+ // the CLI). When neither is set the blueprint's own slug applies.
91
+ const appliedSlug = typeof effectiveSlug === 'string' && effectiveSlug.length > 0
92
+ ? effectiveSlug
93
+ : blueprint.slug;
94
+ const namespace = namespaceOverride ?? appliedSlug;
66
95
 
67
96
  const applied = tree.manifest?.blueprints ?? [];
68
- const existing = applied.find((b) => b.slug === blueprint.slug);
97
+ const existing = applied.find((b) => b.slug === appliedSlug);
69
98
  const stamped = stampContributions(blueprint.contributions, namespace);
70
99
  if (stamped.error) {
71
100
  return rcfError({ kind: 'validation', message: stamped.error });
72
101
  }
102
+ // Apply-time band gate for external-library blueprints.
103
+ if (libraryBands) {
104
+ const bandErr = detectContributionsOutOfBand(stamped.contributions, libraryBands);
105
+ if (bandErr) return bandErr;
106
+ }
73
107
 
74
108
  // Pre-detection: fold operator-supplied --resolve declarations into a
75
109
  // WORKING COPY of the manifest so the detector honours them on this
76
110
  // run. The final manifest write later composes the same resolution
77
111
  // records into the persisted manifest, so the in-memory copy and the
78
112
  // on-disk write agree.
79
- const incomingForConflicts = { slug: blueprint.slug, contributions: stamped.contributions };
113
+ const incomingForConflicts = { slug: appliedSlug, contributions: stamped.contributions };
80
114
  const declResult = composeDeclaredResolutions({
81
115
  manifest: tree.manifest,
82
116
  applied,
@@ -109,7 +143,7 @@ export async function applyBlueprint({ projectRoot, tree, source, displaySource,
109
143
  ...detectCrossBlueprintClaims(applied, incomingForConflicts),
110
144
  ];
111
145
  if (conflicts.length > 0) {
112
- return { applied: false, slug: blueprint.slug, version: blueprint.version, contributions: [], conflicts };
146
+ return { applied: false, slug: appliedSlug, version: blueprint.version, contributions: [], conflicts };
113
147
  }
114
148
 
115
149
  // Ownership set for the overwrite guard below. On a re-apply, the
@@ -122,7 +156,7 @@ export async function applyBlueprint({ projectRoot, tree, source, displaySource,
122
156
  if (existing && existing.version === blueprint.version) {
123
157
  return {
124
158
  applied: false, alreadyApplied: true,
125
- slug: blueprint.slug, version: blueprint.version,
159
+ slug: appliedSlug, version: blueprint.version,
126
160
  contributions: stamped.contributions,
127
161
  ...(duplicateResolveTopics.length > 0 ? { warnings: [{ kind: 'duplicateResolveTopic', topics: duplicateResolveTopics }] } : {}),
128
162
  };
@@ -151,7 +185,7 @@ export async function applyBlueprint({ projectRoot, tree, source, displaySource,
151
185
  writtenContributions.push(preserveScope({ id: c.id, kind: c.kind, path: relDest }, c));
152
186
  }
153
187
  } else {
154
- const tmpSuffix = `.rcf-tmp-${blueprint.slug}-${now.getTime()}`;
188
+ const tmpSuffix = `.rcf-tmp-${appliedSlug}-${now.getTime()}`;
155
189
  const stagedWrites = []; // { tmpAbs, absDest, relDest, id, kind, c }
156
190
  const rollback = async (err) => {
157
191
  for (const w of stagedWrites) {
@@ -184,7 +218,7 @@ export async function applyBlueprint({ projectRoot, tree, source, displaySource,
184
218
  for (const w of stagedWrites) await unlink(w.tmpAbs).catch(() => {});
185
219
  return rcfError({
186
220
  kind: 'duplicateId',
187
- message: `blueprint apply: contribution ${c.id} would overwrite an existing file at ${relDest} that is not recorded as owned by blueprint '${blueprint.slug}'.`,
221
+ message: `blueprint apply: contribution ${c.id} would overwrite an existing file at ${relDest} that is not recorded as owned by blueprint '${appliedSlug}'.`,
188
222
  filePath: relDest,
189
223
  });
190
224
  }
@@ -225,13 +259,26 @@ export async function applyBlueprint({ projectRoot, tree, source, displaySource,
225
259
  }
226
260
  }
227
261
 
228
- // Update manifest.blueprints[].
262
+ // Update manifest.blueprints[]. The applied record's `source` field
263
+ // carries the qualified typed ref for library-resolved blueprints
264
+ // (`wsd:auth-oauth2`) so `rcf define blueprint upgrade` reads back
265
+ // cleanly (spec §5.3); local-path applies carry the absolute path as
266
+ // today. When the apply resolved through a registered external
267
+ // library the record additionally carries `libraryPrefix`: the
268
+ // ownership fact for the library-registered ownership check in
269
+ // `rcf library remove`, so the registry may be edited (renamed,
270
+ // re-pointed, unregistered) without orphaning previously applied
271
+ // records. Shelf and path applies carry no `libraryPrefix`.
272
+ const recordSource = typeof displaySource === 'string' && displaySource.length > 0 && displaySource !== source
273
+ ? displaySource
274
+ : source;
229
275
  const nextEntry = {
230
- slug: blueprint.slug,
276
+ slug: appliedSlug,
231
277
  version: blueprint.version,
232
278
  appliedAt: now.toISOString(),
233
- source,
279
+ source: recordSource,
234
280
  ...(namespaceOverride ? { namespace: namespaceOverride } : {}),
281
+ ...(typeof libraryPrefix === 'string' && libraryPrefix.length > 0 ? { libraryPrefix } : {}),
235
282
  ...(writtenContributions.length > 0 ? { contributions: writtenContributions } : {}),
236
283
  };
237
284
  const manifestResult = await updateManifest({
@@ -239,7 +286,7 @@ export async function applyBlueprint({ projectRoot, tree, source, displaySource,
239
286
  manifest: tree.manifest,
240
287
  mutate: (next) => {
241
288
  const list = Array.isArray(next.blueprints) ? next.blueprints : [];
242
- const filtered = list.filter((b) => b.slug !== blueprint.slug);
289
+ const filtered = list.filter((b) => b.slug !== appliedSlug);
243
290
  filtered.push(nextEntry);
244
291
  next.blueprints = filtered;
245
292
  // Fold any --resolve declarations recorded on this add into the
@@ -257,7 +304,7 @@ export async function applyBlueprint({ projectRoot, tree, source, displaySource,
257
304
 
258
305
  return {
259
306
  applied: true,
260
- slug: blueprint.slug,
307
+ slug: appliedSlug,
261
308
  version: blueprint.version,
262
309
  contributions: writtenContributions,
263
310
  ...(duplicateResolveTopics.length > 0 ? { warnings: [{ kind: 'duplicateResolveTopic', topics: duplicateResolveTopics }] } : {}),
@@ -11,3 +11,15 @@ export { nextResolutionId, matchingResolution } from './resolutions.js';
11
11
  export { supersedeBlueprintTopic } from './supersede.js';
12
12
  export { diffBlueprintTopic, renderDiff } from './diff.js';
13
13
  export { resolveBlueprintSource, knownShelfSlugs, packagedShelfPath } from './shelf-resolver.js';
14
+ export { loadLibrary } from './library-loader.js';
15
+ export {
16
+ REGISTRY_PATH,
17
+ REGISTRY_VERSION,
18
+ readLibraryRegistry,
19
+ writeLibraryRegistry,
20
+ findLibrary,
21
+ loadCoreBandReservations,
22
+ detectBandOverlap,
23
+ detectContributionsOutOfBand,
24
+ detectPrefixCollision,
25
+ } from './library-registry.js';
@@ -0,0 +1,292 @@
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
+ * @property {string[]} [globalTopics] scope:global ADR topics this
48
+ * blueprint contributes. Populated only when the loader ran with
49
+ * `validateBlueprints: true` (the review-on-add path); resolver-time
50
+ * loads that skip per-blueprint validation leave the field absent.
51
+ * Callers use it to render the section 8.1 "Global topics these
52
+ * blueprints claim" line during library-add review.
53
+ */
54
+
55
+ /**
56
+ * @typedef {object} LibraryPublisher
57
+ * @property {string} id
58
+ * @property {string} displayName
59
+ * @property {string} [contact]
60
+ */
61
+
62
+ /**
63
+ * @typedef {object} LoadedLibrary
64
+ * @property {number} libraryVersion
65
+ * @property {string} libraryPrefix
66
+ * @property {string} displayName
67
+ * @property {LibraryPublisher} publisher
68
+ * @property {string} libraryRef
69
+ * @property {LibraryBands} bands
70
+ * @property {LibraryBlueprintEntry[]} blueprints
71
+ * @property {string} [notes]
72
+ * @property {string} root absolute path to the library root
73
+ */
74
+
75
+ /**
76
+ * Load and validate a library from a directory that carries a
77
+ * `library.json` at its root plus a `blueprints/` subtree.
78
+ *
79
+ * @param {string} libraryRoot - absolute or relative path
80
+ * @param {object} [opts]
81
+ * @param {boolean} [opts.validateBlueprints=true] - when false, skips the
82
+ * per-blueprint validation walk. Add-time review-on-add sets this to
83
+ * true so the operator sees a real refusal on a broken shelf; a
84
+ * registry read at resolver-time can set false to keep the hot path
85
+ * cheap.
86
+ * @returns {Promise<LoadedLibrary | import('../core/errors/index.js').RcfError>}
87
+ */
88
+ export async function loadLibrary(libraryRoot, opts = {}) {
89
+ const root = resolve(libraryRoot);
90
+ const metaPath = join(root, 'library.json');
91
+ try {
92
+ await stat(metaPath);
93
+ } catch (err) {
94
+ if (err.code === 'ENOENT') {
95
+ return rcfError({
96
+ kind: 'usage',
97
+ message: `library: no library.json found at ${metaPath}`,
98
+ filePath: metaPath,
99
+ });
100
+ }
101
+ return rcfError({ kind: 'ioFailure', message: `library: ${err.message}`, filePath: metaPath });
102
+ }
103
+ let raw;
104
+ try {
105
+ raw = await readFile(metaPath, 'utf8');
106
+ } catch (err) {
107
+ return rcfError({ kind: 'ioFailure', message: `library: read failed: ${err.message}`, filePath: metaPath });
108
+ }
109
+ let doc;
110
+ try {
111
+ doc = JSON.parse(raw);
112
+ } catch (err) {
113
+ return rcfError({ kind: 'parseFailure', message: `library: JSON parse failed: ${err.message}`, filePath: metaPath });
114
+ }
115
+ const shapeError = validateManifestShape(doc, metaPath);
116
+ if (shapeError) return shapeError;
117
+
118
+ const loaded = {
119
+ libraryVersion: doc.libraryVersion,
120
+ libraryPrefix: doc.libraryPrefix,
121
+ displayName: doc.displayName,
122
+ publisher: {
123
+ id: doc.publisher.id,
124
+ displayName: doc.publisher.displayName,
125
+ ...(typeof doc.publisher.contact === 'string' ? { contact: doc.publisher.contact } : {}),
126
+ },
127
+ libraryRef: doc.libraryRef,
128
+ bands: normaliseBands(doc.bands),
129
+ blueprints: doc.blueprints.map((b) => ({ slug: b.slug, path: b.path })),
130
+ ...(typeof doc.notes === 'string' ? { notes: doc.notes } : {}),
131
+ root,
132
+ };
133
+
134
+ if (opts.validateBlueprints !== false) {
135
+ const walkErr = await validateDeclaredBlueprints(loaded);
136
+ if (walkErr) return walkErr;
137
+ }
138
+ return loaded;
139
+ }
140
+
141
+ function validateManifestShape(doc, metaPath) {
142
+ if (typeof doc !== 'object' || doc === null) {
143
+ return rcfError({ kind: 'validation', message: 'library.json must be a JSON object', filePath: metaPath });
144
+ }
145
+ if (!Number.isInteger(doc.libraryVersion) || doc.libraryVersion < 1) {
146
+ return rcfError({ kind: 'validation', message: `library.json: libraryVersion must be a positive integer, got ${JSON.stringify(doc.libraryVersion)}`, filePath: metaPath });
147
+ }
148
+ if (doc.libraryVersion > LIBRARY_VERSION_KNOWN) {
149
+ return rcfError({
150
+ kind: 'validation',
151
+ message: `library.json: libraryVersion ${doc.libraryVersion} is newer than this CLI understands (max ${LIBRARY_VERSION_KNOWN}); upgrade rcf-lite.`,
152
+ filePath: metaPath,
153
+ });
154
+ }
155
+ if (typeof doc.libraryPrefix !== 'string' || !KEBAB_SLUG.test(doc.libraryPrefix)) {
156
+ return rcfError({ kind: 'validation', message: `library.json: libraryPrefix '${doc.libraryPrefix}' is not a valid kebab slug`, filePath: metaPath });
157
+ }
158
+ if (typeof doc.displayName !== 'string' || doc.displayName.length === 0) {
159
+ return rcfError({ kind: 'validation', message: 'library.json: displayName is required (non-empty string)', filePath: metaPath });
160
+ }
161
+ if (typeof doc.publisher !== 'object' || doc.publisher === null) {
162
+ return rcfError({ kind: 'validation', message: 'library.json: publisher object is required', filePath: metaPath });
163
+ }
164
+ if (typeof doc.publisher.id !== 'string' || !KEBAB_SLUG.test(doc.publisher.id)) {
165
+ return rcfError({ kind: 'validation', message: `library.json: publisher.id '${doc.publisher.id}' is not a valid short slug`, filePath: metaPath });
166
+ }
167
+ if (typeof doc.publisher.displayName !== 'string' || doc.publisher.displayName.length === 0) {
168
+ return rcfError({ kind: 'validation', message: 'library.json: publisher.displayName is required', filePath: metaPath });
169
+ }
170
+ if (doc.publisher.contact !== undefined && typeof doc.publisher.contact !== 'string') {
171
+ return rcfError({ kind: 'validation', message: 'library.json: publisher.contact must be a string when present', filePath: metaPath });
172
+ }
173
+ if (typeof doc.libraryRef !== 'string' || doc.libraryRef.length === 0) {
174
+ return rcfError({ kind: 'validation', message: 'library.json: libraryRef is required (non-empty string)', filePath: metaPath });
175
+ }
176
+ const bandsError = validateBands(doc.bands, metaPath);
177
+ if (bandsError) return bandsError;
178
+ if (!Array.isArray(doc.blueprints) || doc.blueprints.length === 0) {
179
+ return rcfError({ kind: 'validation', message: 'library.json: blueprints[] is required and must not be empty', filePath: metaPath });
180
+ }
181
+ const seenSlugs = new Set();
182
+ for (const [i, entry] of doc.blueprints.entries()) {
183
+ if (typeof entry !== 'object' || entry === null) {
184
+ return rcfError({ kind: 'validation', message: `library.json: blueprints[${i}] must be an object`, filePath: metaPath });
185
+ }
186
+ if (typeof entry.slug !== 'string' || !KEBAB_SLUG.test(entry.slug)) {
187
+ return rcfError({ kind: 'validation', message: `library.json: blueprints[${i}].slug '${entry.slug}' is not a valid kebab slug`, filePath: metaPath });
188
+ }
189
+ if (seenSlugs.has(entry.slug)) {
190
+ return rcfError({ kind: 'validation', message: `library.json: blueprints[${i}].slug '${entry.slug}' is declared more than once`, filePath: metaPath });
191
+ }
192
+ seenSlugs.add(entry.slug);
193
+ if (typeof entry.path !== 'string' || entry.path.length === 0) {
194
+ return rcfError({ kind: 'validation', message: `library.json: blueprints[${i}].path is required (non-empty string)`, filePath: metaPath });
195
+ }
196
+ if (isAbsolute(entry.path)) {
197
+ return rcfError({ kind: 'validation', message: `library.json: blueprints[${i}].path '${entry.path}' must be relative to the library root`, filePath: metaPath });
198
+ }
199
+ if (entry.path.split(/[\\/]/).some((s) => s === '..')) {
200
+ return rcfError({ kind: 'validation', message: `library.json: blueprints[${i}].path '${entry.path}' contains a '..' segment (parent-directory traversal is refused)`, filePath: metaPath });
201
+ }
202
+ }
203
+ if (doc.notes !== undefined && typeof doc.notes !== 'string') {
204
+ return rcfError({ kind: 'validation', message: 'library.json: notes must be a string when present', filePath: metaPath });
205
+ }
206
+ return null;
207
+ }
208
+
209
+ function validateBands(bands, metaPath) {
210
+ if (typeof bands !== 'object' || bands === null) {
211
+ return rcfError({ kind: 'validation', message: 'library.json: bands object is required', filePath: metaPath });
212
+ }
213
+ if (typeof bands.ac !== 'object' || bands.ac === null) {
214
+ return rcfError({ kind: 'validation', message: 'library.json: bands.ac object is required with { start, end }', filePath: metaPath });
215
+ }
216
+ const err = validateBandRange('bands.ac', bands.ac, metaPath);
217
+ if (err) return err;
218
+ if (bands.suffixBlocks !== undefined) {
219
+ if (!Array.isArray(bands.suffixBlocks)) {
220
+ return rcfError({ kind: 'validation', message: 'library.json: bands.suffixBlocks must be an array when present', filePath: metaPath });
221
+ }
222
+ for (const [i, block] of bands.suffixBlocks.entries()) {
223
+ if (typeof block !== 'object' || block === null) {
224
+ return rcfError({ kind: 'validation', message: `library.json: bands.suffixBlocks[${i}] must be an object`, filePath: metaPath });
225
+ }
226
+ if (typeof block.kind !== 'string' || !KNOWN_SUFFIX_BLOCK_KINDS.has(block.kind)) {
227
+ return rcfError({ kind: 'validation', message: `library.json: bands.suffixBlocks[${i}].kind '${block.kind}' must be one of ${[...KNOWN_SUFFIX_BLOCK_KINDS].join(', ')}`, filePath: metaPath });
228
+ }
229
+ const subErr = validateBandRange(`bands.suffixBlocks[${i}]`, block, metaPath);
230
+ if (subErr) return subErr;
231
+ }
232
+ }
233
+ return null;
234
+ }
235
+
236
+ function validateBandRange(label, range, metaPath) {
237
+ if (!Number.isInteger(range.start) || !Number.isInteger(range.end)) {
238
+ return rcfError({ kind: 'validation', message: `library.json: ${label}.start and ${label}.end must be integers`, filePath: metaPath });
239
+ }
240
+ if (range.start < 1 || range.end > 99999) {
241
+ return rcfError({ kind: 'validation', message: `library.json: ${label} out of allowed 1..99999 range (got ${range.start}..${range.end})`, filePath: metaPath });
242
+ }
243
+ if (range.start > range.end) {
244
+ return rcfError({ kind: 'validation', message: `library.json: ${label}.start (${range.start}) must be <= ${label}.end (${range.end})`, filePath: metaPath });
245
+ }
246
+ return null;
247
+ }
248
+
249
+ function normaliseBands(bands) {
250
+ const out = { ac: { start: bands.ac.start, end: bands.ac.end } };
251
+ if (Array.isArray(bands.suffixBlocks)) {
252
+ out.suffixBlocks = bands.suffixBlocks.map((b) => ({ kind: b.kind, start: b.start, end: b.end }));
253
+ }
254
+ return out;
255
+ }
256
+
257
+ async function validateDeclaredBlueprints(library) {
258
+ for (const entry of library.blueprints) {
259
+ const bpRoot = join(library.root, entry.path);
260
+ const loaded = await loadBlueprint(bpRoot);
261
+ if (loaded.kind) {
262
+ return rcfError({
263
+ kind: 'validation',
264
+ message: `library.json: blueprint '${entry.slug}' at '${entry.path}' failed to load: ${loaded.message}`,
265
+ filePath: bpRoot,
266
+ });
267
+ }
268
+ if (loaded.slug !== entry.slug) {
269
+ return rcfError({
270
+ kind: 'validation',
271
+ message: `library.json: blueprint '${entry.slug}' at '${entry.path}' declares its own slug '${loaded.slug}' on disk (library manifest and blueprint.json must agree)`,
272
+ filePath: bpRoot,
273
+ });
274
+ }
275
+ // Attach the scope:global ADR topics this blueprint claims so the
276
+ // review-on-add printer can render the spec §8.1 "Global topics
277
+ // these blueprints claim" line without re-walking every blueprint.
278
+ // Order is contribution-declaration order; duplicates within one
279
+ // blueprint (an author mistake caught by phase-1 conflict logic)
280
+ // are de-duplicated here to keep the render terse.
281
+ const seen = new Set();
282
+ const topics = [];
283
+ for (const c of loaded.contributions ?? []) {
284
+ if (c.scope === 'global' && typeof c.topic === 'string' && !seen.has(c.topic)) {
285
+ seen.add(c.topic);
286
+ topics.push(c.topic);
287
+ }
288
+ }
289
+ entry.globalTopics = topics;
290
+ }
291
+ return null;
292
+ }