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,563 @@
|
|
|
1
|
+
// Build/DAG sub-tab renderer (viewer UI refresh PR 6; REQ-002, US-204,
|
|
2
|
+
// TAC-4131-build-dag-view, ADR-4133-dag-dependency-depth-layout).
|
|
3
|
+
//
|
|
4
|
+
// Pure HTML-string renderer for the Build tab's DAG sub-tab:
|
|
5
|
+
// - DagToolbar: Status chips, Area select, Buildable-now, Critical-path
|
|
6
|
+
// and Show-unconnected toggles plus a cycle finding (findingsList).
|
|
7
|
+
// - DagCanvas: HTML buttons for each FBS node + inline SVG edges, laid
|
|
8
|
+
// by `dependsOnFbsIds` depth (columns) and `buildOrder` (rows). The
|
|
9
|
+
// canvas owns its own overflow so the viewer never scrolls
|
|
10
|
+
// horizontally (ADR-4133).
|
|
11
|
+
// - DagInspector: a sticky side panel the client fills on select.
|
|
12
|
+
// - Unconnected lane: FBS with no incoming and no outgoing dependency,
|
|
13
|
+
// hidden by default; the toggle carries the count.
|
|
14
|
+
//
|
|
15
|
+
// Layout, closures and critical path come from `buildDagLayout` so the
|
|
16
|
+
// toolbar and the canvas see the same numbers (AC-204-1, AC-204-3). The
|
|
17
|
+
// inspector is HTML-only; the client in page-init.js (wireBuildDag)
|
|
18
|
+
// swaps its body on select without any outbound postMessage (AC-204-7).
|
|
19
|
+
|
|
20
|
+
import { escapeHtml } from './doc-renderers/helpers.js';
|
|
21
|
+
import { pill } from './components/pill.js';
|
|
22
|
+
import { renderBadge } from './components/badge.js';
|
|
23
|
+
import { findingsList } from './components/findings-list.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {object} DagNode
|
|
27
|
+
* @property {string} id
|
|
28
|
+
* @property {string} title
|
|
29
|
+
* @property {string} status
|
|
30
|
+
* @property {string} domain
|
|
31
|
+
* @property {string} size
|
|
32
|
+
* @property {number} buildOrder
|
|
33
|
+
* @property {number} acCount
|
|
34
|
+
* @property {number} depth
|
|
35
|
+
* @property {string[]} needs direct dependencies
|
|
36
|
+
* @property {string[]} waits direct dependants
|
|
37
|
+
* @property {string[]} upstream transitive closure of needs
|
|
38
|
+
* @property {string[]} downstream transitive closure of waits
|
|
39
|
+
* @property {boolean} buildable
|
|
40
|
+
* @property {boolean} critical
|
|
41
|
+
* @property {boolean} unconnected
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @typedef {object} DagLayout
|
|
46
|
+
* @property {Map<string, DagNode>} nodes
|
|
47
|
+
* @property {Array<{ from: string, to: string }>} edges
|
|
48
|
+
* @property {string[][]} columns column index -> fbsIds (buildOrder asc)
|
|
49
|
+
* @property {string[]} unconnectedIds fbsIds with no in and no out edge
|
|
50
|
+
* @property {string[]} criticalPathIds longest chain by node count
|
|
51
|
+
* @property {string[]} cycleIds fbsIds that sit on a dependsOnFbsIds cycle
|
|
52
|
+
* @property {number} maxDepth
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Pure projection of `fbsItems[].dependsOnFbsIds` into a DAG layout.
|
|
57
|
+
*
|
|
58
|
+
* - A node's depth is the longest chain length from `dependsOnFbsIds` to a
|
|
59
|
+
* root (0 for an FBS that declares no dependency).
|
|
60
|
+
* - Unconnected: no incoming and no outgoing edge.
|
|
61
|
+
* - Critical path: longest chain by node count across the DAG (ADR-4133).
|
|
62
|
+
* - Cycle members are excluded from the depth layout and surfaced as a
|
|
63
|
+
* finding by the toolbar (AC-204-6); non-cycle FBS still render.
|
|
64
|
+
*
|
|
65
|
+
* @param {object[]} fbsItems
|
|
66
|
+
* @param {Set<string>} [buildableIds] - the actionable set from computeQueue
|
|
67
|
+
* @returns {DagLayout}
|
|
68
|
+
*/
|
|
69
|
+
export function buildDagLayout(fbsItems, buildableIds) {
|
|
70
|
+
const items = Array.isArray(fbsItems) ? fbsItems : [];
|
|
71
|
+
const buildable = buildableIds instanceof Set ? buildableIds : new Set();
|
|
72
|
+
const byId = new Map();
|
|
73
|
+
for (const f of items) {
|
|
74
|
+
if (!f || typeof f.fbsId !== 'string') continue;
|
|
75
|
+
byId.set(f.fbsId, f);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const needsById = new Map();
|
|
79
|
+
const waitsById = new Map();
|
|
80
|
+
for (const f of items) {
|
|
81
|
+
const id = f.fbsId;
|
|
82
|
+
if (!id) continue;
|
|
83
|
+
const deps = Array.isArray(f.dependsOnFbsIds) ? f.dependsOnFbsIds.filter((d) => byId.has(d)) : [];
|
|
84
|
+
needsById.set(id, deps);
|
|
85
|
+
if (!waitsById.has(id)) waitsById.set(id, []);
|
|
86
|
+
for (const d of deps) {
|
|
87
|
+
if (!waitsById.has(d)) waitsById.set(d, []);
|
|
88
|
+
waitsById.get(d).push(id);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const cycleIds = findCycleIds(byId, needsById);
|
|
93
|
+
const cycleSet = new Set(cycleIds);
|
|
94
|
+
|
|
95
|
+
const depth = new Map();
|
|
96
|
+
function computeDepth(id, seen) {
|
|
97
|
+
if (cycleSet.has(id)) return 0;
|
|
98
|
+
if (depth.has(id)) return depth.get(id);
|
|
99
|
+
if (seen.has(id)) return 0;
|
|
100
|
+
seen.add(id);
|
|
101
|
+
const deps = needsById.get(id) ?? [];
|
|
102
|
+
let d = 0;
|
|
103
|
+
for (const dep of deps) {
|
|
104
|
+
if (cycleSet.has(dep)) continue;
|
|
105
|
+
d = Math.max(d, computeDepth(dep, seen) + 1);
|
|
106
|
+
}
|
|
107
|
+
seen.delete(id);
|
|
108
|
+
depth.set(id, d);
|
|
109
|
+
return d;
|
|
110
|
+
}
|
|
111
|
+
for (const id of byId.keys()) {
|
|
112
|
+
if (!cycleSet.has(id)) computeDepth(id, new Set());
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const upstream = new Map();
|
|
116
|
+
const downstream = new Map();
|
|
117
|
+
for (const id of byId.keys()) {
|
|
118
|
+
upstream.set(id, closure(id, needsById));
|
|
119
|
+
downstream.set(id, closure(id, waitsById));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const criticalPathIds = longestChain(byId, needsById, waitsById, cycleSet);
|
|
123
|
+
const criticalSet = new Set(criticalPathIds);
|
|
124
|
+
|
|
125
|
+
const unconnectedIds = [];
|
|
126
|
+
for (const id of byId.keys()) {
|
|
127
|
+
if (cycleSet.has(id)) continue;
|
|
128
|
+
const inDeg = (needsById.get(id) ?? []).length;
|
|
129
|
+
const outDeg = (waitsById.get(id) ?? []).length;
|
|
130
|
+
if (inDeg === 0 && outDeg === 0) unconnectedIds.push(id);
|
|
131
|
+
}
|
|
132
|
+
unconnectedIds.sort((a, b) => byBuildOrder(byId.get(a), byId.get(b)));
|
|
133
|
+
|
|
134
|
+
let maxDepth = 0;
|
|
135
|
+
for (const id of byId.keys()) {
|
|
136
|
+
if (cycleSet.has(id)) continue;
|
|
137
|
+
const inDeg = (needsById.get(id) ?? []).length;
|
|
138
|
+
const outDeg = (waitsById.get(id) ?? []).length;
|
|
139
|
+
if (inDeg === 0 && outDeg === 0) continue;
|
|
140
|
+
const d = depth.get(id) ?? 0;
|
|
141
|
+
if (d > maxDepth) maxDepth = d;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const columns = Array.from({ length: maxDepth + 1 }, () => []);
|
|
145
|
+
for (const id of byId.keys()) {
|
|
146
|
+
if (cycleSet.has(id)) continue;
|
|
147
|
+
const inDeg = (needsById.get(id) ?? []).length;
|
|
148
|
+
const outDeg = (waitsById.get(id) ?? []).length;
|
|
149
|
+
if (inDeg === 0 && outDeg === 0) continue;
|
|
150
|
+
const d = depth.get(id) ?? 0;
|
|
151
|
+
columns[d].push(id);
|
|
152
|
+
}
|
|
153
|
+
for (const col of columns) {
|
|
154
|
+
col.sort((a, b) => byBuildOrder(byId.get(a), byId.get(b)));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const nodes = new Map();
|
|
158
|
+
const unconnectedSet = new Set(unconnectedIds);
|
|
159
|
+
for (const [id, f] of byId.entries()) {
|
|
160
|
+
const node = {
|
|
161
|
+
id,
|
|
162
|
+
title: typeof f.title === 'string' ? f.title : '',
|
|
163
|
+
status: typeof f.executionStatus === 'string' ? f.executionStatus : '',
|
|
164
|
+
domain: typeof f.domain === 'string' ? f.domain : '',
|
|
165
|
+
size: typeof f.estimatedSize === 'string' ? f.estimatedSize : '',
|
|
166
|
+
buildOrder: typeof f.buildOrder === 'number' ? f.buildOrder : 0,
|
|
167
|
+
acCount: Array.isArray(f.acIds) ? f.acIds.length : 0,
|
|
168
|
+
depth: cycleSet.has(id) ? -1 : (depth.get(id) ?? 0),
|
|
169
|
+
needs: (needsById.get(id) ?? []).slice(),
|
|
170
|
+
waits: (waitsById.get(id) ?? []).slice(),
|
|
171
|
+
upstream: Array.from(upstream.get(id) ?? []).sort(idSort(byId)),
|
|
172
|
+
downstream: Array.from(downstream.get(id) ?? []).sort(idSort(byId)),
|
|
173
|
+
buildable: buildable.has(id),
|
|
174
|
+
critical: criticalSet.has(id),
|
|
175
|
+
unconnected: unconnectedSet.has(id),
|
|
176
|
+
};
|
|
177
|
+
nodes.set(id, node);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const edges = [];
|
|
181
|
+
for (const [to, deps] of needsById.entries()) {
|
|
182
|
+
for (const from of deps) {
|
|
183
|
+
edges.push({ from, to });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return { nodes, edges, columns, unconnectedIds, criticalPathIds, cycleIds, maxDepth };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function closure(start, edgesById) {
|
|
191
|
+
const out = new Set();
|
|
192
|
+
const stack = [start];
|
|
193
|
+
while (stack.length > 0) {
|
|
194
|
+
const cur = stack.pop();
|
|
195
|
+
const next = edgesById.get(cur) ?? [];
|
|
196
|
+
for (const n of next) {
|
|
197
|
+
if (out.has(n)) continue;
|
|
198
|
+
out.add(n);
|
|
199
|
+
stack.push(n);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function byBuildOrder(a, b) {
|
|
206
|
+
const ao = a?.buildOrder ?? 0;
|
|
207
|
+
const bo = b?.buildOrder ?? 0;
|
|
208
|
+
if (ao !== bo) return ao - bo;
|
|
209
|
+
return String(a?.fbsId ?? '').localeCompare(String(b?.fbsId ?? ''));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function idSort(byId) {
|
|
213
|
+
return (a, b) => byBuildOrder(byId.get(a), byId.get(b));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function findCycleIds(byId, needsById) {
|
|
217
|
+
const WHITE = 0;
|
|
218
|
+
const GREY = 1;
|
|
219
|
+
const BLACK = 2;
|
|
220
|
+
const colour = new Map();
|
|
221
|
+
const onStack = new Map();
|
|
222
|
+
const stack = [];
|
|
223
|
+
const result = new Set();
|
|
224
|
+
for (const id of byId.keys()) colour.set(id, WHITE);
|
|
225
|
+
for (const start of byId.keys()) {
|
|
226
|
+
if (colour.get(start) !== WHITE) continue;
|
|
227
|
+
const work = [{ id: start, i: 0 }];
|
|
228
|
+
colour.set(start, GREY);
|
|
229
|
+
onStack.set(start, true);
|
|
230
|
+
stack.push(start);
|
|
231
|
+
while (work.length > 0) {
|
|
232
|
+
const top = work[work.length - 1];
|
|
233
|
+
const deps = needsById.get(top.id) ?? [];
|
|
234
|
+
if (top.i >= deps.length) {
|
|
235
|
+
colour.set(top.id, BLACK);
|
|
236
|
+
onStack.delete(top.id);
|
|
237
|
+
stack.pop();
|
|
238
|
+
work.pop();
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const next = deps[top.i];
|
|
242
|
+
top.i += 1;
|
|
243
|
+
if (colour.get(next) === WHITE) {
|
|
244
|
+
colour.set(next, GREY);
|
|
245
|
+
onStack.set(next, true);
|
|
246
|
+
stack.push(next);
|
|
247
|
+
work.push({ id: next, i: 0 });
|
|
248
|
+
} else if (onStack.get(next)) {
|
|
249
|
+
for (let i = stack.length - 1; i >= 0; i -= 1) {
|
|
250
|
+
result.add(stack[i]);
|
|
251
|
+
if (stack[i] === next) break;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return Array.from(result).sort();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function longestChain(byId, needsById, waitsById, cycleSet) {
|
|
260
|
+
const memo = new Map();
|
|
261
|
+
function longest(id) {
|
|
262
|
+
if (memo.has(id)) return memo.get(id);
|
|
263
|
+
const waits = (waitsById.get(id) ?? []).filter((d) => !cycleSet.has(d));
|
|
264
|
+
let best = [id];
|
|
265
|
+
for (const n of waits) {
|
|
266
|
+
const chain = [id, ...longest(n)];
|
|
267
|
+
if (chain.length > best.length) best = chain;
|
|
268
|
+
}
|
|
269
|
+
memo.set(id, best);
|
|
270
|
+
return best;
|
|
271
|
+
}
|
|
272
|
+
let overall = [];
|
|
273
|
+
for (const id of byId.keys()) {
|
|
274
|
+
if (cycleSet.has(id)) continue;
|
|
275
|
+
const deps = needsById.get(id) ?? [];
|
|
276
|
+
if (deps.filter((d) => !cycleSet.has(d)).length !== 0) continue;
|
|
277
|
+
const chain = longest(id);
|
|
278
|
+
if (chain.length > overall.length) overall = chain;
|
|
279
|
+
}
|
|
280
|
+
return overall;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ------------------------------------------------------------------------
|
|
284
|
+
// HTML render
|
|
285
|
+
// ------------------------------------------------------------------------
|
|
286
|
+
|
|
287
|
+
const NODE_W = 220;
|
|
288
|
+
const NODE_H = 40;
|
|
289
|
+
const COL_GAP = 68;
|
|
290
|
+
const ROW_GAP = 10;
|
|
291
|
+
const PAD = 20;
|
|
292
|
+
const HEADER_H = 28;
|
|
293
|
+
const LANE_HEAD_H = 44;
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* @param {object} args
|
|
297
|
+
* @param {DagLayout} args.layout
|
|
298
|
+
* @param {Record<string, object>} [args.inspectorPayload] - rides on a
|
|
299
|
+
* `data-rcf-dag-inspector-data` attribute on the inspector shell (PR 1
|
|
300
|
+
* contract: component payload data on a `data-*` attribute, never an
|
|
301
|
+
* inline `<script>` body).
|
|
302
|
+
* @returns {string}
|
|
303
|
+
*/
|
|
304
|
+
export function renderBuildDag({ layout, inspectorPayload }) {
|
|
305
|
+
const toolbar = renderDagToolbar(layout);
|
|
306
|
+
const canvas = renderDagCanvas(layout);
|
|
307
|
+
const inspector = renderDagInspector(inspectorPayload);
|
|
308
|
+
const legend = renderDagLegend();
|
|
309
|
+
return `<div class="rcf-dag" data-rcf-dag="build">
|
|
310
|
+
${toolbar}
|
|
311
|
+
<div class="rcf-dag-grid">
|
|
312
|
+
<div class="rcf-dag-canvas-wrap card" data-rcf-dag-canvas-wrap>
|
|
313
|
+
${canvas}
|
|
314
|
+
</div>
|
|
315
|
+
<aside class="rcf-dag-inspector card" data-rcf-dag-inspector>
|
|
316
|
+
${inspector}
|
|
317
|
+
</aside>
|
|
318
|
+
</div>
|
|
319
|
+
${legend}
|
|
320
|
+
</div>`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function renderDagToolbar(layout) {
|
|
324
|
+
const uncCount = layout.unconnectedIds.length;
|
|
325
|
+
const critCount = layout.criticalPathIds.length;
|
|
326
|
+
const statusChips = ['notStarted', 'inProgress', 'complete', 'verified']
|
|
327
|
+
.map((s) => `<button type="button" class="rcf-dag-chip" data-rcf-dag-status="${escapeHtml(s)}" aria-pressed="true">${escapeHtml(statusLabel(s))}</button>`)
|
|
328
|
+
.join('');
|
|
329
|
+
const domainOptions = Array.from(new Set(
|
|
330
|
+
Array.from(layout.nodes.values()).map((n) => n.domain).filter((d) => typeof d === 'string' && d.length > 0),
|
|
331
|
+
)).sort();
|
|
332
|
+
const domainSelect = `<label class="rcf-dag-label"><span class="small muted">Area</span><select class="rcf-dag-select" data-rcf-dag-domain><option value="">All areas</option>${domainOptions.map((d) => `<option value="${escapeHtml(d)}">${escapeHtml(d)}</option>`).join('')}</select></label>`;
|
|
333
|
+
const toggleBuildable = `<button type="button" class="rcf-dag-chip" data-rcf-dag-toggle="buildable" aria-pressed="false">Buildable now</button>`;
|
|
334
|
+
const toggleCritical = `<button type="button" class="rcf-dag-chip" data-rcf-dag-toggle="critical" aria-pressed="false">Critical path <span class="rcf-badge rcf-badge--count">${critCount}</span></button>`;
|
|
335
|
+
const toggleUnconnected = `<button type="button" class="rcf-dag-chip" data-rcf-dag-toggle="unconnected" aria-pressed="false">Show unconnected <span class="rcf-badge rcf-badge--count">${uncCount}</span></button>`;
|
|
336
|
+
const findings = layout.cycleIds.length > 0
|
|
337
|
+
? findingsList({
|
|
338
|
+
heading: 'Dependency cycle',
|
|
339
|
+
count: layout.cycleIds.length,
|
|
340
|
+
items: layout.cycleIds.map((id) => ({ id, why: 'sits on a dependsOnFbsIds cycle; excluded from the depth layout', href: `#tab=build&sub=specs&entity=${id}` })),
|
|
341
|
+
})
|
|
342
|
+
: '';
|
|
343
|
+
return `<div class="rcf-dag-toolbar" data-rcf-dag-toolbar>
|
|
344
|
+
<div class="rcf-dag-toolbar-row">
|
|
345
|
+
<span class="small muted">Status</span>
|
|
346
|
+
${statusChips}
|
|
347
|
+
${domainSelect}
|
|
348
|
+
${toggleBuildable}
|
|
349
|
+
${toggleCritical}
|
|
350
|
+
${toggleUnconnected}
|
|
351
|
+
</div>
|
|
352
|
+
${findings ? `<div class="rcf-dag-toolbar-findings">${findings}</div>` : ''}
|
|
353
|
+
</div>`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function statusLabel(s) {
|
|
357
|
+
switch (s) {
|
|
358
|
+
case 'notStarted': return 'not started';
|
|
359
|
+
case 'inProgress': return 'in progress';
|
|
360
|
+
default: return s;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function renderDagCanvas(layout) {
|
|
365
|
+
const positions = layoutPositions(layout);
|
|
366
|
+
const connectedCols = layout.columns.length;
|
|
367
|
+
const connectedHeight = positions.connectedHeight;
|
|
368
|
+
const canvasW = Math.max(PAD * 2 + Math.max(1, connectedCols) * (NODE_W + COL_GAP), 900);
|
|
369
|
+
let canvasH = PAD + HEADER_H + connectedHeight + PAD;
|
|
370
|
+
|
|
371
|
+
const laneRows = [];
|
|
372
|
+
if (layout.unconnectedIds.length > 0) {
|
|
373
|
+
const perRow = Math.max(1, Math.floor((canvasW - PAD * 2) / (NODE_W + 12)));
|
|
374
|
+
const laneTop = canvasH + 8;
|
|
375
|
+
for (let i = 0; i < layout.unconnectedIds.length; i += 1) {
|
|
376
|
+
const id = layout.unconnectedIds[i];
|
|
377
|
+
const col = i % perRow;
|
|
378
|
+
const row = Math.floor(i / perRow);
|
|
379
|
+
positions.pos.set(id, {
|
|
380
|
+
x: PAD + col * (NODE_W + 12),
|
|
381
|
+
y: laneTop + LANE_HEAD_H + row * (NODE_H + ROW_GAP),
|
|
382
|
+
});
|
|
383
|
+
laneRows.push({ id, col, row });
|
|
384
|
+
}
|
|
385
|
+
const rows = Math.ceil(layout.unconnectedIds.length / perRow);
|
|
386
|
+
canvasH = laneTop + LANE_HEAD_H + rows * (NODE_H + ROW_GAP) + PAD;
|
|
387
|
+
positions.laneTop = laneTop;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const columnHeaders = layout.columns.map((_, ci) => {
|
|
391
|
+
const label = ci === 0 ? 'NO DEPENDENCIES' : `DEPTH ${ci}`;
|
|
392
|
+
return `<div class="rcf-dag-colhead" style="left:${PAD + ci * (NODE_W + COL_GAP)}px;top:${PAD}px;width:${NODE_W}px">${label}</div>`;
|
|
393
|
+
}).join('');
|
|
394
|
+
|
|
395
|
+
const laneHeader = layout.unconnectedIds.length > 0
|
|
396
|
+
? `<div class="rcf-dag-lane-head" data-rcf-dag-lane-head style="left:${PAD}px;top:${positions.laneTop}px;right:${PAD}px">UNCONNECTED (<span data-rcf-dag-lane-count>${layout.unconnectedIds.length}</span>): no declared dependency, nothing depends on them; ordered by buildOrder</div>`
|
|
397
|
+
: '';
|
|
398
|
+
|
|
399
|
+
const edgesSvg = renderEdges(layout, positions, canvasW, canvasH);
|
|
400
|
+
const nodesHtml = renderNodes(layout, positions);
|
|
401
|
+
|
|
402
|
+
const laneDataAttrs = layout.unconnectedIds.length > 0 ? ' data-has-unconnected="1"' : '';
|
|
403
|
+
|
|
404
|
+
return `<div class="rcf-dag-canvas" data-rcf-dag-canvas${laneDataAttrs} style="width:${canvasW}px;height:${canvasH}px">
|
|
405
|
+
${edgesSvg}
|
|
406
|
+
${columnHeaders}
|
|
407
|
+
${laneHeader}
|
|
408
|
+
${nodesHtml}
|
|
409
|
+
</div>`;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function layoutPositions(layout) {
|
|
413
|
+
const pos = new Map();
|
|
414
|
+
let maxRows = 0;
|
|
415
|
+
layout.columns.forEach((col, ci) => {
|
|
416
|
+
col.forEach((id, ri) => {
|
|
417
|
+
pos.set(id, {
|
|
418
|
+
x: PAD + ci * (NODE_W + COL_GAP),
|
|
419
|
+
y: PAD + HEADER_H + ri * (NODE_H + ROW_GAP),
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
if (col.length > maxRows) maxRows = col.length;
|
|
423
|
+
});
|
|
424
|
+
const connectedHeight = Math.max(0, maxRows * (NODE_H + ROW_GAP));
|
|
425
|
+
return { pos, maxRows, connectedHeight, laneTop: 0 };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function renderEdges(layout, positions, w, h) {
|
|
429
|
+
const defs = `<defs>
|
|
430
|
+
<marker id="rcf-dag-arrow" markerWidth="7" markerHeight="7" refX="6" refY="3.5" orient="auto">
|
|
431
|
+
<path d="M0,0 L7,3.5 L0,7 Z" fill="var(--rcf-dag-edge, currentColor)"/>
|
|
432
|
+
</marker>
|
|
433
|
+
<marker id="rcf-dag-arrow-up" markerWidth="7" markerHeight="7" refX="6" refY="3.5" orient="auto">
|
|
434
|
+
<path d="M0,0 L7,3.5 L0,7 Z" fill="var(--sv-link)"/>
|
|
435
|
+
</marker>
|
|
436
|
+
<marker id="rcf-dag-arrow-down" markerWidth="7" markerHeight="7" refX="6" refY="3.5" orient="auto">
|
|
437
|
+
<path d="M0,0 L7,3.5 L0,7 Z" fill="var(--sv-warning)"/>
|
|
438
|
+
</marker>
|
|
439
|
+
</defs>`;
|
|
440
|
+
const paths = layout.edges.map((e) => {
|
|
441
|
+
const a = positions.pos.get(e.from);
|
|
442
|
+
const b = positions.pos.get(e.to);
|
|
443
|
+
if (!a || !b) return '';
|
|
444
|
+
const x1 = a.x + NODE_W;
|
|
445
|
+
const y1 = a.y + NODE_H / 2;
|
|
446
|
+
const x2 = b.x;
|
|
447
|
+
const y2 = b.y + NODE_H / 2;
|
|
448
|
+
const c1x = x1 + COL_GAP / 2;
|
|
449
|
+
const c2x = x2 - COL_GAP / 2;
|
|
450
|
+
const d = `M${x1},${y1} C${c1x},${y1} ${c2x},${y2} ${x2},${y2}`;
|
|
451
|
+
return `<path class="rcf-dag-edge" d="${d}" data-rcf-dag-edge data-from="${escapeHtml(e.from)}" data-to="${escapeHtml(e.to)}" marker-end="url(#rcf-dag-arrow)"></path>`;
|
|
452
|
+
}).join('');
|
|
453
|
+
return `<svg class="rcf-dag-edges" width="${w}" height="${h}" aria-hidden="true" focusable="false">
|
|
454
|
+
${defs}
|
|
455
|
+
${paths}
|
|
456
|
+
</svg>`;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function renderNodes(layout, positions) {
|
|
460
|
+
const html = [];
|
|
461
|
+
for (const [id, node] of layout.nodes.entries()) {
|
|
462
|
+
const p = positions.pos.get(id);
|
|
463
|
+
if (!p) continue;
|
|
464
|
+
const dotClass = statusDot(node.status);
|
|
465
|
+
const title = `${node.id} ${node.title} (${node.status})`;
|
|
466
|
+
const buildableChip = node.buildable
|
|
467
|
+
? '<span class="rcf-dag-node-tag" title="No unmet dependencies">ready</span>'
|
|
468
|
+
: '';
|
|
469
|
+
html.push(
|
|
470
|
+
`<button type="button" class="rcf-dag-node"`
|
|
471
|
+
+ ` data-fbs-id="${escapeHtml(id)}"`
|
|
472
|
+
+ ` data-status="${escapeHtml(node.status)}"`
|
|
473
|
+
+ ` data-domain="${escapeHtml(node.domain)}"`
|
|
474
|
+
+ ` data-depth="${node.depth}"`
|
|
475
|
+
+ ` data-buildable="${node.buildable ? '1' : '0'}"`
|
|
476
|
+
+ ` data-critical="${node.critical ? '1' : '0'}"`
|
|
477
|
+
+ ` data-unconnected="${node.unconnected ? '1' : '0'}"`
|
|
478
|
+
+ ` style="left:${p.x}px;top:${p.y}px;width:${NODE_W}px;height:${NODE_H}px"`
|
|
479
|
+
+ ` title="${escapeHtml(title)}">`
|
|
480
|
+
+ `<span class="rcf-dag-node-dot ${dotClass}" aria-hidden="true"></span>`
|
|
481
|
+
+ `<span class="rcf-dag-node-id mono">${escapeHtml(id.replace(/^FBS-/, ''))}</span>`
|
|
482
|
+
+ `<span class="rcf-dag-node-title">${escapeHtml(node.title)}</span>`
|
|
483
|
+
+ buildableChip
|
|
484
|
+
+ `</button>`,
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
return html.join('\n ');
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function statusDot(status) {
|
|
491
|
+
if (status === 'verified' || status === 'complete') return 'rcf-dag-node-dot--ok';
|
|
492
|
+
if (status === 'inProgress') return 'rcf-dag-node-dot--warn';
|
|
493
|
+
return 'rcf-dag-node-dot--none';
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function renderDagInspector(payload) {
|
|
497
|
+
// PR 1 contract (standing criterion): component payload rides on a
|
|
498
|
+
// `data-*` attribute on this shell, never an inline `<script>` body.
|
|
499
|
+
// The client in page-init.js reads `data-rcf-dag-inspector-data`
|
|
500
|
+
// directly off this element. No outbound postMessage, no fetch.
|
|
501
|
+
const safeJson = JSON.stringify(payload || {}).replace(/</g, '\\u003c');
|
|
502
|
+
const attr = safeJson.replace(/&/g, '&').replace(/"/g, '"');
|
|
503
|
+
return `<div class="rcf-dag-inspector-body" data-rcf-dag-inspector-body data-rcf-dag-inspector-data="${attr}">
|
|
504
|
+
<div class="muted small">Select a build spec to see what it needs and what waits on it.</div>
|
|
505
|
+
</div>`;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function renderDagLegend() {
|
|
509
|
+
return `<p class="rcf-dag-legend small muted">`
|
|
510
|
+
+ `Columns are dependency depth (left has no declared dependency), computed from <code>dependsOnFbsIds</code>; row order inside a column is <code>buildOrder</code>. `
|
|
511
|
+
+ `Click a node: <span style="color:var(--sv-link)">upstream</span> outlines in link colour, <span style="color:var(--sv-warning)">downstream</span> in warning. `
|
|
512
|
+
+ `Specs with no dependency and no dependant sit in the unconnected lane so they do not swamp the graph.`
|
|
513
|
+
+ `</p>`;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Build an inspector payload the client can render after a selection
|
|
518
|
+
* without having to recompute the layout. One payload per selectable
|
|
519
|
+
* node (including unconnected); keyed by fbsId.
|
|
520
|
+
*
|
|
521
|
+
* The payload lives in a <script type="application/json"> tag beside
|
|
522
|
+
* the canvas so the DAG stays SSR-driven and the client wiring has no
|
|
523
|
+
* other data source.
|
|
524
|
+
*
|
|
525
|
+
* @param {DagLayout} layout
|
|
526
|
+
* @returns {Record<string, object>}
|
|
527
|
+
*/
|
|
528
|
+
export function buildDagInspectorPayload(layout) {
|
|
529
|
+
const out = {};
|
|
530
|
+
for (const [id, node] of layout.nodes.entries()) {
|
|
531
|
+
out[id] = {
|
|
532
|
+
id: node.id,
|
|
533
|
+
title: node.title,
|
|
534
|
+
status: node.status,
|
|
535
|
+
domain: node.domain,
|
|
536
|
+
size: node.size,
|
|
537
|
+
buildOrder: node.buildOrder,
|
|
538
|
+
acCount: node.acCount,
|
|
539
|
+
buildable: node.buildable,
|
|
540
|
+
critical: node.critical,
|
|
541
|
+
unconnected: node.unconnected,
|
|
542
|
+
needs: node.needs,
|
|
543
|
+
waits: node.waits,
|
|
544
|
+
needsTotal: node.upstream.length,
|
|
545
|
+
waitsTotal: node.downstream.length,
|
|
546
|
+
upstream: node.upstream,
|
|
547
|
+
downstream: node.downstream,
|
|
548
|
+
needsMeta: node.needs.map((depId) => inspectorMeta(layout, depId)),
|
|
549
|
+
waitsMeta: node.waits.map((depId) => inspectorMeta(layout, depId)),
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
return out;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function inspectorMeta(layout, id) {
|
|
556
|
+
const n = layout.nodes.get(id);
|
|
557
|
+
return {
|
|
558
|
+
id,
|
|
559
|
+
title: n?.title ?? '',
|
|
560
|
+
status: n?.status ?? '',
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Shared Badge component (viewer UI refresh PR 2, design doc section 4):
|
|
2
|
+
// a small, muted count or facet marker used in a DocRow's `meta` slot or
|
|
3
|
+
// anywhere else a numeric count / tag needs to ride beside a label. One
|
|
4
|
+
// markup shape, variant-driven styling:
|
|
5
|
+
//
|
|
6
|
+
// <span class="rcf-badge rcf-badge--{variant}" [title?]>
|
|
7
|
+
// [label] [value]
|
|
8
|
+
// </span>
|
|
9
|
+
//
|
|
10
|
+
// Variants (and intent):
|
|
11
|
+
// - count : plain muted count ("3 US", "7 AC"); the default
|
|
12
|
+
// - facet : neutral tag chip (status filter facets, area labels)
|
|
13
|
+
// - accent : primary / link-coloured accent (buildable-now, open tally)
|
|
14
|
+
//
|
|
15
|
+
// `label` is optional and renders muted-left-of value when both are
|
|
16
|
+
// present ("US 3" -> label="US", value=3). An explicit `title` surfaces
|
|
17
|
+
// as a tooltip; the Pill component (shared PR 0.29) still handles
|
|
18
|
+
// pill-shaped status rendering.
|
|
19
|
+
|
|
20
|
+
import { escapeHtml } from '../doc-renderers/helpers.js';
|
|
21
|
+
|
|
22
|
+
const VARIANTS = new Set(['count', 'facet', 'accent']);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {object} args
|
|
26
|
+
* @param {string|number} args.value
|
|
27
|
+
* @param {string} [args.label]
|
|
28
|
+
* @param {string} [args.variant] one of count | facet | accent (default: count)
|
|
29
|
+
* @param {string} [args.title]
|
|
30
|
+
* @returns {string}
|
|
31
|
+
*/
|
|
32
|
+
export function renderBadge({ value, label, variant = 'count', title }) {
|
|
33
|
+
const v = VARIANTS.has(variant) ? variant : 'count';
|
|
34
|
+
const classes = ['rcf-badge', `rcf-badge--${v}`];
|
|
35
|
+
const titleAttr = typeof title === 'string' && title.length > 0
|
|
36
|
+
? ` title="${escapeHtml(title)}"`
|
|
37
|
+
: '';
|
|
38
|
+
const labelHtml = label ? `<span class="rcf-badge-label">${escapeHtml(label)}</span>` : '';
|
|
39
|
+
const valueHtml = `<span class="rcf-badge-value">${escapeHtml(String(value ?? ''))}</span>`;
|
|
40
|
+
return `<span class="${classes.join(' ')}"${titleAttr}>${labelHtml}${valueHtml}</span>`;
|
|
41
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Shared diff component (REQ-181, TAC-4127-readiness-view). Renders a
|
|
2
|
+
// two-column before-and-after of two document bodies for the delta
|
|
3
|
+
// list on the Readiness tab. Document-level only in 0.29.0;
|
|
4
|
+
// criterion-level before-and-after is deferred per ADR-4127 (open
|
|
5
|
+
// question 7 of the DEFINE step 2 spec).
|
|
6
|
+
//
|
|
7
|
+
// Each side may be:
|
|
8
|
+
// - null / undefined renders an `(absent)` placeholder
|
|
9
|
+
// - a string renders as a <pre> body
|
|
10
|
+
// - an object `{ hash?, text?, note? }` renders an optional
|
|
11
|
+
// short-hash subtitle, then `text` as a <pre>
|
|
12
|
+
// body, OR `note` as a muted <p>, OR the
|
|
13
|
+
// `(absent)` placeholder when both are empty.
|
|
14
|
+
// The object form is what the Readiness tab's hash-only document-level
|
|
15
|
+
// diff passes in 0.29.0: the frozen body is not persisted in the freeze
|
|
16
|
+
// record, so the before column renders the frozen short-hash + a
|
|
17
|
+
// "frozen body not captured" note; the after column renders the
|
|
18
|
+
// current short-hash + the serialised current body.
|
|
19
|
+
|
|
20
|
+
import { escapeHtml } from '../doc-renderers/helpers.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {object} DiffSide
|
|
24
|
+
* @property {string} [hash] full-length hash (e.g. 'sha256:abc...'),
|
|
25
|
+
* rendered as the short form in a subtitle
|
|
26
|
+
* @property {string} [text] serialised body
|
|
27
|
+
* @property {string} [note] muted note shown when `text` is absent
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {string | DiffSide | null | undefined} before
|
|
32
|
+
* @param {string | DiffSide | null | undefined} after
|
|
33
|
+
* @returns {string}
|
|
34
|
+
*/
|
|
35
|
+
export function diff(before, after) {
|
|
36
|
+
return `<div class="rcf-diff">`
|
|
37
|
+
+ `<div class="rcf-diff__col rcf-diff__col--before"><h5>Before</h5>${renderSide(before)}</div>`
|
|
38
|
+
+ `<div class="rcf-diff__col rcf-diff__col--after"><h5>After</h5>${renderSide(after)}</div>`
|
|
39
|
+
+ `</div>`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function renderSide(side) {
|
|
43
|
+
if (typeof side === 'string') {
|
|
44
|
+
return side.length > 0
|
|
45
|
+
? `<pre class="rcf-diff__pre">${escapeHtml(side)}</pre>`
|
|
46
|
+
: `<p class="rcf-diff__absent"><em>(absent)</em></p>`;
|
|
47
|
+
}
|
|
48
|
+
if (side && typeof side === 'object') {
|
|
49
|
+
const parts = [];
|
|
50
|
+
if (typeof side.hash === 'string' && side.hash.length > 0) {
|
|
51
|
+
parts.push(`<p class="rcf-diff__hash"><code>${escapeHtml(shortHash(side.hash))}</code></p>`);
|
|
52
|
+
}
|
|
53
|
+
if (typeof side.text === 'string' && side.text.length > 0) {
|
|
54
|
+
parts.push(`<pre class="rcf-diff__pre">${escapeHtml(side.text)}</pre>`);
|
|
55
|
+
} else if (typeof side.note === 'string' && side.note.length > 0) {
|
|
56
|
+
parts.push(`<p class="rcf-diff__note"><em>${escapeHtml(side.note)}</em></p>`);
|
|
57
|
+
} else if (typeof side.hash !== 'string' || side.hash.length === 0) {
|
|
58
|
+
parts.push(`<p class="rcf-diff__absent"><em>(absent)</em></p>`);
|
|
59
|
+
}
|
|
60
|
+
return parts.join('');
|
|
61
|
+
}
|
|
62
|
+
return `<p class="rcf-diff__absent"><em>(absent)</em></p>`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function shortHash(hash) {
|
|
66
|
+
if (typeof hash !== 'string') return '';
|
|
67
|
+
const colonIdx = hash.indexOf(':');
|
|
68
|
+
const body = colonIdx >= 0 ? hash.slice(colonIdx + 1) : hash;
|
|
69
|
+
const short = body.slice(0, 7);
|
|
70
|
+
const prefix = colonIdx >= 0 ? `${hash.slice(0, colonIdx)}:` : '';
|
|
71
|
+
return `${prefix}${short}`;
|
|
72
|
+
}
|