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