rcf-lite 0.28.2 → 0.29.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 +317 -0
- package/bin/rcf.js +8 -0
- package/fixtures/canary-manifest.json +9 -9
- package/guidance/define-draft-shapes.md +89 -0
- package/guidance/define-intake.md +97 -0
- package/guidance/define-intent.md +107 -0
- package/guidance/discover-prototype.md +67 -0
- package/guidance/discovery-prototypes.md +69 -0
- package/guidance/harness-template.md +34 -0
- package/guidance/managed/agent-instructions-block.hash +1 -1
- package/guidance/managed/agent-instructions-block.md +34 -0
- package/guidance/manifest.json +9 -2
- package/package.json +1 -1
- package/rcf/adrs/adr-4120-canonical-json-hashing.json +30 -0
- package/rcf/adrs/adr-4121-define-sidecars.json +30 -0
- package/rcf/adrs/adr-4122-define-gates-blocking-and-optout.json +30 -0
- package/rcf/adrs/adr-4123-readiness-single-object.json +30 -0
- package/rcf/adrs/adr-4124-define-freeze-verb.json +30 -0
- package/rcf/adrs/adr-4126-persona-tagged-blockers-two-levels.json +40 -0
- package/rcf/adrs/adr-4127-readiness-tab-order-and-register.json +40 -0
- package/rcf/adrs/adr-4129-questions-harness-driven-loop.json +25 -0
- package/rcf/adrs/adr-4130-intake-harness-split-tool-scanned.json +20 -0
- package/rcf/adrs/adr-4132-prototypes-in-discovery-as-evidence.json +20 -0
- package/rcf/adrs/adr-4133-dag-dependency-depth-layout.json +40 -0
- package/rcf/adrs/adr-4134-index-json-route.json +35 -0
- package/rcf/adrs/adr-4135-readiness-po-register-and-adapter.json +35 -0
- package/rcf/adrs/adr-4136-embedding-host-fixture.json +35 -0
- package/rcf/code-nodes/cn-700.json +23 -0
- package/rcf/code-nodes/cn-701.json +14 -0
- package/rcf/code-nodes/cn-702.json +14 -0
- package/rcf/code-nodes/cn-703.json +16 -0
- package/rcf/code-nodes/cn-720.json +15 -0
- package/rcf/code-nodes/cn-721.json +15 -0
- package/rcf/code-nodes/cn-722.json +15 -0
- package/rcf/code-nodes/cn-723.json +18 -0
- package/rcf/code-nodes/cn-724.json +14 -0
- package/rcf/code-nodes/cn-725.json +37 -0
- package/rcf/code-nodes/cn-726.json +25 -0
- package/rcf/code-nodes/cn-727.json +23 -0
- package/rcf/code-nodes/cn-728.json +37 -0
- package/rcf/code-nodes/cn-729.json +26 -0
- package/rcf/code-nodes/cn-730.json +24 -0
- package/rcf/code-nodes/cn-731.json +20 -0
- package/rcf/code-nodes/cn-732.json +14 -0
- package/rcf/code-nodes/cn-733.json +14 -0
- package/rcf/code-nodes/cn-734.json +14 -0
- package/rcf/code-nodes/cn-735.json +20 -0
- package/rcf/code-nodes/cn-736.json +14 -0
- package/rcf/code-nodes/cn-737.json +14 -0
- package/rcf/code-nodes/cn-738.json +14 -0
- package/rcf/code-nodes/cn-739.json +18 -0
- package/rcf/code-nodes/cn-740.json +19 -0
- package/rcf/code-nodes/cn-741.json +16 -0
- package/rcf/code-nodes/cn-742.json +16 -0
- package/rcf/code-nodes/cn-743.json +15 -0
- package/rcf/code-nodes/cn-744.json +22 -0
- package/rcf/code-nodes/cn-745.json +16 -0
- package/rcf/code-nodes/cn-746.json +14 -0
- package/rcf/code-nodes/cn-747.json +18 -0
- package/rcf/code-nodes/cn-748.json +16 -0
- package/rcf/code-nodes/cn-749.json +16 -0
- package/rcf/code-nodes/cn-750.json +15 -0
- package/rcf/code-nodes/cn-751.json +18 -0
- package/rcf/code-nodes/cn-752.json +14 -0
- package/rcf/fbs/fbs-190.json +37 -0
- package/rcf/fbs/fbs-191.json +34 -0
- package/rcf/fbs/fbs-192.json +17 -0
- package/rcf/fbs/fbs-193.json +37 -0
- package/rcf/fbs/fbs-194.json +35 -0
- package/rcf/fbs/fbs-195.json +37 -0
- package/rcf/fbs/fbs-196.json +45 -0
- package/rcf/fbs/fbs-197.json +41 -0
- package/rcf/fbs/fbs-198.json +42 -0
- package/rcf/fbs/fbs-199.json +59 -0
- package/rcf/fbs/fbs-200.json +46 -0
- package/rcf/fbs/fbs-201.json +49 -0
- package/rcf/fbs/fbs-202.json +110 -0
- package/rcf/fbs/fbs-203.json +100 -0
- package/rcf/requirements/req-001.json +2 -1
- package/rcf/requirements/req-002.json +3 -2
- package/rcf/requirements/req-003.json +2 -1
- package/rcf/requirements/req-004.json +2 -1
- package/rcf/requirements/req-005.json +2 -1
- package/rcf/requirements/req-006.json +2 -1
- package/rcf/requirements/req-007.json +2 -1
- package/rcf/requirements/req-008.json +2 -1
- package/rcf/requirements/req-009.json +2 -1
- package/rcf/requirements/req-010.json +5 -2
- package/rcf/requirements/req-011.json +2 -1
- package/rcf/requirements/req-012.json +2 -1
- package/rcf/requirements/req-013.json +2 -1
- package/rcf/requirements/req-014.json +2 -1
- package/rcf/requirements/req-015.json +2 -1
- package/rcf/requirements/req-016.json +5 -2
- package/rcf/requirements/req-017.json +6 -2
- package/rcf/requirements/req-018.json +5 -2
- package/rcf/requirements/req-019.json +5 -2
- package/rcf/requirements/req-020.json +5 -2
- package/rcf/requirements/req-021.json +5 -2
- package/rcf/requirements/req-022.json +5 -2
- package/rcf/requirements/req-023.json +6 -4
- package/rcf/requirements/req-024.json +5 -2
- package/rcf/requirements/req-025.json +5 -2
- package/rcf/requirements/req-026.json +5 -2
- package/rcf/requirements/req-027.json +5 -2
- package/rcf/requirements/req-028.json +5 -2
- package/rcf/requirements/req-029.json +5 -2
- package/rcf/requirements/req-030.json +5 -2
- package/rcf/requirements/req-040.json +5 -2
- package/rcf/requirements/req-041.json +5 -2
- package/rcf/requirements/req-042.json +5 -2
- package/rcf/requirements/req-043.json +5 -2
- package/rcf/requirements/req-050.json +5 -2
- package/rcf/requirements/req-051.json +5 -2
- package/rcf/requirements/req-052.json +5 -2
- package/rcf/requirements/req-053.json +5 -2
- package/rcf/requirements/req-054.json +5 -2
- package/rcf/requirements/req-060.json +5 -2
- package/rcf/requirements/req-061.json +5 -2
- package/rcf/requirements/req-062.json +5 -2
- package/rcf/requirements/req-063.json +5 -2
- package/rcf/requirements/req-070.json +5 -2
- package/rcf/requirements/req-071.json +5 -2
- package/rcf/requirements/req-072.json +5 -2
- package/rcf/requirements/req-073.json +5 -2
- package/rcf/requirements/req-074.json +5 -2
- package/rcf/requirements/req-075.json +5 -2
- package/rcf/requirements/req-076.json +5 -2
- package/rcf/requirements/req-077.json +5 -2
- package/rcf/requirements/req-080.json +5 -2
- package/rcf/requirements/req-081.json +5 -2
- package/rcf/requirements/req-082.json +5 -2
- package/rcf/requirements/req-083.json +5 -2
- package/rcf/requirements/req-084.json +5 -2
- package/rcf/requirements/req-085.json +5 -2
- package/rcf/requirements/req-086.json +5 -2
- package/rcf/requirements/req-090.json +5 -2
- package/rcf/requirements/req-091.json +5 -2
- package/rcf/requirements/req-092.json +5 -2
- package/rcf/requirements/req-093.json +5 -2
- package/rcf/requirements/req-094.json +5 -2
- package/rcf/requirements/req-100.json +5 -2
- package/rcf/requirements/req-101.json +5 -2
- package/rcf/requirements/req-102.json +5 -2
- package/rcf/requirements/req-103.json +5 -2
- package/rcf/requirements/req-104.json +5 -2
- package/rcf/requirements/req-110.json +5 -2
- package/rcf/requirements/req-111.json +5 -2
- package/rcf/requirements/req-112.json +5 -2
- package/rcf/requirements/req-113.json +5 -2
- package/rcf/requirements/req-114.json +5 -2
- package/rcf/requirements/req-115.json +5 -2
- package/rcf/requirements/req-120.json +5 -2
- package/rcf/requirements/req-121.json +5 -2
- package/rcf/requirements/req-122.json +5 -2
- package/rcf/requirements/req-123.json +5 -2
- package/rcf/requirements/req-124.json +5 -2
- package/rcf/requirements/req-125.json +5 -2
- package/rcf/requirements/req-130.json +6 -2
- package/rcf/requirements/req-131.json +6 -2
- package/rcf/requirements/req-132.json +6 -2
- package/rcf/requirements/req-133.json +6 -2
- package/rcf/requirements/req-134.json +6 -2
- package/rcf/requirements/req-140.json +5 -2
- package/rcf/requirements/req-145.json +5 -2
- package/rcf/requirements/req-150.json +6 -2
- package/rcf/requirements/req-151.json +6 -2
- package/rcf/requirements/req-152.json +6 -2
- package/rcf/requirements/req-153.json +6 -2
- package/rcf/requirements/req-154.json +5 -2
- package/rcf/requirements/req-155.json +4 -1
- package/rcf/requirements/req-156.json +4 -1
- package/rcf/requirements/req-157.json +4 -1
- package/rcf/requirements/req-158.json +4 -1
- package/rcf/requirements/req-159.json +4 -1
- package/rcf/requirements/req-160.json +4 -1
- package/rcf/requirements/req-161.json +4 -1
- package/rcf/requirements/req-162.json +4 -1
- package/rcf/requirements/req-163.json +5 -2
- package/rcf/requirements/req-164.json +5 -2
- package/rcf/requirements/req-165.json +5 -2
- package/rcf/requirements/req-170.json +33 -0
- package/rcf/requirements/req-171.json +44 -0
- package/rcf/requirements/req-172.json +25 -0
- package/rcf/requirements/req-173.json +25 -0
- package/rcf/requirements/req-174.json +31 -0
- package/rcf/requirements/req-175.json +31 -0
- package/rcf/requirements/req-176.json +25 -0
- package/rcf/requirements/req-180.json +31 -0
- package/rcf/requirements/req-181.json +31 -0
- package/rcf/requirements/req-182.json +25 -0
- package/rcf/requirements/req-186.json +25 -0
- package/rcf/requirements/req-187.json +31 -0
- package/rcf/requirements/req-188.json +25 -0
- package/rcf/tacs/tac-4120-define-delta-and-freeze.json +21 -0
- package/rcf/tacs/tac-4121-define-ledgers.json +35 -0
- package/rcf/tacs/tac-4122-define-gates.json +61 -0
- package/rcf/tacs/tac-4123-define-readiness.json +61 -0
- package/rcf/tacs/tac-4124-define-freeze.json +28 -0
- package/rcf/tacs/tac-4127-readiness-view.json +49 -0
- package/rcf/tacs/tac-4128-define-guidance.json +53 -0
- package/rcf/tacs/tac-4129-define-questions.json +45 -0
- package/rcf/tacs/tac-4130-define-intake-brief.json +35 -0
- package/rcf/tacs/tac-4131-build-dag-view.json +39 -0
- package/rcf/tacs/tac-4132-id-lookup.json +42 -0
- package/rcf/tacs/tac-4133-readiness-po-layer.json +37 -0
- package/rcf/tacs/tac-4134-wespa-host-fixture.json +54 -0
- package/rcf/test-suites/ts-016.json +3 -3
- package/rcf/test-suites/ts-204.json +30 -0
- package/rcf/test-suites/ts-205.json +22 -0
- package/rcf/test-suites/ts-206.json +22 -0
- package/rcf/test-suites/ts-207.json +22 -0
- package/rcf/test-suites/ts-208.json +22 -0
- package/rcf/test-suites/ts-209.json +22 -0
- package/rcf/test-suites/ts-210.json +22 -0
- package/rcf/test-suites/ts-211.json +22 -0
- package/rcf/test-suites/ts-212.json +22 -0
- package/rcf/test-suites/ts-213.json +50 -0
- package/rcf/test-suites/ts-214.json +74 -0
- package/rcf/test-suites/ts-215.json +211 -0
- package/rcf/test-suites/ts-216.json +94 -0
- package/rcf/test-suites/ts-217.json +62 -0
- package/rcf/test-suites/ts-218.json +78 -0
- package/rcf/test-suites/ts-219.json +70 -0
- package/rcf/test-suites/ts-220.json +78 -0
- package/rcf/test-suites/ts-221.json +78 -0
- package/rcf/test-suites/ts-222.json +100 -0
- package/rcf/test-suites/ts-223.json +94 -0
- package/rcf/test-suites/ts-224.json +70 -0
- package/rcf/test-suites/ts-225.json +78 -0
- package/rcf/test-suites/ts-226.json +158 -0
- package/rcf/test-suites/ts-227.json +38 -0
- package/rcf/test-suites/ts-228.json +86 -0
- package/rcf/test-suites/ts-229.json +83 -0
- package/rcf/user-stories/us-15601.json +4 -4
- package/rcf/user-stories/us-17001.json +27 -0
- package/rcf/user-stories/us-17002.json +22 -0
- package/rcf/user-stories/us-17003.json +22 -0
- package/rcf/user-stories/us-17004.json +22 -0
- package/rcf/user-stories/us-17005.json +22 -0
- package/rcf/user-stories/us-17006.json +21 -0
- package/rcf/user-stories/us-17007.json +21 -0
- package/rcf/user-stories/us-17008.json +22 -0
- package/rcf/user-stories/us-17101.json +22 -0
- package/rcf/user-stories/us-17102.json +22 -0
- package/rcf/user-stories/us-17201.json +38 -0
- package/rcf/user-stories/us-17301.json +38 -0
- package/rcf/user-stories/us-17302.json +50 -0
- package/rcf/user-stories/us-17401.json +70 -0
- package/rcf/user-stories/us-17402.json +55 -0
- package/rcf/user-stories/us-17403.json +60 -0
- package/rcf/user-stories/us-17501.json +48 -0
- package/rcf/user-stories/us-17502.json +60 -0
- package/rcf/user-stories/us-17503.json +65 -0
- package/rcf/user-stories/us-17601.json +58 -0
- package/rcf/user-stories/us-18001.json +55 -0
- package/rcf/user-stories/us-18002.json +35 -0
- package/rcf/user-stories/us-18101.json +35 -0
- package/rcf/user-stories/us-18202.json +40 -0
- package/rcf/user-stories/us-18601.json +55 -0
- package/rcf/user-stories/us-18602.json +50 -0
- package/rcf/user-stories/us-18603.json +45 -0
- package/rcf/user-stories/us-18701.json +50 -0
- package/rcf/user-stories/us-18702.json +50 -0
- package/rcf/user-stories/us-18801.json +45 -0
- package/rcf/user-stories/us-204.json +59 -0
- package/rcf/user-stories/us-205.json +64 -0
- package/rcf/user-stories/us-206.json +54 -0
- package/releases/releases.yaml +31 -1
- package/src/cli/feedback.js +126 -10
- package/src/cli/freeze.js +730 -0
- package/src/cli/help.js +23 -0
- package/src/cli/ledger.js +615 -0
- package/src/cli/questions.js +288 -0
- package/src/cli/readiness.js +498 -0
- package/src/cli/view.js +29 -0
- package/src/core/store/writer.js +86 -6
- package/src/define/freeze-record.js +247 -0
- package/src/define/ledgers.js +712 -0
- package/src/feedback/redact.js +105 -19
- package/src/intake/index.js +1 -1
- package/src/intake/orchestrator.js +117 -0
- package/src/mcp/tools.js +354 -0
- package/src/query/delta.js +282 -0
- package/src/query/gates.js +1107 -0
- package/src/query/index.js +50 -0
- package/src/query/questions.js +438 -0
- package/src/query/readiness.js +574 -0
- package/src/server/index.js +60 -1
- package/src/server/routes.js +133 -4
- package/src/setup/identity-seed.js +17 -0
- package/src/view/build-dag.js +563 -0
- package/src/view/components/badge.js +41 -0
- package/src/view/components/diff.js +72 -0
- package/src/view/components/doc-row.js +62 -0
- package/src/view/components/empty-state.js +35 -0
- package/src/view/components/entity-selector.js +137 -0
- package/src/view/components/filter-bar.js +97 -0
- package/src/view/components/findings-list.js +49 -0
- package/src/view/components/index.js +12 -0
- package/src/view/components/pill.js +46 -0
- package/src/view/components/sub-tab-strip.js +43 -0
- package/src/view/components/toast.js +25 -0
- package/src/view/components-fixture.js +265 -0
- package/src/view/doc-renderers/architecture.js +304 -0
- package/src/view/doc-renderers/build.js +160 -0
- package/src/view/doc-renderers/helpers.js +2 -2
- package/src/view/doc-renderers/index.js +2 -0
- package/src/view/doc-renderers/prd.js +35 -6
- package/src/view/doc-renderers/req.js +10 -2
- package/src/view/doc-renderers/user-story.js +10 -5
- package/src/view/html-page.js +717 -300
- package/src/view/index.js +109 -3
- package/src/view/live-client.js +61 -8
- package/src/view/needs-work.js +79 -0
- package/src/view/page-init.js +2193 -0
- package/src/view/product-map.js +751 -0
- package/src/view/readiness/phrasebook.js +115 -0
- package/src/view/readiness/po-layer.js +288 -0
- package/src/view/readiness/question-adapter.js +329 -0
- package/src/view/readiness/stage-legend.js +71 -0
- package/src/view/readiness.js +505 -0
- package/src/view/style.css +2279 -266
- package/src/view/test-host/embed-client.js +121 -0
- package/src/view/test-host/fixture.js +183 -0
- package/src/view/test-host/lints.js +220 -0
- package/src/view/view-index.js +323 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Wespa host fixture client (viewer UI refresh PR 9; TAC-4134).
|
|
2
|
+
// Served at /test-host.js by the viewer's dev server when --test-host
|
|
3
|
+
// is on (RCF_VIEW_TEST_HOST=1 equivalent). Wires three fixture
|
|
4
|
+
// behaviours:
|
|
5
|
+
// - Theme toggle: posts { type: "rcf-view-theme", theme } to the
|
|
6
|
+
// iframe.contentWindow, targeting window.location.origin so a
|
|
7
|
+
// cross-origin receiver would refuse the message. Does not read
|
|
8
|
+
// event.origin itself; the viewer does, and the fixture READS the
|
|
9
|
+
// iframe's data-theme attribute as evidence.
|
|
10
|
+
// - Sidebar drag: a 6px handle on the sidebar's right edge sets
|
|
11
|
+
// --wespa-sidebar-width on the <main> grid; clamped to 64..400 px
|
|
12
|
+
// live.
|
|
13
|
+
// - Collapse/Expand: snaps the sidebar between 64 px (collapsed) and
|
|
14
|
+
// 200 px (expanded) and toggles the .collapsed class.
|
|
15
|
+
//
|
|
16
|
+
// Does NOT persist theme or sidebar width to localStorage (ADR-4136):
|
|
17
|
+
// a passing run has to flip the iframe through postMessage, which is
|
|
18
|
+
// the invariant the fixture pins.
|
|
19
|
+
//
|
|
20
|
+
// Classic (non-module) browser script so the fixture page carries no
|
|
21
|
+
// inline <script> and the embed-client is a plain <script src>.
|
|
22
|
+
|
|
23
|
+
(function () {
|
|
24
|
+
var SIDEBAR_MIN = 64;
|
|
25
|
+
var SIDEBAR_EXPANDED = 200;
|
|
26
|
+
var SIDEBAR_MAX = 400;
|
|
27
|
+
|
|
28
|
+
function iframe() {
|
|
29
|
+
return document.getElementById('rcf-test-host-iframe');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function postThemeToIframe(theme) {
|
|
33
|
+
var el = iframe();
|
|
34
|
+
if (!el || !el.contentWindow) return;
|
|
35
|
+
try {
|
|
36
|
+
el.contentWindow.postMessage(
|
|
37
|
+
{ type: 'rcf-view-theme', theme: theme },
|
|
38
|
+
window.location.origin,
|
|
39
|
+
);
|
|
40
|
+
} catch (e) { /* swallow */ }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function wireThemeToggle() {
|
|
44
|
+
var buttons = Array.prototype.slice.call(
|
|
45
|
+
document.querySelectorAll('[data-rcf-host-theme]'),
|
|
46
|
+
);
|
|
47
|
+
if (buttons.length === 0) return;
|
|
48
|
+
buttons.forEach(function (btn) {
|
|
49
|
+
btn.addEventListener('click', function () {
|
|
50
|
+
var theme = btn.getAttribute('data-rcf-host-theme');
|
|
51
|
+
if (theme !== 'light' && theme !== 'dark') return;
|
|
52
|
+
buttons.forEach(function (b) {
|
|
53
|
+
b.setAttribute('aria-pressed', b === btn ? 'true' : 'false');
|
|
54
|
+
});
|
|
55
|
+
postThemeToIframe(theme);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function setSidebarWidth(px) {
|
|
61
|
+
var shell = document.querySelector('main.wespa-shell');
|
|
62
|
+
if (!shell) return;
|
|
63
|
+
var clamped = Math.max(SIDEBAR_MIN, Math.min(SIDEBAR_MAX, Math.round(px)));
|
|
64
|
+
shell.style.setProperty('--wespa-sidebar-width', clamped + 'px');
|
|
65
|
+
var aside = document.querySelector('[data-rcf-host-sidebar]');
|
|
66
|
+
if (aside) {
|
|
67
|
+
if (clamped <= SIDEBAR_MIN) aside.classList.add('collapsed');
|
|
68
|
+
else aside.classList.remove('collapsed');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function wireSidebarDrag() {
|
|
73
|
+
var handle = document.querySelector('[data-rcf-host-sidebar-drag]');
|
|
74
|
+
var aside = document.querySelector('[data-rcf-host-sidebar]');
|
|
75
|
+
if (!handle || !aside) return;
|
|
76
|
+
var dragging = false;
|
|
77
|
+
handle.addEventListener('mousedown', function (ev) {
|
|
78
|
+
dragging = true;
|
|
79
|
+
handle.classList.add('dragging');
|
|
80
|
+
ev.preventDefault();
|
|
81
|
+
});
|
|
82
|
+
document.addEventListener('mousemove', function (ev) {
|
|
83
|
+
if (!dragging) return;
|
|
84
|
+
var rect = aside.getBoundingClientRect();
|
|
85
|
+
setSidebarWidth(ev.clientX - rect.left);
|
|
86
|
+
});
|
|
87
|
+
document.addEventListener('mouseup', function () {
|
|
88
|
+
if (!dragging) return;
|
|
89
|
+
dragging = false;
|
|
90
|
+
handle.classList.remove('dragging');
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function wireSidebarToggle() {
|
|
95
|
+
var btn = document.querySelector('[data-rcf-host-sidebar-toggle]');
|
|
96
|
+
var aside = document.querySelector('[data-rcf-host-sidebar]');
|
|
97
|
+
if (!btn || !aside) return;
|
|
98
|
+
btn.addEventListener('click', function () {
|
|
99
|
+
if (aside.classList.contains('collapsed')) {
|
|
100
|
+
setSidebarWidth(SIDEBAR_EXPANDED);
|
|
101
|
+
btn.textContent = 'Collapse';
|
|
102
|
+
} else {
|
|
103
|
+
setSidebarWidth(SIDEBAR_MIN);
|
|
104
|
+
btn.textContent = 'Expand';
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function boot() {
|
|
110
|
+
setSidebarWidth(SIDEBAR_EXPANDED);
|
|
111
|
+
wireThemeToggle();
|
|
112
|
+
wireSidebarDrag();
|
|
113
|
+
wireSidebarToggle();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (document.readyState === 'loading') {
|
|
117
|
+
document.addEventListener('DOMContentLoaded', boot);
|
|
118
|
+
} else {
|
|
119
|
+
boot();
|
|
120
|
+
}
|
|
121
|
+
})();
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// Wespa host fixture (viewer UI refresh PR 9; TAC-4134, ADR-4136).
|
|
2
|
+
// Served by the viewer's own dev server at /test-host.html when
|
|
3
|
+
// --test-host is passed (or RCF_VIEW_TEST_HOST=1 is set); 404 otherwise
|
|
4
|
+
// so wespa's reverse-proxy allow-list is unaffected. Mimics the wespa
|
|
5
|
+
// shell: a 56px header carrying a Light/Dark toggle, a resizable
|
|
6
|
+
// sidebar (64px collapsed, 200-400px expanded, user-resizable live via
|
|
7
|
+
// a drag handle), a content pane carrying a single iframe pointed at
|
|
8
|
+
// the viewer root ("./?embed=1&theme=light" resolves to "/?..."
|
|
9
|
+
// because the fixture lives at the root path, not under /test-host/).
|
|
10
|
+
// The fixture carries no inline script; its client-side wiring lives
|
|
11
|
+
// at /test-host.js (served as a snapshotted asset at startup; see
|
|
12
|
+
// server/index.js).
|
|
13
|
+
//
|
|
14
|
+
// The fixture does not persist theme to localStorage: a passing run
|
|
15
|
+
// has to flip the iframe through the postMessage path, which is the
|
|
16
|
+
// invariant ADR-4136 pins. The host never reads event.origin itself
|
|
17
|
+
// (the viewer does); the host only READS the iframe's data-theme and
|
|
18
|
+
// data-embed attributes as evidence.
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Return the fixture HTML string. Pure function, no I/O.
|
|
22
|
+
*
|
|
23
|
+
* @returns {string}
|
|
24
|
+
*/
|
|
25
|
+
export function renderTestHostPage() {
|
|
26
|
+
return `<!DOCTYPE html>
|
|
27
|
+
<html lang="en-GB" data-rcf-test-host="wespa">
|
|
28
|
+
<head>
|
|
29
|
+
<meta charset="utf-8">
|
|
30
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
31
|
+
<title>Wespa host fixture - rcf-lite viewer embed proof</title>
|
|
32
|
+
<style>
|
|
33
|
+
html, body { height: 100%; margin: 0; }
|
|
34
|
+
body {
|
|
35
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, sans-serif;
|
|
36
|
+
display: grid;
|
|
37
|
+
grid-template-rows: 56px 1fr;
|
|
38
|
+
background: #0e1116;
|
|
39
|
+
color: #c9d1d9;
|
|
40
|
+
}
|
|
41
|
+
header.wespa-header {
|
|
42
|
+
height: 56px;
|
|
43
|
+
display: flex;
|
|
44
|
+
align-items: center;
|
|
45
|
+
gap: 1rem;
|
|
46
|
+
padding: 0 1rem;
|
|
47
|
+
background: #161b22;
|
|
48
|
+
border-bottom: 1px solid #30363d;
|
|
49
|
+
box-sizing: border-box;
|
|
50
|
+
}
|
|
51
|
+
header.wespa-header .wespa-brand {
|
|
52
|
+
font-weight: 700;
|
|
53
|
+
font-size: 0.95rem;
|
|
54
|
+
letter-spacing: -0.01em;
|
|
55
|
+
}
|
|
56
|
+
header.wespa-header .wespa-brand-sub {
|
|
57
|
+
color: #8b949e;
|
|
58
|
+
font-size: 0.85rem;
|
|
59
|
+
margin-left: 0.5rem;
|
|
60
|
+
}
|
|
61
|
+
header.wespa-header .wespa-theme-toggle {
|
|
62
|
+
margin-left: auto;
|
|
63
|
+
display: inline-flex;
|
|
64
|
+
align-items: center;
|
|
65
|
+
gap: 0.3rem;
|
|
66
|
+
font-size: 0.85rem;
|
|
67
|
+
}
|
|
68
|
+
header.wespa-header .wespa-theme-toggle button {
|
|
69
|
+
appearance: none;
|
|
70
|
+
border: 1px solid #30363d;
|
|
71
|
+
background: #0e1116;
|
|
72
|
+
color: #c9d1d9;
|
|
73
|
+
padding: 0.25rem 0.65rem;
|
|
74
|
+
border-radius: 6px;
|
|
75
|
+
cursor: pointer;
|
|
76
|
+
font-family: inherit;
|
|
77
|
+
font-size: 0.85rem;
|
|
78
|
+
}
|
|
79
|
+
header.wespa-header .wespa-theme-toggle button[aria-pressed="true"] {
|
|
80
|
+
background: #1f6feb;
|
|
81
|
+
border-color: #1f6feb;
|
|
82
|
+
color: #ffffff;
|
|
83
|
+
font-weight: 600;
|
|
84
|
+
}
|
|
85
|
+
main.wespa-shell {
|
|
86
|
+
display: grid;
|
|
87
|
+
grid-template-columns: var(--wespa-sidebar-width, 200px) 1fr;
|
|
88
|
+
overflow: hidden;
|
|
89
|
+
}
|
|
90
|
+
aside.wespa-sidebar {
|
|
91
|
+
background: #161b22;
|
|
92
|
+
border-right: 1px solid #30363d;
|
|
93
|
+
position: relative;
|
|
94
|
+
overflow: hidden;
|
|
95
|
+
display: flex;
|
|
96
|
+
flex-direction: column;
|
|
97
|
+
}
|
|
98
|
+
aside.wespa-sidebar .wespa-sidebar-head {
|
|
99
|
+
display: flex;
|
|
100
|
+
align-items: center;
|
|
101
|
+
justify-content: space-between;
|
|
102
|
+
padding: 0.5rem 0.75rem;
|
|
103
|
+
border-bottom: 1px solid #30363d;
|
|
104
|
+
}
|
|
105
|
+
aside.wespa-sidebar .wespa-sidebar-head button {
|
|
106
|
+
appearance: none;
|
|
107
|
+
border: 1px solid #30363d;
|
|
108
|
+
background: #0e1116;
|
|
109
|
+
color: #c9d1d9;
|
|
110
|
+
padding: 0.2rem 0.5rem;
|
|
111
|
+
border-radius: 6px;
|
|
112
|
+
cursor: pointer;
|
|
113
|
+
font-family: inherit;
|
|
114
|
+
font-size: 0.75rem;
|
|
115
|
+
}
|
|
116
|
+
aside.wespa-sidebar .wespa-sidebar-items {
|
|
117
|
+
flex: 1;
|
|
118
|
+
overflow: auto;
|
|
119
|
+
padding: 0.5rem;
|
|
120
|
+
color: #8b949e;
|
|
121
|
+
font-size: 0.85rem;
|
|
122
|
+
}
|
|
123
|
+
aside.wespa-sidebar .wespa-sidebar-items p { margin: 0 0 0.5rem; }
|
|
124
|
+
aside.wespa-sidebar.collapsed .wespa-sidebar-items { display: none; }
|
|
125
|
+
aside.wespa-sidebar .wespa-sidebar-drag {
|
|
126
|
+
position: absolute;
|
|
127
|
+
top: 0;
|
|
128
|
+
right: 0;
|
|
129
|
+
bottom: 0;
|
|
130
|
+
width: 6px;
|
|
131
|
+
cursor: col-resize;
|
|
132
|
+
background: transparent;
|
|
133
|
+
}
|
|
134
|
+
aside.wespa-sidebar .wespa-sidebar-drag:hover,
|
|
135
|
+
aside.wespa-sidebar .wespa-sidebar-drag.dragging {
|
|
136
|
+
background: #1f6feb;
|
|
137
|
+
}
|
|
138
|
+
section.wespa-content {
|
|
139
|
+
position: relative;
|
|
140
|
+
overflow: hidden;
|
|
141
|
+
background: #0e1116;
|
|
142
|
+
}
|
|
143
|
+
iframe.wespa-iframe {
|
|
144
|
+
border: 0;
|
|
145
|
+
width: 100%;
|
|
146
|
+
height: 100%;
|
|
147
|
+
display: block;
|
|
148
|
+
background: #ffffff;
|
|
149
|
+
}
|
|
150
|
+
</style>
|
|
151
|
+
</head>
|
|
152
|
+
<body>
|
|
153
|
+
<header class="wespa-header">
|
|
154
|
+
<span class="wespa-brand">Wespa</span>
|
|
155
|
+
<span class="wespa-brand-sub">rcf-lite viewer embed proof</span>
|
|
156
|
+
<div class="wespa-theme-toggle" role="group" aria-label="Iframe theme">
|
|
157
|
+
<span>Theme</span>
|
|
158
|
+
<button type="button" data-rcf-host-theme="light" aria-pressed="true">Light</button>
|
|
159
|
+
<button type="button" data-rcf-host-theme="dark" aria-pressed="false">Dark</button>
|
|
160
|
+
</div>
|
|
161
|
+
</header>
|
|
162
|
+
<main class="wespa-shell">
|
|
163
|
+
<aside class="wespa-sidebar" data-rcf-host-sidebar>
|
|
164
|
+
<div class="wespa-sidebar-head">
|
|
165
|
+
<span>Projects</span>
|
|
166
|
+
<button type="button" data-rcf-host-sidebar-toggle>Collapse</button>
|
|
167
|
+
</div>
|
|
168
|
+
<div class="wespa-sidebar-items">
|
|
169
|
+
<p>rcf-lite</p>
|
|
170
|
+
<p>wespa</p>
|
|
171
|
+
<p>design docs</p>
|
|
172
|
+
</div>
|
|
173
|
+
<div class="wespa-sidebar-drag" data-rcf-host-sidebar-drag title="Drag to resize"></div>
|
|
174
|
+
</aside>
|
|
175
|
+
<section class="wespa-content">
|
|
176
|
+
<iframe class="wespa-iframe" id="rcf-test-host-iframe" title="rcf-lite viewer" src="./?embed=1&theme=light"></iframe>
|
|
177
|
+
</section>
|
|
178
|
+
</main>
|
|
179
|
+
<script src="./test-host.js" defer></script>
|
|
180
|
+
</body>
|
|
181
|
+
</html>
|
|
182
|
+
`;
|
|
183
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// Three embedding lints (viewer UI refresh PR 9; TAC-4134, ADR-4136).
|
|
2
|
+
// Pure assertions each caller composes into whatever test harness it
|
|
3
|
+
// owns; test/view/wespa-host-fixture.test.js is the suite caller.
|
|
4
|
+
//
|
|
5
|
+
// (1) assertFramingHeaders(response)
|
|
6
|
+
// Refuses on X-Frame-Options, on CSP frame-ancestors that forbids
|
|
7
|
+
// same-origin, or on any header the viewer sets that would block
|
|
8
|
+
// wespa's same-origin iframe mount. The viewer today sets neither,
|
|
9
|
+
// so a passing run proves the browser accepts the iframe.
|
|
10
|
+
//
|
|
11
|
+
// (2) assertNoTargetTop(renderedHtml)
|
|
12
|
+
// Greps the server-rendered HTML for target="_top" and target='_top'
|
|
13
|
+
// and refuses on any match. Dex's contract (section 9) bans
|
|
14
|
+
// target="_top" links so a legitimate in-iframe navigation never
|
|
15
|
+
// breaks the embedding host out of its box.
|
|
16
|
+
//
|
|
17
|
+
// (3) assertLocalStorageNamespace(sourceTreeRoot)
|
|
18
|
+
// Walks every .js file under sourceTreeRoot/src/view (excluding
|
|
19
|
+
// vendored and .min.js) and asserts every localStorage.* and shared
|
|
20
|
+
// storage.* call site uses a key literal starting with
|
|
21
|
+
// "rcf-view:v1:" (or references STORAGE_NS declared equal to that
|
|
22
|
+
// prefix). Dex's contract (section 9) requires the viewer namespace
|
|
23
|
+
// every localStorage key because the viewer shares localStorage
|
|
24
|
+
// with the wespa SPA.
|
|
25
|
+
|
|
26
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
27
|
+
import { join, extname } from 'node:path';
|
|
28
|
+
|
|
29
|
+
const STORAGE_NS = 'rcf-view:v1:';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {{ headers: Headers | Record<string, string> | Map<string, string> }} response
|
|
33
|
+
* @returns {{ ok: true, headers: { xFrameOptions: null, csp: string | null } } | { ok: false, reason: string }}
|
|
34
|
+
*/
|
|
35
|
+
export function assertFramingHeaders(response) {
|
|
36
|
+
const headers = response && response.headers;
|
|
37
|
+
if (!headers) return { ok: false, reason: 'no headers on response' };
|
|
38
|
+
const get = pickHeaderGetter(headers);
|
|
39
|
+
const xFrameOptions = get('x-frame-options');
|
|
40
|
+
if (xFrameOptions !== null && xFrameOptions !== undefined && String(xFrameOptions).trim() !== '') {
|
|
41
|
+
return { ok: false, reason: `X-Frame-Options present: ${xFrameOptions}` };
|
|
42
|
+
}
|
|
43
|
+
const csp = get('content-security-policy');
|
|
44
|
+
if (csp) {
|
|
45
|
+
const directive = parseCspFrameAncestors(String(csp));
|
|
46
|
+
if (directive !== null && !directive.allowsSameOrigin) {
|
|
47
|
+
return { ok: false, reason: `CSP frame-ancestors forbids same-origin: ${directive.raw}` };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { ok: true, headers: { xFrameOptions: null, csp: csp ?? null } };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {string} renderedHtml
|
|
55
|
+
* @returns {{ ok: true, count: 0 } | { ok: false, count: number, matches: string[] }}
|
|
56
|
+
*/
|
|
57
|
+
export function assertNoTargetTop(renderedHtml) {
|
|
58
|
+
if (typeof renderedHtml !== 'string') {
|
|
59
|
+
return { ok: false, count: 1, matches: ['non-string input'] };
|
|
60
|
+
}
|
|
61
|
+
const matches = [];
|
|
62
|
+
const re = /target\s*=\s*(?:"_top"|'_top'|_top\b)/gi;
|
|
63
|
+
let m;
|
|
64
|
+
while ((m = re.exec(renderedHtml)) !== null) {
|
|
65
|
+
matches.push(m[0]);
|
|
66
|
+
}
|
|
67
|
+
if (matches.length === 0) return { ok: true, count: 0 };
|
|
68
|
+
return { ok: false, count: matches.length, matches };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param {string} sourceTreeRoot - absolute path to the package root
|
|
73
|
+
* (e.g. packages/rcf-lite). The lint walks sourceTreeRoot/src/view.
|
|
74
|
+
* @returns {Promise<{ ok: true, scanned: number } | { ok: false, violations: Array<{ file: string, line: number, snippet: string, key: string }> }>}
|
|
75
|
+
*/
|
|
76
|
+
export async function assertLocalStorageNamespace(sourceTreeRoot) {
|
|
77
|
+
const viewRoot = join(sourceTreeRoot, 'src', 'view');
|
|
78
|
+
const files = await listJsFiles(viewRoot);
|
|
79
|
+
const violations = [];
|
|
80
|
+
for (const file of files) {
|
|
81
|
+
const text = await readFile(file, 'utf8');
|
|
82
|
+
const re = /(?:localStorage|storage|sessionStorage)\s*\.\s*(?:setItem|getItem|removeItem)\s*\(\s*([^,)]+)/g;
|
|
83
|
+
let m;
|
|
84
|
+
while ((m = re.exec(text)) !== null) {
|
|
85
|
+
const argExpr = m[1].trim();
|
|
86
|
+
const key = resolveKeyLiteral(argExpr, text);
|
|
87
|
+
if (key === null) continue;
|
|
88
|
+
if (!key.startsWith(STORAGE_NS)) {
|
|
89
|
+
const line = text.slice(0, m.index).split('\n').length;
|
|
90
|
+
violations.push({ file, line, snippet: m[0], key });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (violations.length > 0) return { ok: false, violations };
|
|
95
|
+
return { ok: true, scanned: files.length };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Pick a case-insensitive header getter that works whether headers is
|
|
100
|
+
* a Headers object, a Map, or a plain record.
|
|
101
|
+
*
|
|
102
|
+
* @param {Headers | Map<string, string> | Record<string, string>} headers
|
|
103
|
+
* @returns {(name: string) => string | null | undefined}
|
|
104
|
+
*/
|
|
105
|
+
function pickHeaderGetter(headers) {
|
|
106
|
+
if (typeof headers.get === 'function') return (name) => headers.get(name);
|
|
107
|
+
const map = /** @type {Record<string, string>} */ (headers);
|
|
108
|
+
return (name) => {
|
|
109
|
+
for (const k of Object.keys(map)) {
|
|
110
|
+
if (k.toLowerCase() === name.toLowerCase()) return map[k];
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Parse a CSP header string for a frame-ancestors directive. If the
|
|
118
|
+
* directive is present, return `{ allowsSameOrigin, raw }` where
|
|
119
|
+
* allowsSameOrigin is true if the directive contains 'self',
|
|
120
|
+
* window.location.origin, or '*'. Null if the directive is absent (no
|
|
121
|
+
* opinion on framing).
|
|
122
|
+
*
|
|
123
|
+
* @param {string} csp
|
|
124
|
+
* @returns {{ allowsSameOrigin: boolean, raw: string } | null}
|
|
125
|
+
*/
|
|
126
|
+
function parseCspFrameAncestors(csp) {
|
|
127
|
+
const parts = csp.split(';');
|
|
128
|
+
for (const part of parts) {
|
|
129
|
+
const trimmed = part.trim();
|
|
130
|
+
if (!/^frame-ancestors\b/i.test(trimmed)) continue;
|
|
131
|
+
const value = trimmed.slice('frame-ancestors'.length).trim();
|
|
132
|
+
if (value.length === 0) return { allowsSameOrigin: false, raw: trimmed };
|
|
133
|
+
const tokens = value.split(/\s+/);
|
|
134
|
+
const allowsSameOrigin = tokens.some((t) => (
|
|
135
|
+
t === "'self'" || t === '*' || /^https?:\/\//i.test(t)
|
|
136
|
+
));
|
|
137
|
+
return { allowsSameOrigin, raw: trimmed };
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Resolve a storage key argument expression to its literal string if
|
|
144
|
+
* possible. Returns null when the key is dynamic (we only lint literals,
|
|
145
|
+
* since a dynamic key already has to be composed from a literal prefix
|
|
146
|
+
* further up the file to pin the namespace).
|
|
147
|
+
*
|
|
148
|
+
* Accepts:
|
|
149
|
+
* - "rcf-view:v1:openDetails"
|
|
150
|
+
* - 'rcf-view:v1:openDetails'
|
|
151
|
+
* - STORAGE_OPEN -> resolves if `var STORAGE_OPEN = STORAGE_NS + 'openDetails';` or similar
|
|
152
|
+
* - STORAGE_NS + 'something'
|
|
153
|
+
*
|
|
154
|
+
* @param {string} argExpr - the trimmed argument expression as source
|
|
155
|
+
* @param {string} fileText - the full file text (for const resolution)
|
|
156
|
+
* @returns {string | null}
|
|
157
|
+
*/
|
|
158
|
+
function resolveKeyLiteral(argExpr, fileText) {
|
|
159
|
+
const strLit = argExpr.match(/^(['"`])((?:\\.|(?!\1).)*)\1/);
|
|
160
|
+
if (strLit) return strLit[2];
|
|
161
|
+
const addLit = argExpr.match(/^([A-Z_][A-Z0-9_]*)\s*\+\s*(['"`])((?:\\.|(?!\2).)*)\2/);
|
|
162
|
+
if (addLit) {
|
|
163
|
+
const base = resolveIdentifier(addLit[1], fileText);
|
|
164
|
+
if (base === null) return null;
|
|
165
|
+
return base + addLit[3];
|
|
166
|
+
}
|
|
167
|
+
const ident = argExpr.match(/^([A-Z_][A-Z0-9_]*)$/);
|
|
168
|
+
if (ident) {
|
|
169
|
+
return resolveIdentifier(ident[1], fileText);
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Resolve a module-level `var X = '...';` or `var X = Y + '...';`
|
|
176
|
+
* declaration to its literal string.
|
|
177
|
+
*
|
|
178
|
+
* @param {string} name
|
|
179
|
+
* @param {string} fileText
|
|
180
|
+
* @returns {string | null}
|
|
181
|
+
*/
|
|
182
|
+
function resolveIdentifier(name, fileText) {
|
|
183
|
+
const re = new RegExp(`(?:var|let|const)\\s+${name}\\s*=\\s*([^;\\n]+)[;\\n]`);
|
|
184
|
+
const m = fileText.match(re);
|
|
185
|
+
if (!m) return null;
|
|
186
|
+
return resolveKeyLiteral(m[1].trim(), fileText);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Walk a directory recursively and return every `.js` file path, excluding
|
|
191
|
+
* `vendored/` subtrees and `.min.js` artefacts.
|
|
192
|
+
*
|
|
193
|
+
* @param {string} dir
|
|
194
|
+
* @returns {Promise<string[]>}
|
|
195
|
+
*/
|
|
196
|
+
async function listJsFiles(dir) {
|
|
197
|
+
const out = [];
|
|
198
|
+
async function walk(d) {
|
|
199
|
+
let entries;
|
|
200
|
+
try {
|
|
201
|
+
entries = await readdir(d, { withFileTypes: true });
|
|
202
|
+
} catch (err) {
|
|
203
|
+
if (/** @type {NodeJS.ErrnoException} */ (err).code === 'ENOENT') return;
|
|
204
|
+
throw err;
|
|
205
|
+
}
|
|
206
|
+
for (const e of entries) {
|
|
207
|
+
const full = join(d, e.name);
|
|
208
|
+
if (e.isDirectory()) {
|
|
209
|
+
if (e.name === 'vendored') continue;
|
|
210
|
+
await walk(full);
|
|
211
|
+
} else if (e.isFile()) {
|
|
212
|
+
if (extname(e.name) !== '.js') continue;
|
|
213
|
+
if (e.name.endsWith('.min.js')) continue;
|
|
214
|
+
out.push(full);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
await walk(dir);
|
|
219
|
+
return out;
|
|
220
|
+
}
|