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.
- package/CHANGELOG.md +48 -1
- package/bin/rcf.js +3 -1
- package/bin/view-supervisor-child.mjs +0 -0
- package/blueprints/application-api-rest/docs/topics.md +1 -1
- package/blueprints/application-spa/README.md +3 -3
- package/blueprints/application-spa/contributions/adrs/adr-202-application-spa-theming.json +1 -1
- package/blueprints/application-spa/contributions/adrs/adr-206-application-spa-iconography.json +1 -1
- package/blueprints/application-spa/contributions/tacs/tac-207-application-spa-token-adherence-probe.json +7 -7
- package/blueprints/application-spa/contributions/tacs/tac-208-application-spa-icon-adherence-probe.json +7 -7
- package/blueprints/application-spa/contributions/tacs/tac-209-application-spa-csp-styled-adherence-probe.json +8 -8
- package/blueprints/application-spa/contributions/tacs/tac-210-application-spa-external-dependency-provisioning-probe.json +8 -8
- package/blueprints/application-spa/contributions/tacs/tac-211-application-spa-core-flow-e2e-probe.json +8 -8
- package/blueprints/application-spa/contributions/user-stories/application-spa-us-1129.json +2 -2
- package/blueprints/application-spa/contributions/user-stories/application-spa-us-1130.json +2 -2
- package/blueprints/application-spa/contributions/user-stories/application-spa-us-1131.json +2 -2
- package/blueprints/application-spa/contributions/user-stories/application-spa-us-1132.json +2 -2
- package/blueprints/application-spa/contributions/user-stories/application-spa-us-1133.json +3 -3
- package/blueprints/application-spa/docs/topics.md +1 -1
- package/blueprints/delivery-ci-workflows/CHANGELOG.md +39 -0
- package/blueprints/delivery-ci-workflows/README.md +61 -0
- package/blueprints/delivery-ci-workflows/assets/bootstrap/README.md +26 -0
- package/blueprints/delivery-ci-workflows/assets/bootstrap/adr-bootstrap-coverage-supersession.template.json +28 -0
- package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/github-actions/default-branch-checks.yml +61 -0
- package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/github-actions/pull-request-checks.yml +74 -0
- package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/github-actions/release.yml +75 -0
- package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/github-actions/scheduled-audit.yml +65 -0
- package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/notes.md +61 -0
- package/blueprints/{ci-pipeline → delivery-ci-workflows}/assets/report-samples/per-gate.json +3 -2
- package/blueprints/delivery-ci-workflows/blueprint.json +87 -0
- package/blueprints/delivery-ci-workflows/contributions/adrs/adr-701-delivery-ci-workflows-ci-gates.json +30 -0
- 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
- 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
- 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
- package/blueprints/delivery-ci-workflows/contributions/adrs/adr-705-delivery-ci-workflows-elicitation-surface.json +25 -0
- package/blueprints/delivery-ci-workflows/contributions/adrs/adr-706-delivery-ci-workflows-branch-model-defaults.json +25 -0
- package/blueprints/delivery-ci-workflows/contributions/adrs/adr-707-delivery-ci-workflows-release-workflow-shape.json +25 -0
- package/blueprints/delivery-ci-workflows/contributions/adrs/adr-708-delivery-ci-workflows-provider-hint-shape.json +25 -0
- package/blueprints/delivery-ci-workflows/contributions/adrs/adr-709-delivery-ci-workflows-release-artefacts.json +25 -0
- package/blueprints/delivery-ci-workflows/contributions/adrs/adr-710-delivery-ci-workflows-scheduled-audit.json +25 -0
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-001.json +18 -0
- package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-002.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-002.json} +2 -2
- package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-003.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-003.json} +2 -2
- package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-004.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-004.json} +2 -2
- package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-005.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-005.json} +4 -4
- package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-006.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-006.json} +2 -2
- package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-007.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-007.json} +2 -2
- package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-008.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-008.json} +2 -2
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-009.json +18 -0
- package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-010.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-010.json} +2 -2
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-011.json +18 -0
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-012.json +18 -0
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-013.json +18 -0
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-014.json +18 -0
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-015.json +18 -0
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-016.json +18 -0
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-017.json +18 -0
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-018.json +18 -0
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-019.json +18 -0
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-020.json +18 -0
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-021.json +18 -0
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-022.json +18 -0
- package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-023.json +18 -0
- 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
- 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
- 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
- package/blueprints/delivery-ci-workflows/contributions/tacs/tac-704-delivery-ci-workflows-workflow-materialiser.json +67 -0
- package/blueprints/delivery-ci-workflows/contributions/tacs/tac-705-delivery-ci-workflows-release-workflow.json +51 -0
- package/blueprints/delivery-ci-workflows/contributions/tacs/tac-706-delivery-ci-workflows-scheduled-audit.json +38 -0
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6101.json +37 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6109.json +36 -0
- 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
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6111.json +36 -0
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6112.json +36 -0
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6113.json +36 -0
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6114.json +46 -0
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6115.json +37 -0
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6116.json +28 -0
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6117.json +28 -0
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6118.json +37 -0
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6119.json +28 -0
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6120.json +28 -0
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6121.json +46 -0
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6122.json +46 -0
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6123.json +37 -0
- package/blueprints/delivery-ci-workflows/docs/topics.md +61 -0
- package/blueprints/delivery-ci-workflows/guide/delivery-ci-workflows.md +174 -0
- package/blueprints/deploy-cloudflare-workers/docs/topics.md +3 -3
- package/blueprints/email-smtp-resend/docs/topics.md +1 -1
- package/blueprints/observability-essentials/README.md +2 -2
- package/blueprints/observability-essentials/docs/topics.md +5 -5
- package/blueprints/observability-probe-endpoints/docs/topics.md +2 -2
- package/blueprints/persistence-data-d1/README.md +2 -2
- package/blueprints/persistence-data-d1/assets/facade-shape/facade-module-shape.md +1 -1
- package/blueprints/persistence-data-d1/contributions/tacs/tac-1403-persistence-data-d1-deploy-gate.json +1 -1
- package/blueprints/persistence-data-d1/docs/topics.md +2 -2
- package/blueprints/persistence-data-d1/guide/persistence-data-d1.md +1 -1
- package/blueprints/persistence-data-sqlite/README.md +1 -1
- package/blueprints/persistence-data-sqlite/docs/topics.md +1 -1
- package/blueprints/security-auth-clerk/README.md +5 -3
- package/blueprints/security-auth-clerk/assets/middleware/workers-fetch-shape.md +123 -0
- package/blueprints/security-auth-clerk/assets/wiring/workers-wrangler-toml-shape.md +51 -0
- package/blueprints/security-auth-clerk/blueprint.json +1 -1
- package/blueprints/security-auth-clerk/docs/topics.md +2 -2
- package/blueprints/security-auth-clerk/guide/security-auth-clerk.md +9 -0
- package/blueprints/security-auth-keycloak/docs/topics.md +2 -2
- package/blueprints/security-auth-magic-link/README.md +1 -1
- package/blueprints/security-auth-magic-link/docs/topics.md +1 -1
- package/blueprints/security-auth-oauth2/README.md +1 -1
- package/blueprints/security-auth-oauth2/docs/topics.md +2 -2
- package/blueprints/security-secrets-management/README.md +1 -1
- package/blueprints/security-secrets-management/docs/topics.md +3 -3
- package/guidance/build-cycle-playbook.md +2 -2
- package/guidance/document-model.md +1 -1
- package/guidance/harness-template.md +13 -0
- package/guidance/managed/agent-instructions-block.hash +1 -1
- package/guidance/managed/agent-instructions-block.md +13 -0
- package/package.json +15 -14
- package/rcf/adrs/adr-001.json +1 -1
- package/rcf/adrs/adr-009.json +1 -1
- package/rcf/build-sequence.json +1 -1
- package/rcf/manifest.json +2 -2
- package/rcf/prd.json +2 -2
- package/releases/releases.yaml +126 -0
- package/src/blueprint/apply.js +60 -13
- package/src/blueprint/index.js +12 -0
- package/src/blueprint/library-loader.js +292 -0
- package/src/blueprint/library-registry.js +341 -0
- package/src/blueprint/list.js +38 -4
- package/src/blueprint/shelf-resolver.js +144 -31
- package/src/blueprint/supersede.js +56 -13
- package/src/cli/blueprint-library.js +447 -0
- package/src/cli/blueprint.js +50 -9
- package/src/cli/guidance.js +1 -1
- package/src/cli/help.js +27 -1
- package/src/cli/version.js +673 -0
- package/src/cli/view.js +282 -1
- package/src/server/index.js +3 -0
- package/src/server/routes.js +15 -1
- package/src/server/scope-endpoint.js +105 -0
- package/src/view/live-client.js +253 -6
- package/src/view/scope.js +231 -0
- package/src/view/style.css +42 -0
- package/blueprints/ci-pipeline/README.md +0 -49
- package/blueprints/ci-pipeline/assets/ci-provider-examples/github-actions.yml +0 -61
- package/blueprints/ci-pipeline/assets/ci-provider-examples/notes.md +0 -50
- package/blueprints/ci-pipeline/blueprint.json +0 -46
- package/blueprints/ci-pipeline/contributions/adrs/adr-701-ci-pipeline-ci-gates.json +0 -25
- package/blueprints/ci-pipeline/contributions/requirements/ci-pipeline-req-001.json +0 -18
- package/blueprints/ci-pipeline/contributions/requirements/ci-pipeline-req-009.json +0 -18
- package/blueprints/ci-pipeline/contributions/user-stories/ci-pipeline-us-6101.json +0 -37
- package/blueprints/ci-pipeline/contributions/user-stories/ci-pipeline-us-6109.json +0 -36
- package/blueprints/ci-pipeline/docs/topics.md +0 -49
- package/blueprints/ci-pipeline/guide/ci-pipeline.md +0 -79
- package/rcf/.identity/profile.md +0 -37
- package/rcf/knowledge/INDEX.md +0 -12
- package/rcf/knowledge/README.md +0 -41
- package/rcf/knowledge/docs/.gitkeep +0 -0
- package/rcf/knowledge/notes/.gitkeep +0 -0
- /package/blueprints/{ci-pipeline → delivery-ci-workflows}/assets/report-samples/pipeline.json +0 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
// Project-config external-library registry, at
|
|
2
|
+
// `rcf/blueprint-libraries.json`. Written and read only by the
|
|
3
|
+
// `rcf define blueprint library` verb family; the manifest walker does
|
|
4
|
+
// not touch it (per spec 4.1 rationale: adding a required cross-cutting
|
|
5
|
+
// section into `manifest.json` ripples through every downstream
|
|
6
|
+
// validator, so the registry sits as a discrete file next to the
|
|
7
|
+
// manifest, exactly like `rcf/.identity/` sits next to it).
|
|
8
|
+
//
|
|
9
|
+
// Phase 2b (this file) covers registry CRUD + band-overlap gates.
|
|
10
|
+
// Phase 2c layers the network fetchers on top and populates the
|
|
11
|
+
// `resolvedSha` / `tarballSha256` provenance fields; a `local` source
|
|
12
|
+
// bypasses both and carries a `local` provenance tag.
|
|
13
|
+
//
|
|
14
|
+
// Spec: external-blueprint-libraries-spec-2026-08-31.md sections 4, 7, 8.3.
|
|
15
|
+
|
|
16
|
+
import { existsSync } from 'node:fs';
|
|
17
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
18
|
+
import { dirname, join } from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
|
|
21
|
+
import { rcfError } from '../core/errors/index.js';
|
|
22
|
+
|
|
23
|
+
export const REGISTRY_VERSION = 1;
|
|
24
|
+
export const REGISTRY_PATH = 'rcf/blueprint-libraries.json';
|
|
25
|
+
|
|
26
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
// packages/rcf-lite/src/blueprint -> packages/rcf-lite -> data/
|
|
28
|
+
const CORE_BAND_RESERVATIONS_PATH = join(here, '..', '..', 'data', 'core-band-reservations.json');
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @typedef {object} RegistryEntry
|
|
32
|
+
* @property {string} libraryPrefix
|
|
33
|
+
* @property {'local' | 'git' | 'tarball'} sourceKind
|
|
34
|
+
* @property {string} sourceRef
|
|
35
|
+
* @property {string} [resolvedSha]
|
|
36
|
+
* @property {string} displayName
|
|
37
|
+
* @property {{ id: string, displayName: string, contact?: string }} publisher
|
|
38
|
+
* @property {string} libraryRef
|
|
39
|
+
* @property {{ ac: { start: number, end: number }, suffixBlocks?: Array<{ kind: string, start: number, end: number }> }} bands
|
|
40
|
+
* @property {Array<{ slug: string, path: string }>} [blueprints]
|
|
41
|
+
* Snapshot of the library's `blueprints[]` at add-time. The resolver
|
|
42
|
+
* consults this to locate a blueprint by slug without re-reading
|
|
43
|
+
* `library.json` on every `blueprint add`. Absent snapshot falls back
|
|
44
|
+
* to the conventional `blueprints/<slug>/` layout (see shelf-resolver
|
|
45
|
+
* findBlueprintEntry).
|
|
46
|
+
* @property {string} addedAt RFC 3339
|
|
47
|
+
* @property {'operator'} reviewedBy
|
|
48
|
+
* @property {{ tier: 'local' | 'git' | 'tarball', shaVerifiedAt?: string, tarballSha256?: string }} provenance
|
|
49
|
+
* @property {string} cachePath
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @typedef {object} LibraryRegistry
|
|
54
|
+
* @property {number} registryVersion
|
|
55
|
+
* @property {RegistryEntry[]} libraries
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Read the registry from a project. Absent file is not an error: a
|
|
60
|
+
* project that has never added a library returns an empty registry.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} projectRoot - absolute path
|
|
63
|
+
* @returns {Promise<LibraryRegistry | import('../core/errors/index.js').RcfError>}
|
|
64
|
+
*/
|
|
65
|
+
export async function readLibraryRegistry(projectRoot) {
|
|
66
|
+
const path = join(projectRoot, REGISTRY_PATH);
|
|
67
|
+
if (!existsSync(path)) {
|
|
68
|
+
return { registryVersion: REGISTRY_VERSION, libraries: [] };
|
|
69
|
+
}
|
|
70
|
+
let raw;
|
|
71
|
+
try {
|
|
72
|
+
raw = await readFile(path, 'utf8');
|
|
73
|
+
} catch (err) {
|
|
74
|
+
return rcfError({ kind: 'ioFailure', message: `library registry: read failed: ${err.message}`, filePath: path });
|
|
75
|
+
}
|
|
76
|
+
let doc;
|
|
77
|
+
try {
|
|
78
|
+
doc = JSON.parse(raw);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
return rcfError({ kind: 'parseFailure', message: `library registry: JSON parse failed: ${err.message}`, filePath: path });
|
|
81
|
+
}
|
|
82
|
+
const err = validateRegistryShape(doc, path);
|
|
83
|
+
if (err) return err;
|
|
84
|
+
return doc;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Persist a registry to disk. Overwrites the whole file (registry
|
|
89
|
+
* writes are always full-file for legibility; the file is small).
|
|
90
|
+
*
|
|
91
|
+
* @param {string} projectRoot
|
|
92
|
+
* @param {LibraryRegistry} registry
|
|
93
|
+
* @param {object} [opts]
|
|
94
|
+
* @param {boolean} [opts.dryRun]
|
|
95
|
+
* @returns {Promise<{ written: boolean, path: string } | import('../core/errors/index.js').RcfError>}
|
|
96
|
+
*/
|
|
97
|
+
export async function writeLibraryRegistry(projectRoot, registry, opts = {}) {
|
|
98
|
+
const path = join(projectRoot, REGISTRY_PATH);
|
|
99
|
+
const err = validateRegistryShape(registry, path);
|
|
100
|
+
if (err) return err;
|
|
101
|
+
if (opts.dryRun === true) return { written: false, path };
|
|
102
|
+
try {
|
|
103
|
+
await mkdir(dirname(path), { recursive: true });
|
|
104
|
+
await writeFile(path, `${JSON.stringify(registry, null, 2)}\n`, 'utf8');
|
|
105
|
+
} catch (writeErr) {
|
|
106
|
+
return rcfError({ kind: 'ioFailure', message: `library registry: write failed: ${writeErr.message}`, filePath: path });
|
|
107
|
+
}
|
|
108
|
+
return { written: true, path };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @param {LibraryRegistry} registry
|
|
113
|
+
* @param {string} libraryPrefix
|
|
114
|
+
* @returns {RegistryEntry | undefined}
|
|
115
|
+
*/
|
|
116
|
+
export function findLibrary(registry, libraryPrefix) {
|
|
117
|
+
if (!registry || !Array.isArray(registry.libraries)) return undefined;
|
|
118
|
+
return registry.libraries.find((l) => l.libraryPrefix === libraryPrefix);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Load the core-shelf band reservations shipped alongside rcf-lite.
|
|
123
|
+
* Read-through with cache would be fine, but the file is tiny so we
|
|
124
|
+
* re-read on demand; keeps tests hermetic against a stale singleton.
|
|
125
|
+
*
|
|
126
|
+
* @param {string} [dataFilePath] - override for tests
|
|
127
|
+
* @returns {Promise<{ ac: Array<{ blueprint: string, start: number, end: number }>, suffixBlocks: Array<{ blueprint: string, kind: string, start: number, end: number }> }>}
|
|
128
|
+
*/
|
|
129
|
+
export async function loadCoreBandReservations(dataFilePath = CORE_BAND_RESERVATIONS_PATH) {
|
|
130
|
+
try {
|
|
131
|
+
const raw = await readFile(dataFilePath, 'utf8');
|
|
132
|
+
const doc = JSON.parse(raw);
|
|
133
|
+
return {
|
|
134
|
+
ac: Array.isArray(doc.ac) ? doc.ac : [],
|
|
135
|
+
suffixBlocks: Array.isArray(doc.suffixBlocks) ? doc.suffixBlocks : [],
|
|
136
|
+
};
|
|
137
|
+
} catch {
|
|
138
|
+
return { ac: [], suffixBlocks: [] };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Add-time band-overlap gate. Refuses when the candidate library's
|
|
144
|
+
* declared bands (`ac`, `suffixBlocks[]`) overlap either an
|
|
145
|
+
* already-registered library or a core-shelf reservation.
|
|
146
|
+
*
|
|
147
|
+
* @param {object} args
|
|
148
|
+
* @param {{ libraryPrefix: string, bands: { ac: { start: number, end: number }, suffixBlocks?: Array<{ kind: string, start: number, end: number }> } }} args.candidate
|
|
149
|
+
* @param {LibraryRegistry} args.registry
|
|
150
|
+
* @param {{ ac: Array<{ blueprint: string, start: number, end: number }>, suffixBlocks: Array<{ blueprint: string, kind: string, start: number, end: number }> }} args.coreReservations
|
|
151
|
+
* @returns {null | import('../core/errors/index.js').RcfError}
|
|
152
|
+
*/
|
|
153
|
+
export function detectBandOverlap({ candidate, registry, coreReservations }) {
|
|
154
|
+
// AC band: check against every registered library and every core row.
|
|
155
|
+
const cand = candidate.bands.ac;
|
|
156
|
+
for (const reg of registry.libraries) {
|
|
157
|
+
if (reg.libraryPrefix === candidate.libraryPrefix) continue;
|
|
158
|
+
if (rangesOverlap(cand, reg.bands.ac)) {
|
|
159
|
+
return rcfError({
|
|
160
|
+
kind: 'usage',
|
|
161
|
+
message: `AC band ${cand.start}-${cand.end} overlaps registered library '${reg.libraryPrefix}' (${reg.bands.ac.start}-${reg.bands.ac.end}).`,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
for (const core of coreReservations.ac) {
|
|
166
|
+
if (rangesOverlap(cand, core)) {
|
|
167
|
+
return rcfError({
|
|
168
|
+
kind: 'usage',
|
|
169
|
+
message: `AC band ${cand.start}-${cand.end} overlaps core-shelf blueprint '${core.blueprint}' (${core.start}-${core.end}).`,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Suffix blocks: per-kind overlap.
|
|
174
|
+
const suffixBlocks = candidate.bands.suffixBlocks ?? [];
|
|
175
|
+
for (const block of suffixBlocks) {
|
|
176
|
+
for (const reg of registry.libraries) {
|
|
177
|
+
if (reg.libraryPrefix === candidate.libraryPrefix) continue;
|
|
178
|
+
for (const other of reg.bands.suffixBlocks ?? []) {
|
|
179
|
+
if (other.kind === block.kind && rangesOverlap(block, other)) {
|
|
180
|
+
return rcfError({
|
|
181
|
+
kind: 'usage',
|
|
182
|
+
message: `suffix block ${block.kind} ${block.start}-${block.end} overlaps registered library '${reg.libraryPrefix}' (${other.start}-${other.end}).`,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
for (const core of coreReservations.suffixBlocks) {
|
|
188
|
+
if (core.kind === block.kind && rangesOverlap(block, core)) {
|
|
189
|
+
return rcfError({
|
|
190
|
+
kind: 'usage',
|
|
191
|
+
message: `suffix block ${block.kind} ${block.start}-${block.end} overlaps core-shelf blueprint '${core.blueprint}' (${core.start}-${core.end}).`,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Apply-time band gate (spec section 8.3 + open question 9.9 ratified
|
|
201
|
+
* as "add both"). Every contribution id whose numeric portion falls
|
|
202
|
+
* outside the library's declared band is refused, so a library that
|
|
203
|
+
* grew a blueprint outside its declared band (that its own CI should
|
|
204
|
+
* have caught but did not) refuses at the consuming project's `blueprint
|
|
205
|
+
* add`.
|
|
206
|
+
*
|
|
207
|
+
* @param {Array<{ id: string, kind: string }>} stampedContributions
|
|
208
|
+
* @param {{ ac: { start: number, end: number }, suffixBlocks?: Array<{ kind: string, start: number, end: number }> }} bands
|
|
209
|
+
* @returns {null | import('../core/errors/index.js').RcfError}
|
|
210
|
+
*/
|
|
211
|
+
export function detectContributionsOutOfBand(stampedContributions, bands) {
|
|
212
|
+
for (const c of stampedContributions) {
|
|
213
|
+
const parts = extractNumericPart(c.id);
|
|
214
|
+
if (!parts) continue;
|
|
215
|
+
if (c.kind === 'req' || c.kind === 'us' || c.kind === 'ts') {
|
|
216
|
+
if (!inRange(parts.number, bands.ac)) {
|
|
217
|
+
return rcfError({
|
|
218
|
+
kind: 'usage',
|
|
219
|
+
message: `contribution ${c.id} (${c.kind}) numeric ${parts.number} falls outside library AC band ${bands.ac.start}-${bands.ac.end}.`,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
} else if (c.kind === 'adr' || c.kind === 'tac') {
|
|
223
|
+
const blocks = (bands.suffixBlocks ?? []).filter((b) => b.kind === c.kind);
|
|
224
|
+
if (blocks.length === 0) continue; // no block declared for this kind
|
|
225
|
+
const hit = blocks.some((b) => inRange(parts.number, b));
|
|
226
|
+
if (!hit) {
|
|
227
|
+
const desc = blocks.map((b) => `${b.start}-${b.end}`).join(', ');
|
|
228
|
+
return rcfError({
|
|
229
|
+
kind: 'usage',
|
|
230
|
+
message: `contribution ${c.id} (${c.kind}) numeric ${parts.number} falls outside library ${c.kind} suffix block(s) ${desc}.`,
|
|
231
|
+
}); // library-facing prefix is added by the CLI edge
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Prefix-collision gate for `library add` (spec section 5.1). Refuses
|
|
240
|
+
* when the candidate `libraryPrefix` collides with a core-shelf
|
|
241
|
+
* blueprint slug or with any already-registered library's prefix.
|
|
242
|
+
*
|
|
243
|
+
* @param {object} args
|
|
244
|
+
* @param {string} args.libraryPrefix
|
|
245
|
+
* @param {LibraryRegistry} args.registry
|
|
246
|
+
* @param {string[]} args.coreSlugs
|
|
247
|
+
* @returns {null | import('../core/errors/index.js').RcfError}
|
|
248
|
+
*/
|
|
249
|
+
export function detectPrefixCollision({ libraryPrefix, registry, coreSlugs }) {
|
|
250
|
+
if (libraryPrefix.includes(':') || libraryPrefix.includes('/')) {
|
|
251
|
+
return rcfError({ kind: 'usage', message: `library add: libraryPrefix '${libraryPrefix}' must not contain ':' or '/'.` });
|
|
252
|
+
}
|
|
253
|
+
for (const slug of coreSlugs) {
|
|
254
|
+
if (slug === libraryPrefix) {
|
|
255
|
+
return rcfError({ kind: 'usage', message: `library add: libraryPrefix '${libraryPrefix}' collides with core-shelf blueprint slug '${slug}'.` });
|
|
256
|
+
}
|
|
257
|
+
if (slug.startsWith(`${libraryPrefix}-`)) {
|
|
258
|
+
return rcfError({
|
|
259
|
+
kind: 'usage',
|
|
260
|
+
message: `libraryPrefix '${libraryPrefix}' is a boundary-swallowing substring prefix of core-shelf blueprint slug '${slug}'; choose a prefix that does not read as the leading segment of an existing slug.`,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
for (const reg of registry.libraries) {
|
|
265
|
+
if (reg.libraryPrefix === libraryPrefix) {
|
|
266
|
+
return rcfError({ kind: 'usage', message: `library add: libraryPrefix '${libraryPrefix}' is already registered.` });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Extract the leading numeric block from an id like `wsd-auth-oauth2-REQ-1101`
|
|
274
|
+
* or `ADR-1300-deploy-cloudflare-workers`. Returns `{ number }` or null.
|
|
275
|
+
* We do NOT reimplement the id grammar here; the numeric portion of
|
|
276
|
+
* either family shape is unambiguous: the run of digits between two
|
|
277
|
+
* hyphens or between a hyphen and the string end.
|
|
278
|
+
*/
|
|
279
|
+
function extractNumericPart(id) {
|
|
280
|
+
const m = /-(\d+)(?:-|$)/.exec(id);
|
|
281
|
+
if (!m) return null;
|
|
282
|
+
return { number: Number(m[1]) };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function inRange(n, range) {
|
|
286
|
+
return n >= range.start && n <= range.end;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function rangesOverlap(a, b) {
|
|
290
|
+
return !(a.end < b.start || b.end < a.start);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function validateRegistryShape(doc, path) {
|
|
294
|
+
if (typeof doc !== 'object' || doc === null) {
|
|
295
|
+
return rcfError({ kind: 'validation', message: 'library registry: must be a JSON object', filePath: path });
|
|
296
|
+
}
|
|
297
|
+
if (!Number.isInteger(doc.registryVersion) || doc.registryVersion < 1) {
|
|
298
|
+
return rcfError({ kind: 'validation', message: `library registry: registryVersion must be a positive integer, got ${JSON.stringify(doc.registryVersion)}`, filePath: path });
|
|
299
|
+
}
|
|
300
|
+
if (doc.registryVersion > REGISTRY_VERSION) {
|
|
301
|
+
return rcfError({ kind: 'validation', message: `library registry: registryVersion ${doc.registryVersion} is newer than this CLI understands (max ${REGISTRY_VERSION}); upgrade rcf-lite.`, filePath: path });
|
|
302
|
+
}
|
|
303
|
+
if (!Array.isArray(doc.libraries)) {
|
|
304
|
+
return rcfError({ kind: 'validation', message: 'library registry: libraries[] is required (may be empty)', filePath: path });
|
|
305
|
+
}
|
|
306
|
+
const seen = new Set();
|
|
307
|
+
for (const [i, entry] of doc.libraries.entries()) {
|
|
308
|
+
if (typeof entry !== 'object' || entry === null) {
|
|
309
|
+
return rcfError({ kind: 'validation', message: `library registry: libraries[${i}] must be an object`, filePath: path });
|
|
310
|
+
}
|
|
311
|
+
if (typeof entry.libraryPrefix !== 'string') {
|
|
312
|
+
return rcfError({ kind: 'validation', message: `library registry: libraries[${i}].libraryPrefix is required`, filePath: path });
|
|
313
|
+
}
|
|
314
|
+
if (seen.has(entry.libraryPrefix)) {
|
|
315
|
+
return rcfError({ kind: 'validation', message: `library registry: libraries[${i}].libraryPrefix '${entry.libraryPrefix}' is declared more than once`, filePath: path });
|
|
316
|
+
}
|
|
317
|
+
seen.add(entry.libraryPrefix);
|
|
318
|
+
if (!['local', 'git', 'tarball'].includes(entry.sourceKind)) {
|
|
319
|
+
return rcfError({ kind: 'validation', message: `library registry: libraries[${i}].sourceKind '${entry.sourceKind}' must be one of local|git|tarball`, filePath: path });
|
|
320
|
+
}
|
|
321
|
+
if (typeof entry.sourceRef !== 'string' || entry.sourceRef.length === 0) {
|
|
322
|
+
return rcfError({ kind: 'validation', message: `library registry: libraries[${i}].sourceRef is required`, filePath: path });
|
|
323
|
+
}
|
|
324
|
+
if (typeof entry.cachePath !== 'string' || entry.cachePath.length === 0) {
|
|
325
|
+
return rcfError({ kind: 'validation', message: `library registry: libraries[${i}].cachePath is required`, filePath: path });
|
|
326
|
+
}
|
|
327
|
+
if (typeof entry.bands !== 'object' || entry.bands === null || typeof entry.bands.ac !== 'object' || entry.bands.ac === null) {
|
|
328
|
+
return rcfError({ kind: 'validation', message: `library registry: libraries[${i}].bands.ac is required`, filePath: path });
|
|
329
|
+
}
|
|
330
|
+
if (typeof entry.provenance !== 'object' || entry.provenance === null || !['local', 'git', 'tarball'].includes(entry.provenance.tier)) {
|
|
331
|
+
return rcfError({ kind: 'validation', message: `library registry: libraries[${i}].provenance.tier is required (local|git|tarball)`, filePath: path });
|
|
332
|
+
}
|
|
333
|
+
if (entry.provenance.tier !== 'local' && !entry.provenance.shaVerifiedAt) {
|
|
334
|
+
return rcfError({ kind: 'validation', message: `library registry: libraries[${i}].provenance.shaVerifiedAt is required when tier is '${entry.provenance.tier}'`, filePath: path });
|
|
335
|
+
}
|
|
336
|
+
if (entry.sourceKind === 'local' && entry.resolvedSha !== undefined) {
|
|
337
|
+
return rcfError({ kind: 'validation', message: `library registry: libraries[${i}] sourceKind=local must not carry a resolvedSha`, filePath: path });
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return null;
|
|
341
|
+
}
|
package/src/blueprint/list.js
CHANGED
|
@@ -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
|
|
49
|
-
if (
|
|
50
|
-
|
|
69
|
+
const sourceAbs = await resolveSourceForList(row.source, projectRoot);
|
|
70
|
+
if (sourceAbs !== null) {
|
|
71
|
+
const loaded = await loadBlueprint(sourceAbs);
|
|
72
|
+
if (!loaded.kind && typeof loaded.category === 'string') {
|
|
73
|
+
category = loaded.category;
|
|
74
|
+
}
|
|
51
75
|
}
|
|
52
76
|
}
|
|
53
77
|
out.push({ ...row, category });
|
|
@@ -55,6 +79,16 @@ export async function enrichRowsWithCategories(rows) {
|
|
|
55
79
|
return out;
|
|
56
80
|
}
|
|
57
81
|
|
|
82
|
+
async function resolveSourceForList(source, projectRoot) {
|
|
83
|
+
if (isAbsolute(source)) return source;
|
|
84
|
+
if (projectRoot === null) return null;
|
|
85
|
+
const resolved = await resolveBlueprintSource(source, { projectRoot }).catch(() => null);
|
|
86
|
+
if (resolved && !isRcfError(resolved) && typeof resolved.resolved === 'string') {
|
|
87
|
+
return resolved.resolved;
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
58
92
|
/**
|
|
59
93
|
* Group category-enriched rows by category. Preserves appliedAt order
|
|
60
94
|
* within each group. Categories are returned sorted alphabetically;
|
|
@@ -1,26 +1,38 @@
|
|
|
1
1
|
// Resolve a `rcf define blueprint add <source>` argument to an absolute
|
|
2
2
|
// blueprint directory.
|
|
3
3
|
//
|
|
4
|
-
// Resolution order (Phase 1
|
|
5
|
-
//
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
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
|
|
53
|
-
*
|
|
54
|
-
*
|
|
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
|
|
58
|
-
*
|
|
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
|
|
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;
|
|
76
|
-
//
|
|
77
|
-
//
|
|
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}'
|
|
88
|
-
+
|
|
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
|
|
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
|
|
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
|
-
//
|
|
107
|
-
// qualified library slug, nor a bare kebab slug. Treat it as a
|
|
108
|
-
// the loader emits the familiar "no blueprint.json found"
|
|
109
|
-
// the exact string the operator typed; that keeps the
|
|
110
|
-
// close to what they wrote rather than adding a
|
|
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)) {
|