rcf-lite 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -1
- package/bin/rcf.js +3 -1
- 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 +57 -0
- package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/github-actions/default-branch-checks.yml +55 -0
- package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/github-actions/pull-request-checks.yml +65 -0
- package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/github-actions/release.yml +71 -0
- package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/github-actions/scheduled-audit.yml +61 -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 +2 -1
- 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} +2 -2
- 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} +3 -3
- 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 +64 -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 +136 -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 +5 -2
- 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 +116 -0
- package/src/blueprint/apply.js +51 -13
- package/src/blueprint/index.js +12 -0
- package/src/blueprint/library-loader.js +271 -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/cli/blueprint-library.js +419 -0
- package/src/cli/blueprint.js +46 -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/view/live-client.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// Live-view client. Loaded by the server-rendered page as
|
|
2
2
|
// `<script src="/live-client.js" defer>`. Handles the SSE `/events`
|
|
3
|
-
// stream, in-place `innerHTML` swaps into `#rcf-live-content`,
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// stream, in-place `innerHTML` swaps into `#rcf-live-content`, persists
|
|
4
|
+
// `<details>` open-set + main scrollTop to `localStorage` across every
|
|
5
|
+
// swap and every full reload, AND applies the URL-driven scope filter
|
|
6
|
+
// (deep-link deep-links: blueprint / node — w-2026-08-30-dave-020) on
|
|
7
|
+
// every load and every swap.
|
|
6
8
|
//
|
|
7
9
|
// Vanilla classic script (no ESM syntax) so the base HTML script tag
|
|
8
10
|
// stays plain. The IIFE exposes its internals as `window.__rcfLiveClient`
|
|
@@ -10,7 +12,9 @@
|
|
|
10
12
|
// jsdom.
|
|
11
13
|
//
|
|
12
14
|
// Spec: projects/rcf-build-lite/specs/phase-3.8-live-view.md D12, D13a,
|
|
13
|
-
// D13b, D14.
|
|
15
|
+
// D13b, D14. Deep-link overlay: see `src/view/scope.js`
|
|
16
|
+
// (server-side truth) and `test/view/scope-client.test.js`
|
|
17
|
+
// (browser-side proofs).
|
|
14
18
|
|
|
15
19
|
(function () {
|
|
16
20
|
var STORAGE_NS = 'rcf-view:v1:';
|
|
@@ -18,6 +22,232 @@
|
|
|
18
22
|
var STORAGE_SCROLL = STORAGE_NS + 'scrollTop';
|
|
19
23
|
var HEARTBEAT_STALE_MS = 45000;
|
|
20
24
|
|
|
25
|
+
// ---- deep-link scope --------------------------------------------------
|
|
26
|
+
|
|
27
|
+
// Scope model: { blueprint: string|null, node: string|null }. Read on
|
|
28
|
+
// load and after every SSE swap; the DOM class hooks below drive the
|
|
29
|
+
// stylesheet's opacity / focus rules.
|
|
30
|
+
|
|
31
|
+
function parseUrlScope(search) {
|
|
32
|
+
var out = { blueprint: null, node: null };
|
|
33
|
+
if (typeof search !== 'string' || search.length === 0) return out;
|
|
34
|
+
var raw = search.charAt(0) === '?' ? search.slice(1) : search;
|
|
35
|
+
if (raw.length === 0) return out;
|
|
36
|
+
var pairs = raw.split('&');
|
|
37
|
+
for (var i = 0; i < pairs.length; i += 1) {
|
|
38
|
+
var eq = pairs[i].indexOf('=');
|
|
39
|
+
if (eq === -1) continue;
|
|
40
|
+
var key = decodeSafely(pairs[i].slice(0, eq));
|
|
41
|
+
var val = decodeSafely(pairs[i].slice(eq + 1));
|
|
42
|
+
if (typeof val !== 'string') continue;
|
|
43
|
+
var trimmed = val.replace(/^\s+|\s+$/g, '');
|
|
44
|
+
if (trimmed.length === 0) continue;
|
|
45
|
+
if (key === 'blueprint' && out.blueprint === null) out.blueprint = trimmed;
|
|
46
|
+
else if (key === 'node' && out.node === null) out.node = trimmed;
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function decodeSafely(s) {
|
|
52
|
+
try { return decodeURIComponent(String(s).replace(/\+/g, ' ')); }
|
|
53
|
+
catch (e) { return null; }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function markScopeIds(doc, contributionIds, customisedIds) {
|
|
57
|
+
if (!doc) return { inScopeCount: 0, customisedCount: 0 };
|
|
58
|
+
clearScopeMarkers(doc);
|
|
59
|
+
var inSet = toIdSet(contributionIds);
|
|
60
|
+
var customSet = toIdSet(customisedIds);
|
|
61
|
+
var inScopeCount = 0;
|
|
62
|
+
var customisedCount = 0;
|
|
63
|
+
// A doc container carries data-doc-id; we walk them and mark the
|
|
64
|
+
// matching ones. Ancestors of an in-scope doc are also marked so
|
|
65
|
+
// the tree drill-down (details wrappers, tabpanels) stays visible.
|
|
66
|
+
var nodes = doc.querySelectorAll('[data-doc-id]');
|
|
67
|
+
for (var i = 0; i < nodes.length; i += 1) {
|
|
68
|
+
var el = nodes[i];
|
|
69
|
+
var id = el.getAttribute('data-doc-id');
|
|
70
|
+
if (!id) continue;
|
|
71
|
+
// The raw-json disclosures carry ids like `PRD-001::raw`;
|
|
72
|
+
// strip the ::raw suffix so the scope hit matches the parent
|
|
73
|
+
// doc's contribution id.
|
|
74
|
+
var canonical = id.replace(/::raw$/, '');
|
|
75
|
+
if (!inSet[canonical]) continue;
|
|
76
|
+
el.classList.add('rcf-in-scope');
|
|
77
|
+
inScopeCount += 1;
|
|
78
|
+
if (customSet[canonical]) {
|
|
79
|
+
el.classList.add('rcf-customised');
|
|
80
|
+
customisedCount += 1;
|
|
81
|
+
}
|
|
82
|
+
// Walk ancestors and mark them as containing an in-scope doc
|
|
83
|
+
// so the CSS can keep them at full opacity.
|
|
84
|
+
var cur = el.parentNode;
|
|
85
|
+
while (cur && cur !== doc.body) {
|
|
86
|
+
if (cur.classList && !cur.classList.contains('rcf-scope-ancestor')) {
|
|
87
|
+
cur.classList.add('rcf-scope-ancestor');
|
|
88
|
+
}
|
|
89
|
+
cur = cur.parentNode;
|
|
90
|
+
}
|
|
91
|
+
// Auto-open the ancestor <details> so the drill-down surfaces
|
|
92
|
+
// the contribution without a manual click.
|
|
93
|
+
openAncestorDetailsChain(el);
|
|
94
|
+
}
|
|
95
|
+
return { inScopeCount: inScopeCount, customisedCount: customisedCount };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function toIdSet(list) {
|
|
99
|
+
var out = Object.create(null);
|
|
100
|
+
if (!Array.isArray(list)) return out;
|
|
101
|
+
for (var i = 0; i < list.length; i += 1) {
|
|
102
|
+
if (typeof list[i] === 'string') out[list[i]] = true;
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function openAncestorDetailsChain(node) {
|
|
108
|
+
var cur = node.parentNode;
|
|
109
|
+
while (cur && cur.tagName) {
|
|
110
|
+
if (cur.tagName.toLowerCase() === 'details') {
|
|
111
|
+
cur.open = true;
|
|
112
|
+
}
|
|
113
|
+
cur = cur.parentNode;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function clearScopeMarkers(doc) {
|
|
118
|
+
var scoped = doc.querySelectorAll('.rcf-in-scope, .rcf-customised, .rcf-scope-ancestor');
|
|
119
|
+
for (var i = 0; i < scoped.length; i += 1) {
|
|
120
|
+
scoped[i].classList.remove('rcf-in-scope');
|
|
121
|
+
scoped[i].classList.remove('rcf-customised');
|
|
122
|
+
scoped[i].classList.remove('rcf-scope-ancestor');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function renderScopeBanner(doc, meta) {
|
|
127
|
+
// Idempotent: reuse the same banner across swaps rather than
|
|
128
|
+
// stacking one banner per SSE tick.
|
|
129
|
+
if (!doc || !doc.body) return null;
|
|
130
|
+
var main = doc.querySelector('main');
|
|
131
|
+
if (!main) return null;
|
|
132
|
+
var banner = doc.getElementById('rcf-scope-banner');
|
|
133
|
+
if (!banner) {
|
|
134
|
+
banner = doc.createElement('aside');
|
|
135
|
+
banner.id = 'rcf-scope-banner';
|
|
136
|
+
banner.setAttribute('role', 'status');
|
|
137
|
+
main.insertBefore(banner, main.firstChild);
|
|
138
|
+
}
|
|
139
|
+
var lines = [];
|
|
140
|
+
if (meta.blueprint) {
|
|
141
|
+
var counted = 'scoped to blueprint ' + escapeText(meta.blueprint)
|
|
142
|
+
+ ' (' + meta.inScopeCount + ' contribution' + (meta.inScopeCount === 1 ? '' : 's');
|
|
143
|
+
if (typeof meta.customisedCount === 'number' && meta.customisedCount > 0) {
|
|
144
|
+
counted += ', ' + meta.customisedCount + ' customised';
|
|
145
|
+
}
|
|
146
|
+
counted += ')';
|
|
147
|
+
lines.push(counted);
|
|
148
|
+
}
|
|
149
|
+
if (meta.node) {
|
|
150
|
+
lines.push('focus: ' + escapeText(meta.node));
|
|
151
|
+
}
|
|
152
|
+
if (typeof meta.warning === 'string' && meta.warning.length > 0) {
|
|
153
|
+
lines.push(meta.warning);
|
|
154
|
+
}
|
|
155
|
+
lines.push('<a href="?">clear scope</a>');
|
|
156
|
+
banner.innerHTML = lines.join(' · ');
|
|
157
|
+
return banner;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function removeScopeBanner(doc) {
|
|
161
|
+
if (!doc) return;
|
|
162
|
+
var banner = doc.getElementById('rcf-scope-banner');
|
|
163
|
+
if (banner && banner.parentNode) banner.parentNode.removeChild(banner);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function escapeText(s) {
|
|
167
|
+
return String(s).replace(/[&<>"']/g, function (ch) {
|
|
168
|
+
return ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[ch];
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function focusNode(doc, id) {
|
|
173
|
+
if (!doc || !id) return false;
|
|
174
|
+
var target = doc.getElementById(id) || doc.querySelector('[data-doc-id="' + id.replace(/"/g, '\\"') + '"]');
|
|
175
|
+
if (!target) return false;
|
|
176
|
+
openAncestorDetailsChain(target);
|
|
177
|
+
if (target.tagName && target.tagName.toLowerCase() === 'details') target.open = true;
|
|
178
|
+
try { target.scrollIntoView({ block: 'start' }); }
|
|
179
|
+
catch (e) { try { target.scrollIntoView(); } catch (_e) { /* soft */ } }
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Fetch the scope payload from /scope.json for a given slug. Kept as
|
|
184
|
+
// a small helper so the tests can substitute a fake fetch impl.
|
|
185
|
+
function fetchScope(slug, fetchImpl) {
|
|
186
|
+
var f = typeof fetchImpl === 'function' ? fetchImpl : (typeof fetch === 'function' ? fetch : null);
|
|
187
|
+
if (!f) return Promise.reject(new Error('no fetch available'));
|
|
188
|
+
var url = '/scope.json?blueprint=' + encodeURIComponent(slug);
|
|
189
|
+
return Promise.resolve(f(url)).then(function (resp) {
|
|
190
|
+
if (!resp) throw new Error('scope: no response');
|
|
191
|
+
var okFlag = resp.ok !== undefined ? resp.ok : (resp.status >= 200 && resp.status < 300);
|
|
192
|
+
return Promise.resolve(resp.json ? resp.json() : null).then(function (body) {
|
|
193
|
+
return { ok: !!okFlag, status: resp.status, body: body };
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Apply the URL scope to the currently-rendered DOM. Returns a
|
|
199
|
+
// Promise that resolves with a summary object; never rejects (any
|
|
200
|
+
// failure is downgraded to a banner warning).
|
|
201
|
+
function applyUrlScope(deps) {
|
|
202
|
+
var doc = deps.document;
|
|
203
|
+
var win = deps.window;
|
|
204
|
+
var scope = parseUrlScope((win && win.location && win.location.search) || '');
|
|
205
|
+
if (!scope.blueprint && !scope.node) {
|
|
206
|
+
if (doc && doc.body) doc.body.classList.remove('rcf-scoped');
|
|
207
|
+
clearScopeMarkers(doc);
|
|
208
|
+
removeScopeBanner(doc);
|
|
209
|
+
return Promise.resolve({ scope: scope, applied: false });
|
|
210
|
+
}
|
|
211
|
+
if (doc && doc.body) doc.body.classList.add('rcf-scoped');
|
|
212
|
+
|
|
213
|
+
var meta = {
|
|
214
|
+
blueprint: scope.blueprint,
|
|
215
|
+
node: scope.node,
|
|
216
|
+
inScopeCount: 0,
|
|
217
|
+
customisedCount: 0,
|
|
218
|
+
warning: '',
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
var blueprintPart = scope.blueprint
|
|
222
|
+
? fetchScope(scope.blueprint, deps.fetch).then(function (r) {
|
|
223
|
+
if (!r.ok) {
|
|
224
|
+
meta.warning = r.status === 404
|
|
225
|
+
? 'blueprint ' + scope.blueprint + ' is not applied to this project'
|
|
226
|
+
: 'scope lookup failed (status ' + r.status + ')';
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
var body = r.body || {};
|
|
230
|
+
var marks = markScopeIds(doc, body.contributionIds || [], body.customisedIds || []);
|
|
231
|
+
meta.inScopeCount = marks.inScopeCount;
|
|
232
|
+
meta.customisedCount = marks.customisedCount;
|
|
233
|
+
}).catch(function (err) {
|
|
234
|
+
meta.warning = 'scope lookup error: ' + (err && err.message ? err.message : 'unknown');
|
|
235
|
+
})
|
|
236
|
+
: Promise.resolve();
|
|
237
|
+
|
|
238
|
+
return blueprintPart.then(function () {
|
|
239
|
+
renderScopeBanner(doc, meta);
|
|
240
|
+
if (scope.node) {
|
|
241
|
+
var focused = focusNode(doc, scope.node);
|
|
242
|
+
if (!focused && !meta.warning) {
|
|
243
|
+
meta.warning = 'node ' + scope.node + ' is not present on the tree';
|
|
244
|
+
renderScopeBanner(doc, meta);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return { scope: scope, applied: true, meta: meta };
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
21
251
|
// ---- state persistence ------------------------------------------------
|
|
22
252
|
|
|
23
253
|
function safeGetItem(storage, key) {
|
|
@@ -142,12 +372,22 @@
|
|
|
142
372
|
// whitelisted deltas from Phase 3.6).
|
|
143
373
|
injectConnectionDot(doc);
|
|
144
374
|
|
|
145
|
-
// Restore prior state on initial load
|
|
375
|
+
// Restore prior state on initial load, then apply URL-driven scope
|
|
376
|
+
// so a deep-link (?blueprint=…&node=…) surfaces the target section
|
|
377
|
+
// even on the very first render.
|
|
378
|
+
var applyScope = function () { applyUrlScope(deps).catch(function () { /* soft */ }); };
|
|
146
379
|
if (doc.readyState === 'loading') {
|
|
147
|
-
doc.addEventListener('DOMContentLoaded', function () {
|
|
380
|
+
doc.addEventListener('DOMContentLoaded', function () {
|
|
381
|
+
restoreState(deps);
|
|
382
|
+
applyScope();
|
|
383
|
+
});
|
|
148
384
|
} else {
|
|
149
385
|
restoreState(deps);
|
|
386
|
+
applyScope();
|
|
150
387
|
}
|
|
388
|
+
// Scope tracks URL changes without a reload (agent pushes a new
|
|
389
|
+
// deep-link into the same tab, or the user clicks "clear scope").
|
|
390
|
+
win.addEventListener('popstate', applyScope);
|
|
151
391
|
|
|
152
392
|
var currentVersion = null;
|
|
153
393
|
var lastEventAt = Date.now();
|
|
@@ -203,6 +443,7 @@
|
|
|
203
443
|
// the payload have no `data-processed` yet.
|
|
204
444
|
resyncTabsAfterSwap(doc, win);
|
|
205
445
|
restoreState(deps);
|
|
446
|
+
applyScope();
|
|
206
447
|
setState('connected');
|
|
207
448
|
});
|
|
208
449
|
|
|
@@ -319,6 +560,12 @@
|
|
|
319
560
|
injectConnectionDot: injectConnectionDot,
|
|
320
561
|
updateDot: updateDot,
|
|
321
562
|
resyncTabsAfterSwap: resyncTabsAfterSwap,
|
|
563
|
+
parseUrlScope: parseUrlScope,
|
|
564
|
+
applyUrlScope: applyUrlScope,
|
|
565
|
+
markScopeIds: markScopeIds,
|
|
566
|
+
renderScopeBanner: renderScopeBanner,
|
|
567
|
+
removeScopeBanner: removeScopeBanner,
|
|
568
|
+
focusNode: focusNode,
|
|
322
569
|
STORAGE_OPEN: STORAGE_OPEN,
|
|
323
570
|
STORAGE_SCROLL: STORAGE_SCROLL,
|
|
324
571
|
HEARTBEAT_STALE_MS: HEARTBEAT_STALE_MS,
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// Deep-link scope resolution. Pure helpers shared by the server-side
|
|
2
|
+
// /scope.json endpoint, the CLI `audit view open` verb, and the
|
|
3
|
+
// live-client filter. Nothing here reads process.env, prints, or
|
|
4
|
+
// mutates state.
|
|
5
|
+
//
|
|
6
|
+
// Scope model (spec: w-2026-08-30-dave-020, Baz-ratified 2026-08-30):
|
|
7
|
+
// * blueprint scope -> the applied blueprint's namespaced
|
|
8
|
+
// contribution ids, read from manifest.blueprints[].contributions[].
|
|
9
|
+
// The applied record IS the truth (project-local), never the
|
|
10
|
+
// shipping blueprint source (which may drift or vanish).
|
|
11
|
+
// * node scope -> a single document id (any kind in tree.byId),
|
|
12
|
+
// resolved by the existing hash-router in the client script. Server
|
|
13
|
+
// validates existence so a bad id refuses at the CLI seam rather
|
|
14
|
+
// than opening a dead browser tab.
|
|
15
|
+
// * customisation set -> the subset of contribution ids whose current
|
|
16
|
+
// on-disk bytes differ from the shipping blueprint source's file.
|
|
17
|
+
// Best-effort: any read failure (source missing, unreadable) treats
|
|
18
|
+
// the file as "unknown" (excluded), which the renderer displays as
|
|
19
|
+
// plain in-scope rather than as customised.
|
|
20
|
+
|
|
21
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
22
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
23
|
+
|
|
24
|
+
import { loadBlueprint } from '../blueprint/loader.js';
|
|
25
|
+
import { resolveBlueprintSource } from '../blueprint/shelf-resolver.js';
|
|
26
|
+
import { isRcfError } from '../core/errors/index.js';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Parse the recognised scope keys from a URL query string.
|
|
30
|
+
*
|
|
31
|
+
* @param {URLSearchParams} params
|
|
32
|
+
* @returns {{ blueprint: string|null, node: string|null }}
|
|
33
|
+
*/
|
|
34
|
+
export function parseUrlScope(params) {
|
|
35
|
+
const blueprint = normaliseString(params.get('blueprint'));
|
|
36
|
+
const node = normaliseString(params.get('node'));
|
|
37
|
+
return { blueprint, node };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normaliseString(v) {
|
|
41
|
+
if (typeof v !== 'string') return null;
|
|
42
|
+
const trimmed = v.trim();
|
|
43
|
+
return trimmed.length === 0 ? null : trimmed;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Resolve the applied blueprint record for a slug.
|
|
48
|
+
*
|
|
49
|
+
* @param {object|null} manifest
|
|
50
|
+
* @param {string} slug
|
|
51
|
+
* @returns {{ found: false } | { found: true, record: object, contributionIds: string[] }}
|
|
52
|
+
*/
|
|
53
|
+
export function contributionsForBlueprint(manifest, slug) {
|
|
54
|
+
if (!manifest || !Array.isArray(manifest.blueprints)) return { found: false };
|
|
55
|
+
const record = manifest.blueprints.find((b) => b?.slug === slug);
|
|
56
|
+
if (!record) return { found: false };
|
|
57
|
+
const contributionIds = Array.isArray(record.contributions)
|
|
58
|
+
? record.contributions.map((c) => c?.id).filter((id) => typeof id === 'string')
|
|
59
|
+
: [];
|
|
60
|
+
return { found: true, record, contributionIds };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* List every applied blueprint slug present in the manifest. Used by
|
|
65
|
+
* the client to preflight scope requests without a round trip per view.
|
|
66
|
+
*
|
|
67
|
+
* @param {object|null} manifest
|
|
68
|
+
* @returns {string[]}
|
|
69
|
+
*/
|
|
70
|
+
export function listAppliedSlugs(manifest) {
|
|
71
|
+
if (!manifest || !Array.isArray(manifest.blueprints)) return [];
|
|
72
|
+
return manifest.blueprints
|
|
73
|
+
.map((b) => (typeof b?.slug === 'string' ? b.slug : null))
|
|
74
|
+
.filter((s) => s !== null);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Detect which of the applied blueprint's contribution files have been
|
|
79
|
+
* modified on disk since apply. Compares current file bytes to the
|
|
80
|
+
* shipping source file bytes. Source paths that no longer resolve are
|
|
81
|
+
* treated as "unknown" (excluded from the result), so a deleted
|
|
82
|
+
* shipping tree does not fake a full-tree customisation banner.
|
|
83
|
+
*
|
|
84
|
+
* @param {object} args
|
|
85
|
+
* @param {string} args.projectRoot - absolute path to the project root
|
|
86
|
+
* @param {object} args.record - applied blueprint record
|
|
87
|
+
* @param {(abs: string) => Promise<string>} [args._readFile] - test seam
|
|
88
|
+
* @returns {Promise<{ customisedIds: string[], missingSourceIds: string[] }>}
|
|
89
|
+
*/
|
|
90
|
+
export async function detectCustomisations({ projectRoot, record, _readFile, _loadBlueprint, _resolveSource }) {
|
|
91
|
+
const readImpl = _readFile ?? ((abs) => readFile(abs, 'utf8'));
|
|
92
|
+
const loadImpl = _loadBlueprint ?? loadBlueprint;
|
|
93
|
+
const resolveImpl = _resolveSource ?? resolveBlueprintSource;
|
|
94
|
+
const customisedIds = [];
|
|
95
|
+
const missingSourceIds = [];
|
|
96
|
+
if (!record || !Array.isArray(record.contributions)) {
|
|
97
|
+
return { customisedIds, missingSourceIds };
|
|
98
|
+
}
|
|
99
|
+
const sourceAbs = await resolveSourceAbs(projectRoot, record.source, resolveImpl);
|
|
100
|
+
const sourceIndex = await loadSourceContributionIndex({ sourceAbs, loadImpl });
|
|
101
|
+
for (const c of record.contributions) {
|
|
102
|
+
if (typeof c?.id !== 'string' || typeof c?.path !== 'string') continue;
|
|
103
|
+
const projectAbs = resolve(projectRoot, c.path);
|
|
104
|
+
let currentBytes = null;
|
|
105
|
+
try {
|
|
106
|
+
currentBytes = await readImpl(projectAbs);
|
|
107
|
+
} catch {
|
|
108
|
+
// No project-side file: not a customisation (nothing on disk).
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (!sourceIndex) {
|
|
112
|
+
missingSourceIds.push(c.id);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
// Cross-reference by CONTRIBUTION INDEX (position). The applied
|
|
116
|
+
// record and the shipping blueprint list contributions in the same
|
|
117
|
+
// order at apply time; the applied record's ids are namespaced
|
|
118
|
+
// (stampId) but the position map is stable and does not rely on
|
|
119
|
+
// any string derivation.
|
|
120
|
+
const sourcePathRelative = sourceIndex.byAppliedId(c.id);
|
|
121
|
+
if (!sourcePathRelative) {
|
|
122
|
+
missingSourceIds.push(c.id);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const sourceFileAbs = join(sourceAbs, 'contributions', sourcePathRelative);
|
|
126
|
+
let sourceBytes = null;
|
|
127
|
+
try {
|
|
128
|
+
sourceBytes = await readImpl(sourceFileAbs);
|
|
129
|
+
} catch {
|
|
130
|
+
missingSourceIds.push(c.id);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (currentBytes !== sourceBytes) customisedIds.push(c.id);
|
|
134
|
+
}
|
|
135
|
+
return { customisedIds, missingSourceIds };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function resolveSourceAbs(projectRoot, source, resolveImpl) {
|
|
139
|
+
if (typeof source !== 'string' || source.length === 0) return null;
|
|
140
|
+
if (isAbsolute(source)) return source;
|
|
141
|
+
// Non-path sources - a library colon ref (`wsd:auth-oauth2`), an
|
|
142
|
+
// `@stock/<slug>` sugar, or a bare shelf slug - need re-resolution
|
|
143
|
+
// through the blueprint resolver against the project registry and the
|
|
144
|
+
// packaged shelf. Without this, the applied-record's source is a
|
|
145
|
+
// token the FS cannot open and every contribution renders as
|
|
146
|
+
// missingSource in /scope.json (integration review d-2026-08-31-046).
|
|
147
|
+
// A relative filesystem path still falls through to the pre-fix
|
|
148
|
+
// resolve() below so an existing manifest carrying `./blueprint-foo`
|
|
149
|
+
// keeps its behaviour.
|
|
150
|
+
if (typeof projectRoot === 'string' && projectRoot.length > 0) {
|
|
151
|
+
const resolved = await resolveImpl(source, { projectRoot }).catch(() => null);
|
|
152
|
+
if (resolved && !isRcfError(resolved) && typeof resolved.resolved === 'string') {
|
|
153
|
+
return resolved.resolved;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Fallback: treat as a relative filesystem path against projectRoot.
|
|
157
|
+
// Matches pre-fix behaviour byte-for-byte; a missing directory
|
|
158
|
+
// surfaces as `missingSource` via the loader below rather than here.
|
|
159
|
+
return typeof projectRoot === 'string' && projectRoot.length > 0
|
|
160
|
+
? resolve(projectRoot, source)
|
|
161
|
+
: null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Load the shipping blueprint's contribution list and build an
|
|
166
|
+
* applied-id -> source-relative-path index. Falls back to id-based
|
|
167
|
+
* lookup when positions don't align (a re-apply that added ids will
|
|
168
|
+
* still resolve the shared prefix).
|
|
169
|
+
*/
|
|
170
|
+
async function loadSourceContributionIndex({ sourceAbs, loadImpl }) {
|
|
171
|
+
if (!sourceAbs) return null;
|
|
172
|
+
const loaded = await loadImpl(sourceAbs).catch(() => null);
|
|
173
|
+
if (!loaded || loaded.kind) return null;
|
|
174
|
+
const contributions = Array.isArray(loaded.contributions) ? loaded.contributions : [];
|
|
175
|
+
// Two indexes: exact id match (works whenever the operator used the
|
|
176
|
+
// default namespace, i.e. shipping id === applied id) and bare-id
|
|
177
|
+
// suffix match (works when the applied id carries a namespace prefix
|
|
178
|
+
// over a bare shipping id, e.g. `myproj-application-spa-REQ-001`
|
|
179
|
+
// over shipping `application-spa-REQ-001`).
|
|
180
|
+
const exact = new Map();
|
|
181
|
+
for (const sc of contributions) {
|
|
182
|
+
if (typeof sc?.id !== 'string' || typeof sc?.path !== 'string') continue;
|
|
183
|
+
exact.set(sc.id, sc.path);
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
byAppliedId(appliedId) {
|
|
187
|
+
if (exact.has(appliedId)) return exact.get(appliedId);
|
|
188
|
+
// Namespace fall-back: strip the leading namespace segment and
|
|
189
|
+
// retry. The applied stampId only prefixes; it never rewrites
|
|
190
|
+
// the trailing bare id.
|
|
191
|
+
const idx = appliedId.indexOf('-');
|
|
192
|
+
if (idx > 0) {
|
|
193
|
+
const tail = appliedId.slice(idx + 1);
|
|
194
|
+
if (exact.has(tail)) return exact.get(tail);
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Best-effort node-id existence check against a tree model. Kept here
|
|
203
|
+
* (not in tree-model.js) so the CLI can validate a --node argument
|
|
204
|
+
* without pulling the full renderer pipeline in.
|
|
205
|
+
*
|
|
206
|
+
* @param {import('#core/store/walker.js').Tree} tree
|
|
207
|
+
* @param {string} id
|
|
208
|
+
* @returns {boolean}
|
|
209
|
+
*/
|
|
210
|
+
export function treeHasId(tree, id) {
|
|
211
|
+
if (!tree || typeof id !== 'string' || id.length === 0) return false;
|
|
212
|
+
if (tree.byId && typeof tree.byId.get === 'function') return tree.byId.has(id);
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Fast async-friendly stat wrapper. Handy for tests that want to prove
|
|
218
|
+
* the customisation check honours missing files without stubbing the
|
|
219
|
+
* whole fs module.
|
|
220
|
+
*
|
|
221
|
+
* @param {string} abs
|
|
222
|
+
* @returns {Promise<boolean>}
|
|
223
|
+
*/
|
|
224
|
+
export async function fileExists(abs) {
|
|
225
|
+
try {
|
|
226
|
+
const s = await stat(abs);
|
|
227
|
+
return s.isFile();
|
|
228
|
+
} catch {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
}
|
package/src/view/style.css
CHANGED
|
@@ -733,3 +733,45 @@ footer {
|
|
|
733
733
|
details.doc-details > .req-body,
|
|
734
734
|
details.doc-details > .us-body { padding: 0.75rem 1rem; }
|
|
735
735
|
}
|
|
736
|
+
|
|
737
|
+
/* --------------------------------------------------------------------
|
|
738
|
+
* Deep-link scope overlay (w-2026-08-30-dave-020)
|
|
739
|
+
*
|
|
740
|
+
* When body.rcf-scoped is present the client has applied a URL-driven
|
|
741
|
+
* scope (blueprint / node). Everything inside #rcf-live-content is
|
|
742
|
+
* dimmed except:
|
|
743
|
+
* - .rcf-in-scope a doc that IS in the scope set
|
|
744
|
+
* - .rcf-scope-ancestor an ancestor of an in-scope doc (kept full
|
|
745
|
+
* strength so the drill-down chain is legible)
|
|
746
|
+
* - .rcf-customised an in-scope doc whose bytes differ from
|
|
747
|
+
* the shipping blueprint source (a project
|
|
748
|
+
* customisation over the applied contribution)
|
|
749
|
+
*
|
|
750
|
+
* Left visible-but-dimmed rather than hidden so the reader can see
|
|
751
|
+
* where in the tree the scoped set sits.
|
|
752
|
+
* -------------------------------------------------------------------- */
|
|
753
|
+
|
|
754
|
+
body.rcf-scoped #rcf-live-content [data-doc-id]:not(.rcf-in-scope):not(.rcf-scope-ancestor) {
|
|
755
|
+
opacity: 0.28;
|
|
756
|
+
transition: opacity 120ms ease-in-out;
|
|
757
|
+
}
|
|
758
|
+
body.rcf-scoped #rcf-live-content [data-doc-id].rcf-in-scope {
|
|
759
|
+
opacity: 1;
|
|
760
|
+
outline: 2px solid #4c9a2a;
|
|
761
|
+
outline-offset: 2px;
|
|
762
|
+
}
|
|
763
|
+
body.rcf-scoped #rcf-live-content [data-doc-id].rcf-in-scope.rcf-customised {
|
|
764
|
+
outline-color: #d68f00;
|
|
765
|
+
box-shadow: 0 0 0 3px rgba(214, 143, 0, 0.18);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
#rcf-scope-banner {
|
|
769
|
+
margin: 0 0 1rem;
|
|
770
|
+
padding: 0.5rem 0.8rem;
|
|
771
|
+
border: 1px solid #d68f00;
|
|
772
|
+
background: #fff7e6;
|
|
773
|
+
color: #6b4300;
|
|
774
|
+
font-size: 0.9rem;
|
|
775
|
+
border-radius: 4px;
|
|
776
|
+
}
|
|
777
|
+
#rcf-scope-banner a { color: #b03a2e; text-decoration: underline; }
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
# Ci-pipeline blueprint (v1.0.0)
|
|
2
|
-
|
|
3
|
-
The fifth content blueprint on the rcf-build-lite blueprint mechanism (design brief v2, ratified). Scope: a CI-provider-neutral gate suite that runs `rcf define validate` and `rcf audit coverage --strict` as required gates on every push to the default branch and every pull request into it, writes machine-readable JSON reports at stable paths, and refuses to declare a pipeline pass on any missing or failed required gate. The suite is a single Node entry-point script the CI job invokes with one line; one illustrative GitHub Actions workflow ships as a starting point and alternate providers wire the same script.
|
|
4
|
-
|
|
5
|
-
## Apply
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
rcf define blueprint add <path-to>/blueprints/ci-pipeline
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
Phase 1 resolves local path sources only; registry and git-ref resolution is a mechanism follow-up. Apply is idempotent; `rcf define blueprint list` shows the applied entry; `rcf define blueprint remove ci-pipeline` cleanly removes an unreferenced application.
|
|
12
|
-
|
|
13
|
-
## Anatomy
|
|
14
|
-
|
|
15
|
-
| Piece | Where | What |
|
|
16
|
-
|---|---|---|
|
|
17
|
-
| Metadata | `blueprint.json` | Slug, version, and the 27 contributions with scope/topic on the two global ADRs |
|
|
18
|
-
| Doc set | `contributions/` | 10 REQs, 10 USs (20 ACs), 3 TACs, 4 ADRs, all schema-valid against rcf-schemas 0.4.5 and namespaced (`ci-pipeline-REQ-001` prefix family; `ADR-701-ci-pipeline-ci-gates` suffix family) |
|
|
19
|
-
| GitHub Actions example | `assets/ci-provider-examples/github-actions.yml` | One illustrative workflow: push and pull_request triggers on the default branch, Node and pnpm setup, single-line entry-point invocation, artefact upload of the report directory |
|
|
20
|
-
| Alternate-provider notes | `assets/ci-provider-examples/notes.md` | The four-point mapping (job trigger event, Node setup, single-line entry-point invocation, artefact upload) for GitLab CI, CircleCI, Buildkite, and Jenkins |
|
|
21
|
-
| Report-shape samples | `assets/report-samples/per-gate.json`, `assets/report-samples/pipeline.json` | Worked examples of a per-gate report and an aggregate report at the ratified schemas |
|
|
22
|
-
| Guide | `guide/ci-pipeline.md` | Operator-facing: when to use it, when not, what stays your call, and how to extend the required gate set |
|
|
23
|
-
| Coordination vocabulary | `docs/topics.md` | The two global-topic strings this blueprint contributes, the shared id band registry (application-spa, application-api-rest, security-auth-magic-link, hello-panel, persistence-data-sqlite, ci-pipeline, observability-essentials) |
|
|
24
|
-
|
|
25
|
-
The doc set is contributions (copied into the project tree by `rcf define blueprint add`); the guide, assets, and docs are package-resident references. Guide rendering into `rcf/knowledge/docs/blueprint-guides/` and asset ingestion are mechanism follow-ups; until they land, the working agent reads them from the applied blueprint's source path recorded in `manifest.blueprints[].source`.
|
|
26
|
-
|
|
27
|
-
## What it contributes, and what it deliberately does not
|
|
28
|
-
|
|
29
|
-
Contributed kinds: REQ, US (with inline ACs), TAC, ADR. Adherence is expressed as ACs; the blueprint ships no test files (ratified decision 5) and no code.
|
|
30
|
-
|
|
31
|
-
No FBS contributions, as a matter of principle (ratified policy 2026-08-19): FBSs are the work of the implementing agent, not the blueprint. The blueprint contributes the WHAT (the gate-runner contract, the per-gate report writer contract, the aggregate report writer contract, the fixed-gate-set discipline); the implementing agent derives the HOW-tasks in the host project, where the ACs contributed here get picked up by the project's own build sequencing.
|
|
32
|
-
|
|
33
|
-
Deliberately not contributed: a matrix of provider-specific configuration files (only one illustrative GitHub Actions workflow ships; alternate providers wire the same Node entry point per the guide's four-point mapping); provider-specific pipeline plugins or actions (each provider's action or plugin catalogue is a per-provider decision the project owns); a Node entry-point source file or scaffolding code (adherence is expressed as ACs, not code; the project realises the entry point in its own build cycle against the TAC contracts); a linter, formatter, or unit-test gate (out of scope at v1.0.0; a project that wants those adds them via a project-level ADR superseding ADR-701 and lists the extended gate set); a merge-queue integration (a project that wants one wires the aggregate report into the merge-queue tool the project picked); a coverage-trend dashboard (a project that wants one reads the aggregate reports over time and layers the dashboard on top).
|
|
34
|
-
|
|
35
|
-
## The two global decisions
|
|
36
|
-
|
|
37
|
-
ADR-701-ci-pipeline-ci-gates ships `scope: global` on topic `ciGates`. This is the project's required-gate set: two gates in fixed order at v1.0.0 (`validate` then `coverage-strict`). A composing blueprint that ships its own required-gate opinion (adherence packs, browser-verify smokes, observability probes) conflicts here by design and expects a project-level ADR that names the extended set.
|
|
38
|
-
|
|
39
|
-
ADR-702-ci-pipeline-strict-coverage-gate ships `scope: global` on topic `strictCoverageGate`. This is the project's coverage-mode posture: per-AC strict, not shallow-any. A composing blueprint that holds a different coverage posture (a shallow-any-for-early-projects blueprint, a coverage-with-grace-window blueprint) conflicts here by design.
|
|
40
|
-
|
|
41
|
-
See `docs/topics.md` for the exact strings, the expected resolutions, the delineation from the application-api-rest blueprint's `logging` topic (wire-log shape, not build-time report shape), and the AC id band allocation (ci-pipeline owns 6101-6899).
|
|
42
|
-
|
|
43
|
-
## Quality bar
|
|
44
|
-
|
|
45
|
-
Every push to the default branch and every pull request into it runs the RCF gate suite as a required check; `rcf define validate` runs first and its non-zero exit terminates the aggregate as failed; `rcf audit coverage --strict` runs second and its non-zero exit terminates the aggregate as failed; per-gate JSON reports at stable paths carry a fixed key set (gate, command, toolVersion, startedAt, endedAt, durationMs, exitCode, outcome, stdout, stderr); an aggregate report at `<reportDir>/pipeline.json` carries verdict, trigger, timing, runner metadata, and the ordered gates array; a missing required gate report is recorded as `outcome: missing` with a reason and flips the aggregate to `failed`; the suite continues past a failing gate to record every failure on one run; operator-visible failure summaries name the gate and the report path in the runner-log tail; the Node entry point invocation is identical locally and in CI; no CI-provider environment variable gates the entry point's control flow; the applied blueprint's source path carries one illustrative GitHub Actions workflow and a notes file mapping four points to alternate providers. Every bar is carried by ACs in the doc set, not by this README.
|
|
46
|
-
|
|
47
|
-
## Known mechanism-reach gaps
|
|
48
|
-
|
|
49
|
-
None at v1.0.0. Every AC on every story is bound to at least one TAC that the host project must realise, and every AC's `then` clause is runtime-observable in the deployed pipeline surface (per-gate JSON report inspection, aggregate JSON report inspection, source-tree grep for CI-provider environment reads, workflow-file inspection under the applied blueprint's source path). The mechanism-reach principle from the authoring standard section 7 is satisfied at ship: a project that applies this blueprint and does not realise a TAC leaves an unresolved `tacIds` reference on the story that `rcf define validate` and `rcf audit coverage` refuse. The one operational responsibility a project must own is the branch-protection or equivalent merge-policy configuration on the default branch that binds the pipeline job's aggregate exit to the merge criterion (AC-6101-2); that responsibility is stated as an AC observable on the platform's own configuration surface, not a smuggled runtime probe.
|