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
@@ -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)) {
@@ -0,0 +1,419 @@
1
+ // `rcf define blueprint library <verb>` CLI.
2
+ //
3
+ // Verbs (Phase 2b):
4
+ // add register an external library on this project (local source in
5
+ // 2b; network fetchers land in 2c)
6
+ // list list registered libraries
7
+ // remove unregister a library (refuses when any applied blueprint on
8
+ // the project came through the library)
9
+ // refresh re-validate an already-registered library's on-disk shape
10
+ //
11
+ // Spec: external-blueprint-libraries-spec-2026-08-31.md sections 4, 8.
12
+
13
+ import { isAbsolute, resolve } from 'node:path';
14
+ import { stat } from 'node:fs/promises';
15
+ import { createInterface } from 'node:readline';
16
+
17
+ import { isRcfError } from '#core/errors';
18
+ import { findProjectRoot } from '../view/index.js';
19
+ import { loadLibrary } from '../blueprint/library-loader.js';
20
+ import {
21
+ detectBandOverlap,
22
+ detectPrefixCollision,
23
+ findLibrary,
24
+ loadCoreBandReservations,
25
+ readLibraryRegistry,
26
+ writeLibraryRegistry,
27
+ } from '../blueprint/library-registry.js';
28
+ import { knownShelfSlugs, packagedShelfPath } from '../blueprint/shelf-resolver.js';
29
+ import { walkTree } from '#core/store';
30
+
31
+ /**
32
+ * Union of core-shelf blueprint slugs used for the prefix-collision
33
+ * gate. The packaged-shelf directory is copied into the tarball at
34
+ * prepack time (`files: ['blueprints']`), so a source checkout does
35
+ * not have `packages/rcf-lite/blueprints/` on disk; falling back to
36
+ * the shipped `data/core-band-reservations.json` keeps the gate
37
+ * effective in both shapes.
38
+ */
39
+ async function collectCoreSlugs() {
40
+ const fromShelf = await knownShelfSlugs(packagedShelfPath()).catch(() => []);
41
+ const reservations = await loadCoreBandReservations();
42
+ const slugs = new Set(fromShelf);
43
+ for (const row of reservations.ac ?? []) if (row.blueprint) slugs.add(row.blueprint);
44
+ for (const row of reservations.suffixBlocks ?? []) if (row.blueprint) slugs.add(row.blueprint);
45
+ return [...slugs];
46
+ }
47
+
48
+ export const LIBRARY_HELP = `Usage: rcf define blueprint library <verb> [options]
49
+
50
+ Verbs:
51
+ add <ref> Register an external library on this project.
52
+ <ref> is either a local absolute or relative path
53
+ to a library root (a directory containing
54
+ library.json). Fetches metadata, runs review-on-
55
+ add, and writes an entry to
56
+ rcf/blueprint-libraries.json.
57
+
58
+ Phase 2b covers local sources; the git and
59
+ tarball fetchers land in Phase 2c. A private-repo
60
+ library today is registered via a local clone.
61
+
62
+ list [--json] List every registered library on this project
63
+ (prefix, source, publisher, blueprint count).
64
+
65
+ remove <prefix> Unregister a library. Refuses when any applied
66
+ blueprint on the project came through this
67
+ library.
68
+
69
+ refresh <prefix> Re-validate an already-registered library's
70
+ on-disk shape. Phase 2b re-reads the local
71
+ library.json and reports any drift from the
72
+ registry snapshot; phase 2c will re-fetch a git
73
+ or tarball source and verify the sha.
74
+
75
+ Options:
76
+ --prefix <slug> (add) Override the library's declared prefix.
77
+ Rarely needed; only useful when two libraries
78
+ collide on prefix locally.
79
+ --i-have-reviewed (add) Skip the interactive review-on-add prompt.
80
+ Required companion to --no-review when scripting;
81
+ the two-flag form keeps a library-add trust
82
+ decision loud (spec §9.7).
83
+ --no-review (add) Bypass the interactive prompt. Requires
84
+ --i-have-reviewed.
85
+ --json (add, list) Emit machine-readable JSON.
86
+ --dry-run Print intended writes without executing.
87
+ --quiet Suppress non-error stdout.
88
+ --help Print this help.
89
+ `;
90
+
91
+ const LIBRARY_OPTION_SPEC = {
92
+ prefix: { type: 'string' },
93
+ 'i-have-reviewed': { type: 'boolean' },
94
+ 'no-review': { type: 'boolean' },
95
+ json: { type: 'boolean' },
96
+ 'dry-run': { type: 'boolean' },
97
+ quiet: { type: 'boolean' },
98
+ help: { type: 'boolean' },
99
+ };
100
+
101
+ /**
102
+ * Handle `rcf define blueprint library <verb>`.
103
+ *
104
+ * @param {import('node:util').ParseArgsConfig} parsed - already-parsed argv (see cli/blueprint.js)
105
+ * @param {string[]} rest - positional args after `library`
106
+ * @param {object} deps
107
+ * @returns {Promise<number>}
108
+ */
109
+ export async function handleLibraryVerb(parsed, rest, deps) {
110
+ const stdout = deps.stdout;
111
+ const stderr = deps.stderr;
112
+ const cwd = deps.cwd;
113
+ const now = deps.now ?? new Date();
114
+ const stdin = deps.stdin ?? process.stdin;
115
+
116
+ if (parsed.values.help || rest.length === 0) {
117
+ stdout.write(LIBRARY_HELP);
118
+ return 0;
119
+ }
120
+ const verb = rest[0];
121
+ const args = rest.slice(1);
122
+
123
+ const projectRoot = await findProjectRoot(cwd);
124
+ if (!projectRoot) {
125
+ stderr.write('[error] no rcf/ tree found in this directory or any ancestor.\n');
126
+ return 2;
127
+ }
128
+
129
+ if (verb === 'add') return handleAdd({ args, parsed, projectRoot, now, stdout, stderr, stdin });
130
+ if (verb === 'list') return handleList({ parsed, projectRoot, stdout, stderr });
131
+ if (verb === 'remove') return handleRemove({ args, parsed, projectRoot, stdout, stderr });
132
+ if (verb === 'refresh') return handleRefresh({ args, parsed, projectRoot, stdout, stderr });
133
+
134
+ stderr.write(`[error] blueprint library: unknown verb '${verb}'\n`);
135
+ stderr.write(LIBRARY_HELP);
136
+ return 2;
137
+ }
138
+
139
+ async function handleAdd({ args, parsed, projectRoot, now, stdout, stderr, stdin }) {
140
+ if (args.length === 0) {
141
+ stderr.write('[error] blueprint library add: missing <ref>\n');
142
+ return 2;
143
+ }
144
+ const ref = args[0];
145
+
146
+ // Phase 2b: local sources only. Fetchers land in 2c.
147
+ const kind = classifySourceRef(ref);
148
+ if (kind !== 'local') {
149
+ stderr.write(
150
+ `[error] blueprint library add: source kind '${kind}' requires the network fetchers landing in Phase 2c. `
151
+ + 'In 2b, register the library from a local path (a directory carrying library.json). '
152
+ + 'For private git repos today, clone the repo locally and point add at the clone.\n',
153
+ );
154
+ return 2;
155
+ }
156
+
157
+ const localRoot = isAbsolute(ref) ? ref : resolve(projectRoot, ref);
158
+ let libStat;
159
+ try {
160
+ libStat = await stat(localRoot);
161
+ } catch (err) {
162
+ stderr.write(`[error] blueprint library add: path '${localRoot}' cannot be read: ${err.message}\n`);
163
+ return 2;
164
+ }
165
+ if (!libStat.isDirectory()) {
166
+ stderr.write(`[error] blueprint library add: path '${localRoot}' is not a directory.\n`);
167
+ return 2;
168
+ }
169
+
170
+ const library = await loadLibrary(localRoot, { validateBlueprints: true });
171
+ if (isRcfError(library)) {
172
+ stderr.write(`[error] blueprint library add: ${library.message}\n`);
173
+ return 2;
174
+ }
175
+
176
+ const libraryPrefix = typeof parsed.values.prefix === 'string' && parsed.values.prefix.length > 0
177
+ ? parsed.values.prefix
178
+ : library.libraryPrefix;
179
+
180
+ const registry = await readLibraryRegistry(projectRoot);
181
+ if (isRcfError(registry)) {
182
+ stderr.write(`[error] blueprint library add: ${registry.message}\n`);
183
+ return 2;
184
+ }
185
+ if (findLibrary(registry, libraryPrefix)) {
186
+ stderr.write(`[error] blueprint library add: libraryPrefix '${libraryPrefix}' is already registered on this project. Use 'library refresh' or 'library remove' first.\n`);
187
+ return 2;
188
+ }
189
+ const coreSlugs = await collectCoreSlugs();
190
+ const prefixErr = detectPrefixCollision({ libraryPrefix, registry, coreSlugs });
191
+ if (prefixErr) {
192
+ stderr.write(`[error] blueprint library add: ${prefixErr.message}\n`);
193
+ return 2;
194
+ }
195
+ const coreReservations = await loadCoreBandReservations();
196
+ const bandErr = detectBandOverlap({
197
+ candidate: { libraryPrefix, bands: library.bands },
198
+ registry,
199
+ coreReservations,
200
+ });
201
+ if (bandErr) {
202
+ stderr.write(`[error] blueprint library add: ${bandErr.message}\n`);
203
+ return 2;
204
+ }
205
+
206
+ const wantsReview = parsed.values['no-review'] !== true;
207
+ const iHaveReviewed = parsed.values['i-have-reviewed'] === true;
208
+ if (parsed.values['no-review'] === true && !iHaveReviewed) {
209
+ stderr.write('[error] blueprint library add: --no-review requires --i-have-reviewed (spec §9.7: the two-flag form keeps the trust decision loud).\n');
210
+ return 2;
211
+ }
212
+ if (wantsReview) {
213
+ printReview({ stdout, ref, library, libraryPrefix, coreReservations });
214
+ if (!iHaveReviewed) {
215
+ const proceed = await prompt(stdin, stdout, 'Proceed with add? [y/N] ');
216
+ if (!/^y(es)?$/i.test((proceed ?? '').trim())) {
217
+ stdout.write('[blueprint library] aborted by operator; no registry entry written.\n');
218
+ return 0;
219
+ }
220
+ }
221
+ }
222
+
223
+ const entry = {
224
+ libraryPrefix,
225
+ sourceKind: 'local',
226
+ sourceRef: localRoot,
227
+ displayName: library.displayName,
228
+ publisher: { ...library.publisher },
229
+ libraryRef: library.libraryRef,
230
+ bands: library.bands,
231
+ blueprints: library.blueprints.map((b) => ({ slug: b.slug, path: b.path })),
232
+ addedAt: now.toISOString(),
233
+ reviewedBy: 'operator',
234
+ provenance: { tier: 'local' },
235
+ cachePath: localRoot,
236
+ };
237
+
238
+ const nextRegistry = {
239
+ registryVersion: registry.registryVersion,
240
+ libraries: [...registry.libraries, entry],
241
+ };
242
+ const write = await writeLibraryRegistry(projectRoot, nextRegistry, { dryRun: parsed.values['dry-run'] === true });
243
+ if (isRcfError(write)) {
244
+ stderr.write(`[error] blueprint library add: ${write.message}\n`);
245
+ return 2;
246
+ }
247
+ if (parsed.values.json) {
248
+ stdout.write(`${JSON.stringify({
249
+ added: !parsed.values['dry-run'],
250
+ dryRun: parsed.values['dry-run'] === true,
251
+ libraryPrefix: entry.libraryPrefix,
252
+ blueprintCount: entry.blueprints.length,
253
+ registryPath: write.path,
254
+ })}\n`);
255
+ return 0;
256
+ }
257
+ if (!parsed.values.quiet) {
258
+ if (parsed.values['dry-run']) {
259
+ stdout.write(`[blueprint library] dry-run: would add '${libraryPrefix}' (${entry.blueprints.length} blueprint(s)) to ${write.path}.\n`);
260
+ } else {
261
+ stdout.write(`[blueprint library] added '${libraryPrefix}' (${entry.blueprints.length} blueprint(s)) to ${write.path}.\n`);
262
+ }
263
+ }
264
+ return 0;
265
+ }
266
+
267
+ async function handleList({ parsed, projectRoot, stdout, stderr }) {
268
+ const registry = await readLibraryRegistry(projectRoot);
269
+ if (isRcfError(registry)) {
270
+ stderr.write(`[error] blueprint library list: ${registry.message}\n`);
271
+ return 2;
272
+ }
273
+ if (parsed.values.json) {
274
+ stdout.write(`${JSON.stringify({
275
+ registryVersion: registry.registryVersion,
276
+ libraries: registry.libraries.map((l) => ({
277
+ libraryPrefix: l.libraryPrefix,
278
+ sourceKind: l.sourceKind,
279
+ sourceRef: l.sourceRef,
280
+ publisher: l.publisher,
281
+ libraryRef: l.libraryRef,
282
+ blueprintCount: Array.isArray(l.blueprints) ? l.blueprints.length : 0,
283
+ addedAt: l.addedAt,
284
+ })),
285
+ }, null, 2)}\n`);
286
+ return 0;
287
+ }
288
+ if (registry.libraries.length === 0) {
289
+ if (!parsed.values.quiet) stdout.write('[blueprint library] no libraries registered on this project.\n');
290
+ return 0;
291
+ }
292
+ for (const l of registry.libraries) {
293
+ const count = Array.isArray(l.blueprints) ? l.blueprints.length : 0;
294
+ stdout.write(`${l.libraryPrefix}\t${l.sourceKind}\t${l.publisher.displayName}\t${count} blueprint(s)\t${l.sourceRef}\n`);
295
+ }
296
+ return 0;
297
+ }
298
+
299
+ async function handleRemove({ args, parsed, projectRoot, stdout, stderr }) {
300
+ if (args.length === 0) {
301
+ stderr.write('[error] blueprint library remove: missing <libraryPrefix>\n');
302
+ return 2;
303
+ }
304
+ const libraryPrefix = args[0];
305
+ const registry = await readLibraryRegistry(projectRoot);
306
+ if (isRcfError(registry)) {
307
+ stderr.write(`[error] blueprint library remove: ${registry.message}\n`);
308
+ return 2;
309
+ }
310
+ const entry = findLibrary(registry, libraryPrefix);
311
+ if (!entry) {
312
+ stderr.write(`[error] blueprint library remove: library '${libraryPrefix}' is not registered.\n`);
313
+ return 2;
314
+ }
315
+ // Refuse when any applied blueprint on the project came through the
316
+ // library. Signal: `manifest.blueprints[].source` starts with `<prefix>:`.
317
+ const { tree, errors } = await walkTree({ projectRoot });
318
+ if (errors.length > 0) {
319
+ for (const e of errors) stderr.write(`[tree] ${e.kind}: ${e.message}\n`);
320
+ return 2;
321
+ }
322
+ const referring = (tree.manifest?.blueprints ?? []).filter((b) => typeof b.source === 'string' && b.source.startsWith(`${libraryPrefix}:`));
323
+ if (referring.length > 0) {
324
+ stderr.write(`[error] blueprint library remove: ${referring.length} applied blueprint(s) came through '${libraryPrefix}':\n`);
325
+ for (const r of referring) stderr.write(` ${r.slug} <- ${r.source}\n`);
326
+ stderr.write('remove those first (rcf define blueprint remove <slug>) or explicitly force with an escalation flag (not in v1).\n');
327
+ return 3;
328
+ }
329
+ const nextRegistry = {
330
+ registryVersion: registry.registryVersion,
331
+ libraries: registry.libraries.filter((l) => l.libraryPrefix !== libraryPrefix),
332
+ };
333
+ const write = await writeLibraryRegistry(projectRoot, nextRegistry, { dryRun: parsed.values['dry-run'] === true });
334
+ if (isRcfError(write)) {
335
+ stderr.write(`[error] blueprint library remove: ${write.message}\n`);
336
+ return 2;
337
+ }
338
+ if (!parsed.values.quiet) {
339
+ stdout.write(`[blueprint library] removed '${libraryPrefix}' from ${write.path}.\n`);
340
+ }
341
+ return 0;
342
+ }
343
+
344
+ async function handleRefresh({ args, parsed, projectRoot, stdout, stderr }) {
345
+ if (args.length === 0) {
346
+ stderr.write('[error] blueprint library refresh: missing <libraryPrefix>\n');
347
+ return 2;
348
+ }
349
+ const libraryPrefix = args[0];
350
+ const registry = await readLibraryRegistry(projectRoot);
351
+ if (isRcfError(registry)) {
352
+ stderr.write(`[error] blueprint library refresh: ${registry.message}\n`);
353
+ return 2;
354
+ }
355
+ const entry = findLibrary(registry, libraryPrefix);
356
+ if (!entry) {
357
+ stderr.write(`[error] blueprint library refresh: library '${libraryPrefix}' is not registered.\n`);
358
+ return 2;
359
+ }
360
+ if (entry.sourceKind !== 'local') {
361
+ stderr.write(`[error] blueprint library refresh: source kind '${entry.sourceKind}' requires the network fetchers landing in Phase 2c.\n`);
362
+ return 2;
363
+ }
364
+ const library = await loadLibrary(entry.cachePath, { validateBlueprints: true });
365
+ if (isRcfError(library)) {
366
+ stderr.write(`[error] blueprint library refresh: ${library.message}\n`);
367
+ return 2;
368
+ }
369
+ // Detect drift from the registry snapshot on load-bearing fields.
370
+ const drifted = [];
371
+ if (library.libraryPrefix !== entry.libraryPrefix) drifted.push(`libraryPrefix '${entry.libraryPrefix}' -> '${library.libraryPrefix}'`);
372
+ if (library.libraryRef !== entry.libraryRef) drifted.push(`libraryRef '${entry.libraryRef}' -> '${library.libraryRef}'`);
373
+ if (JSON.stringify(library.bands) !== JSON.stringify(entry.bands)) drifted.push('bands changed');
374
+ if (drifted.length > 0) {
375
+ stderr.write(`[blueprint library refresh] '${libraryPrefix}' has drifted from the registered snapshot:\n`);
376
+ for (const d of drifted) stderr.write(` ${d}\n`);
377
+ stderr.write("re-run 'rcf define blueprint library add <ref>' to pick up the newer library (spec §10.1: freshness is advisory, adoption is an operator act).\n");
378
+ return 3;
379
+ }
380
+ if (!parsed.values.quiet) {
381
+ stdout.write(`[blueprint library] '${libraryPrefix}' refresh clean: on-disk library matches the registry snapshot.\n`);
382
+ }
383
+ return 0;
384
+ }
385
+
386
+ function classifySourceRef(ref) {
387
+ if (ref.startsWith('git+') || ref.startsWith('git@') || ref.startsWith('https://') || ref.startsWith('http://') || ref.startsWith('ssh://')) return 'git';
388
+ if (ref.endsWith('.tar.gz') || ref.endsWith('.tgz') || ref.endsWith('.tar')) return 'tarball';
389
+ return 'local';
390
+ }
391
+
392
+ function printReview({ stdout, ref, library, libraryPrefix, coreReservations }) {
393
+ const suffix = (library.bands.suffixBlocks ?? []).map((b) => `${b.kind} ${b.start}-${b.end}`).join(', ');
394
+ stdout.write(`\nREVIEW - you are about to add this library to the project registry.\n\n`);
395
+ stdout.write(` Library : ${library.displayName}\n`);
396
+ stdout.write(` Prefix : ${libraryPrefix}\n`);
397
+ stdout.write(` Publisher : ${library.publisher.displayName}${library.publisher.contact ? ` <${library.publisher.contact}>` : ''}\n`);
398
+ stdout.write(` Source : ${ref} (local)\n`);
399
+ stdout.write(` Library ref : ${library.libraryRef}\n`);
400
+ stdout.write(` AC band : ${library.bands.ac.start} - ${library.bands.ac.end}\n`);
401
+ if (suffix.length > 0) stdout.write(` Suffix blocks: ${suffix}\n`);
402
+ stdout.write(`\n Blueprints on this library:\n`);
403
+ for (const bp of library.blueprints) {
404
+ stdout.write(` ${libraryPrefix}:${bp.slug}\n`);
405
+ }
406
+ stdout.write(`\n Provenance : local (dev use)\n`);
407
+ stdout.write(` Band check : cross-checked ${coreReservations.ac.length} core AC row(s), ${coreReservations.suffixBlocks.length} core suffix block(s); no overlap.\n`);
408
+ stdout.write(`\n`);
409
+ }
410
+
411
+ async function prompt(stdin, stdout, question) {
412
+ return new Promise((resolveP) => {
413
+ const rl = createInterface({ input: stdin, output: stdout });
414
+ rl.question(question, (answer) => {
415
+ rl.close();
416
+ resolveP(answer);
417
+ });
418
+ });
419
+ }