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,574 @@
|
|
|
1
|
+
// Readiness compute (REQ-175; proposal 2026-09-22 §2.4, §6 v3).
|
|
2
|
+
//
|
|
3
|
+
// One pure composer over the DEFINE detection primitives:
|
|
4
|
+
// 1. `computeDelta` (slice 1) -> changed / added / removed / briefSince
|
|
5
|
+
// 2. `computeImpact` per id in changed union added -> fan-out
|
|
6
|
+
// 3. Eight gate check functions from `./gates.js` -> stage[]
|
|
7
|
+
// 4. `computeCoverage` tree-wide plus per REQ ancestor of the delta
|
|
8
|
+
// 5. Open decisions from the decisions ledger
|
|
9
|
+
// 6. Fold to `freezeable` (every stage passed / acknowledged /
|
|
10
|
+
// notApplicable) and `nextAction` (first failing stage + check)
|
|
11
|
+
//
|
|
12
|
+
// Return shape mirrors proposal §2.4 exactly. The CLI (rcf define
|
|
13
|
+
// readiness) and the viewer's Readiness tab (slice 5) render the same
|
|
14
|
+
// object; the freeze verb (slice 3) refuses on any failing stage.
|
|
15
|
+
|
|
16
|
+
import { computeQueue } from '../build/queue.js';
|
|
17
|
+
import { computeCoverage } from './coverage.js';
|
|
18
|
+
import { computeDelta } from './delta.js';
|
|
19
|
+
import { computeImpact } from './impact.js';
|
|
20
|
+
import {
|
|
21
|
+
STAGE_GATES,
|
|
22
|
+
STAGE_ORDER,
|
|
23
|
+
STAGE_SHORT_NAMES,
|
|
24
|
+
checkD1Brief,
|
|
25
|
+
checkD2Skeleton,
|
|
26
|
+
checkD3Shapes,
|
|
27
|
+
checkD4Stories,
|
|
28
|
+
checkD5Crosscut,
|
|
29
|
+
checkD6Consistency,
|
|
30
|
+
checkD7Decisions,
|
|
31
|
+
checkD8Freeze,
|
|
32
|
+
} from './gates.js';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @typedef {object} ReadinessTree
|
|
36
|
+
* @property {boolean} frozen
|
|
37
|
+
* @property {string | null} frozenAt
|
|
38
|
+
* @property {string | null} treeHash
|
|
39
|
+
* @property {string} currentTreeHash
|
|
40
|
+
* @property {string | null} buildAt fbsId at the queue head, or null
|
|
41
|
+
* @property {number} fbsTotal
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @typedef {object} ReadinessDelta
|
|
46
|
+
* @property {string[]} changed
|
|
47
|
+
* @property {string[]} added
|
|
48
|
+
* @property {string[]} removed
|
|
49
|
+
* @property {number[]} briefSince
|
|
50
|
+
* @property {import('./impact.js').ImpactNode[]} impacted
|
|
51
|
+
* @property {string[]} impactedFbs
|
|
52
|
+
* @property {Record<string, string>} currentDocHashes
|
|
53
|
+
* Live per-document hashes (ledgers keyed as `ledger:<name>`).
|
|
54
|
+
* Threaded from `computeDelta` so the Readiness tab can render a
|
|
55
|
+
* document-level hash diff for each changed / added id (spec §5
|
|
56
|
+
* block 6; the frozen body is not persisted in the freeze record
|
|
57
|
+
* in 0.29.0, so the diff is hash-only for now).
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @typedef {object} CheckResult
|
|
62
|
+
* @property {string} name
|
|
63
|
+
* @property {boolean} ok
|
|
64
|
+
* @property {'delta'|'tree'} over
|
|
65
|
+
* @property {number} pass
|
|
66
|
+
* @property {number} total
|
|
67
|
+
* @property {Array<{ id: string, why: string }>} failing
|
|
68
|
+
* @property {'productOwner'|'engineer'} persona
|
|
69
|
+
* @property {string} question
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @typedef {object} StageResult
|
|
74
|
+
* @property {string} stage
|
|
75
|
+
* @property {string} gate
|
|
76
|
+
* @property {'passed'|'failing'|'acknowledged'|'notApplicable'} state
|
|
77
|
+
* @property {CheckResult[]} checks
|
|
78
|
+
* @property {string} [reason]
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @typedef {object} Blocker
|
|
83
|
+
* @property {string} stage
|
|
84
|
+
* @property {string} gate
|
|
85
|
+
* @property {string} check
|
|
86
|
+
* @property {'productOwner'|'engineer'} persona
|
|
87
|
+
* @property {'delta'|'tree'} over
|
|
88
|
+
* @property {number} failingCount
|
|
89
|
+
* @property {string[]} ids
|
|
90
|
+
* @property {string} question
|
|
91
|
+
*/
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @typedef {object} LevelVerdict
|
|
95
|
+
* @property {boolean} ok
|
|
96
|
+
* @property {Blocker[]} blockedBy
|
|
97
|
+
* @property {NextAction | null} nextAction
|
|
98
|
+
*/
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @typedef {object} ReadinessLevels
|
|
102
|
+
* @property {LevelVerdict} intentComplete
|
|
103
|
+
* @property {LevelVerdict} readyToBuild
|
|
104
|
+
*/
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @typedef {object} PersonaGroup
|
|
108
|
+
* @property {Blocker[]} blockers
|
|
109
|
+
* @property {NextAction | null} nextAction
|
|
110
|
+
*/
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @typedef {object} ReadinessPersonas
|
|
114
|
+
* @property {PersonaGroup} productOwner
|
|
115
|
+
* @property {PersonaGroup} engineer
|
|
116
|
+
*/
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @typedef {object} NextAction
|
|
120
|
+
* @property {string} stage
|
|
121
|
+
* @property {string} check
|
|
122
|
+
* @property {string[]} ids
|
|
123
|
+
* @property {string} command
|
|
124
|
+
*/
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @typedef {object} ReadinessResult
|
|
128
|
+
* @property {ReadinessTree} tree
|
|
129
|
+
* @property {ReadinessDelta} delta
|
|
130
|
+
* @property {StageResult[]} stages
|
|
131
|
+
* @property {NextAction | null} nextAction
|
|
132
|
+
* @property {{ tree: import('./coverage.js').CoverageResult, delta: import('./coverage.js').CoverageResult[] }} coverage
|
|
133
|
+
* @property {Array<Record<string, unknown>>} decisions
|
|
134
|
+
* @property {boolean} freezeable
|
|
135
|
+
* @property {ReadinessLevels} levels
|
|
136
|
+
* @property {ReadinessPersonas} personas
|
|
137
|
+
*/
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Compute the readiness object.
|
|
141
|
+
*
|
|
142
|
+
* Pure over its inputs. Callers supply the ledgers bundle, freeze
|
|
143
|
+
* record, testPointers map (resolveTestPointers output) and optional
|
|
144
|
+
* profile switches; nothing else touches the filesystem.
|
|
145
|
+
*
|
|
146
|
+
* @param {import('#core/store/walker.js').TreeModel} tree
|
|
147
|
+
* @param {object} args
|
|
148
|
+
* @param {import('./delta.js').FreezeRecord | null | undefined} [args.freeze]
|
|
149
|
+
* @param {import('./delta.js').LedgerBundle | undefined} [args.ledgers]
|
|
150
|
+
* @param {{ skipReviewFor?: string } | undefined} [args.profile]
|
|
151
|
+
* @param {string | null | undefined} [args.profileText] rcf/.identity/profile.md contents
|
|
152
|
+
* @param {Map<string, unknown> | undefined} [args.testPointers]
|
|
153
|
+
* @param {Array<unknown> | undefined} [args.validateErrors] tree-wide validate + walker errors
|
|
154
|
+
* @returns {ReadinessResult}
|
|
155
|
+
*/
|
|
156
|
+
export function computeReadiness(tree, args = {}) {
|
|
157
|
+
const {
|
|
158
|
+
freeze = null,
|
|
159
|
+
ledgers = {},
|
|
160
|
+
profile = undefined,
|
|
161
|
+
profileText = null,
|
|
162
|
+
testPointers = undefined,
|
|
163
|
+
validateErrors = [],
|
|
164
|
+
} = args;
|
|
165
|
+
|
|
166
|
+
// 1. Delta.
|
|
167
|
+
const delta = computeDelta(tree, freeze, ledgers);
|
|
168
|
+
|
|
169
|
+
// 2. Fan-out via computeImpact per id in changed union added.
|
|
170
|
+
// Standalone-document ids only; ledger:<name> entries have no fan-out.
|
|
171
|
+
const pivotIds = [
|
|
172
|
+
...delta.changed.filter((id) => !id.startsWith('ledger:')),
|
|
173
|
+
...delta.added.filter((id) => !id.startsWith('ledger:')),
|
|
174
|
+
];
|
|
175
|
+
const pivotSet = new Set(pivotIds);
|
|
176
|
+
/** @type {import('./impact.js').ImpactNode[]} */
|
|
177
|
+
const impactedNodes = [];
|
|
178
|
+
const impactedSeen = new Set();
|
|
179
|
+
const impactedFbsSeen = new Set();
|
|
180
|
+
// Skip fan-out when the tree is unfrozen: the delta IS the whole tree
|
|
181
|
+
// (proposal §2.2 "before the first freeze the delta is the whole
|
|
182
|
+
// tree"), so a per-id fan-out is quadratic for no signal.
|
|
183
|
+
const runFanOut = delta.frozen === true;
|
|
184
|
+
if (runFanOut) {
|
|
185
|
+
for (const id of pivotIds) {
|
|
186
|
+
const impact = computeImpact(tree, { id });
|
|
187
|
+
if (!impact.found || !Array.isArray(impact.nodes)) continue;
|
|
188
|
+
for (const node of impact.nodes) {
|
|
189
|
+
if (node.role === 'pivot') continue;
|
|
190
|
+
if (pivotSet.has(node.id)) continue;
|
|
191
|
+
if (!impactedSeen.has(node.id)) {
|
|
192
|
+
impactedSeen.add(node.id);
|
|
193
|
+
impactedNodes.push(node);
|
|
194
|
+
}
|
|
195
|
+
if (node.kind === 'fbs' && node.actionNeeded === 're-execute') {
|
|
196
|
+
impactedFbsSeen.add(node.id);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const impactedFbs = [...impactedFbsSeen].sort();
|
|
202
|
+
|
|
203
|
+
// 3. Compose scope = changed union added union impacted (dedup, sort).
|
|
204
|
+
const scopeSet = new Set([
|
|
205
|
+
...delta.changed.filter((id) => !id.startsWith('ledger:')),
|
|
206
|
+
...delta.added.filter((id) => !id.startsWith('ledger:')),
|
|
207
|
+
...impactedSeen,
|
|
208
|
+
]);
|
|
209
|
+
|
|
210
|
+
// 4. Coverage: tree-wide plus per REQ ancestor of the delta.
|
|
211
|
+
const coverageTree = computeCoverage(tree, { testPointers, strict: true });
|
|
212
|
+
const deltaReqIds = collectDeltaReqAncestors(tree, pivotIds);
|
|
213
|
+
const coverageDelta = [];
|
|
214
|
+
for (const reqId of deltaReqIds) {
|
|
215
|
+
coverageDelta.push(computeCoverage(tree, { testPointers, strict: true, scopeId: reqId }));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 5. Run the eight gate check functions in order. D8 reads the
|
|
219
|
+
// prior stage states, so compose sequentially.
|
|
220
|
+
/** @type {StageResult[]} */
|
|
221
|
+
const stages = [];
|
|
222
|
+
const currentTreeHash = delta.currentTreeHash;
|
|
223
|
+
/** @type {import('./gates.js').StageContext} */
|
|
224
|
+
const ctx = {
|
|
225
|
+
tree,
|
|
226
|
+
ledgers,
|
|
227
|
+
delta,
|
|
228
|
+
freeze,
|
|
229
|
+
scope: scopeSet,
|
|
230
|
+
validateErrors,
|
|
231
|
+
// Prefer the caller-supplied probe count when it is passed; falls
|
|
232
|
+
// back to the ledger scan inside checkD6Consistency.
|
|
233
|
+
probeOpenCount: undefined,
|
|
234
|
+
coverageTree,
|
|
235
|
+
profileText,
|
|
236
|
+
profile,
|
|
237
|
+
currentTreeHash,
|
|
238
|
+
priorStages: undefined,
|
|
239
|
+
};
|
|
240
|
+
stages.push(checkD1Brief(ctx));
|
|
241
|
+
stages.push(checkD2Skeleton(ctx));
|
|
242
|
+
stages.push(checkD3Shapes(ctx));
|
|
243
|
+
stages.push(checkD4Stories(ctx));
|
|
244
|
+
stages.push(checkD5Crosscut(ctx));
|
|
245
|
+
stages.push(checkD6Consistency(ctx));
|
|
246
|
+
stages.push(checkD7Decisions(ctx));
|
|
247
|
+
stages.push(checkD8Freeze({ ...ctx, priorStages: stages.map((s) => ({ stage: s.stage, state: s.state })) }));
|
|
248
|
+
|
|
249
|
+
// 6. Open decisions in the #241 format for the surface consumer.
|
|
250
|
+
const decisions = /** @type {any} */ (ledgers?.decisions);
|
|
251
|
+
const decisionEntries = Array.isArray(decisions?.decisions) ? decisions.decisions : [];
|
|
252
|
+
const openDecisions = decisionEntries.filter((d) => d?.status === 'open');
|
|
253
|
+
|
|
254
|
+
// 7. Fold freezeable + nextAction.
|
|
255
|
+
const freezeable = stages.every((s) => s.state === 'passed' || s.state === 'acknowledged' || s.state === 'notApplicable');
|
|
256
|
+
const nextAction = freezeable ? null : deriveNextAction(stages);
|
|
257
|
+
|
|
258
|
+
// 7b. Fold `levels` and `personas` from stages[].checks[] alone
|
|
259
|
+
// (ADR-4126, proposal section 2.1). Nothing is written. L1
|
|
260
|
+
// intent-complete is "every productOwner check ok across every
|
|
261
|
+
// stage"; L2 ready-to-build equals freezeable with blockers over
|
|
262
|
+
// failing stages only.
|
|
263
|
+
const levels = deriveLevels(stages, freezeable, nextAction);
|
|
264
|
+
const personas = derivePersonas(stages, levels);
|
|
265
|
+
|
|
266
|
+
// 8. Build the tree summary line inputs.
|
|
267
|
+
const queue = computeQueue(tree);
|
|
268
|
+
const fbsTotal = (tree.fbsItems ?? []).length;
|
|
269
|
+
|
|
270
|
+
/** @type {ReadinessResult} */
|
|
271
|
+
return {
|
|
272
|
+
tree: {
|
|
273
|
+
frozen: delta.frozen,
|
|
274
|
+
frozenAt: delta.frozenAt,
|
|
275
|
+
treeHash: delta.treeHash,
|
|
276
|
+
currentTreeHash: delta.currentTreeHash,
|
|
277
|
+
buildAt: queue.nextActionable ?? null,
|
|
278
|
+
fbsTotal,
|
|
279
|
+
},
|
|
280
|
+
delta: {
|
|
281
|
+
changed: delta.changed,
|
|
282
|
+
added: delta.added,
|
|
283
|
+
removed: delta.removed,
|
|
284
|
+
briefSince: delta.briefSince,
|
|
285
|
+
impacted: impactedNodes,
|
|
286
|
+
impactedFbs,
|
|
287
|
+
currentDocHashes: delta.currentDocHashes ?? {},
|
|
288
|
+
},
|
|
289
|
+
stages,
|
|
290
|
+
nextAction,
|
|
291
|
+
coverage: { tree: coverageTree, delta: coverageDelta },
|
|
292
|
+
decisions: openDecisions,
|
|
293
|
+
freezeable,
|
|
294
|
+
levels,
|
|
295
|
+
personas,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Walk back from every delta pivot to its REQ ancestor and collect
|
|
301
|
+
* the unique REQ ids. The walker records parent-child edges under
|
|
302
|
+
* `parentByChild`; the walk terminates at the first REQ hit.
|
|
303
|
+
*
|
|
304
|
+
* @param {import('#core/store/walker.js').TreeModel} tree
|
|
305
|
+
* @param {string[]} pivotIds
|
|
306
|
+
* @returns {string[]}
|
|
307
|
+
*/
|
|
308
|
+
function collectDeltaReqAncestors(tree, pivotIds) {
|
|
309
|
+
const seen = new Set();
|
|
310
|
+
for (const id of pivotIds) {
|
|
311
|
+
let cursor = id;
|
|
312
|
+
let hops = 0;
|
|
313
|
+
while (cursor && hops < 32) {
|
|
314
|
+
hops += 1;
|
|
315
|
+
const kind = tree.kindById?.get(cursor);
|
|
316
|
+
if (kind === 'req') {
|
|
317
|
+
seen.add(cursor);
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
// Some kinds carry an explicit reqId; use that when the parent
|
|
321
|
+
// chain skips the REQ (e.g. a TC leaf whose parent is a TS).
|
|
322
|
+
const doc = tree.byId?.get(cursor);
|
|
323
|
+
if (doc && typeof doc === 'object' && typeof /** @type {any} */ (doc).reqId === 'string') {
|
|
324
|
+
seen.add(/** @type {any} */ (doc).reqId);
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
const parent = tree.parentByChild?.get(cursor);
|
|
328
|
+
if (!parent) break;
|
|
329
|
+
cursor = parent;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return [...seen].sort();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Pick the first failing stage in D1..D8 order and the first failing
|
|
337
|
+
* check inside it, then compose a `NextAction`. With no `persona`
|
|
338
|
+
* passed, behaves exactly as 0.28.4 did (first failing stage, first
|
|
339
|
+
* failing check, largest failing list). With a `persona` ('productOwner'
|
|
340
|
+
* | 'engineer'), picks the first failing stage that holds a failing
|
|
341
|
+
* check of that persona, then the largest failing-list check among
|
|
342
|
+
* them; the command carries `--level intent` on the productOwner
|
|
343
|
+
* variant and no `--level` flag otherwise (proposal section 2.1).
|
|
344
|
+
*
|
|
345
|
+
* The productOwner next action is defined over every stage holding a
|
|
346
|
+
* failing PO check (not just stages with state `failing`), because
|
|
347
|
+
* `levels.intentComplete.blockedBy` ignores stage state. In practice
|
|
348
|
+
* PO checks live on blocking stages (D1, D2, D4, D7) so any failing
|
|
349
|
+
* PO check also makes its stage `failing`, but the compute does not
|
|
350
|
+
* rely on that coincidence.
|
|
351
|
+
*
|
|
352
|
+
* @param {StageResult[]} stages
|
|
353
|
+
* @param {'productOwner' | 'engineer'} [persona]
|
|
354
|
+
* @returns {NextAction | null}
|
|
355
|
+
*/
|
|
356
|
+
export function deriveNextAction(stages, persona) {
|
|
357
|
+
const byStage = new Map(stages.map((s) => [s.stage, s]));
|
|
358
|
+
for (const stageId of STAGE_ORDER) {
|
|
359
|
+
const s = byStage.get(stageId);
|
|
360
|
+
if (!s) continue;
|
|
361
|
+
// Without a persona filter: today's rule (stage must be
|
|
362
|
+
// `failing`; this skips `acknowledged` warn-with-ack stages).
|
|
363
|
+
// With a persona filter: scan any stage that holds a failing
|
|
364
|
+
// check of that persona. PO checks only live on blocking stages
|
|
365
|
+
// so this coincides with "stage failing" in practice, but it
|
|
366
|
+
// keeps the compute honest if that ever changes.
|
|
367
|
+
if (!persona && s.state !== 'failing') continue;
|
|
368
|
+
const failing = s.checks.filter((c) => !c.ok && (persona ? c.persona === persona : true));
|
|
369
|
+
if (failing.length === 0) continue;
|
|
370
|
+
// First failing check; tie-break by the largest failing list so
|
|
371
|
+
// the operator sees the most impactful gap first.
|
|
372
|
+
const check = failing.reduce((a, b) => (b.failing.length > a.failing.length ? b : a), failing[0]);
|
|
373
|
+
const ids = [...new Set(check.failing.map((f) => f.id))].slice(0, 20);
|
|
374
|
+
const shortName = STAGE_SHORT_NAMES[stageId] ?? stageId;
|
|
375
|
+
const command = persona === 'productOwner'
|
|
376
|
+
? `rcf define readiness --level intent --check ${shortName}`
|
|
377
|
+
: `rcf define readiness --check ${shortName}`;
|
|
378
|
+
return {
|
|
379
|
+
stage: stageId,
|
|
380
|
+
check: check.name,
|
|
381
|
+
ids,
|
|
382
|
+
command,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Build a `Blocker` row from a stage + check pair. `ids` are
|
|
390
|
+
* deduplicated and capped at 20 to match the display convention
|
|
391
|
+
* `nextAction.ids` uses (proposal section 2.1).
|
|
392
|
+
*
|
|
393
|
+
* @param {StageResult} stage
|
|
394
|
+
* @param {CheckResult} check
|
|
395
|
+
* @returns {Blocker}
|
|
396
|
+
*/
|
|
397
|
+
function makeBlocker(stage, check) {
|
|
398
|
+
return {
|
|
399
|
+
stage: stage.stage,
|
|
400
|
+
gate: stage.gate,
|
|
401
|
+
check: check.name,
|
|
402
|
+
persona: check.persona,
|
|
403
|
+
over: check.over,
|
|
404
|
+
failingCount: check.failing.length,
|
|
405
|
+
ids: [...new Set(check.failing.map((f) => f.id))].slice(0, 20),
|
|
406
|
+
question: check.question,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Fold the two levels from `stages[].checks[]` (ADR-4126, proposal
|
|
412
|
+
* section 2.1). L1 ignores stage state and lists every failing PO
|
|
413
|
+
* check; L2 lists every failing check in a stage whose state is
|
|
414
|
+
* `failing` (checks inside an `acknowledged` stage are not blockers).
|
|
415
|
+
*
|
|
416
|
+
* @param {StageResult[]} stages
|
|
417
|
+
* @param {boolean} freezeable
|
|
418
|
+
* @param {NextAction | null} nextAction
|
|
419
|
+
* @returns {ReadinessLevels}
|
|
420
|
+
*/
|
|
421
|
+
export function deriveLevels(stages, freezeable, nextAction) {
|
|
422
|
+
/** @type {Blocker[]} */
|
|
423
|
+
const poBlockers = [];
|
|
424
|
+
/** @type {Blocker[]} */
|
|
425
|
+
const buildBlockers = [];
|
|
426
|
+
for (const stage of stages) {
|
|
427
|
+
for (const check of stage.checks) {
|
|
428
|
+
if (check.ok) continue;
|
|
429
|
+
if (check.persona === 'productOwner') {
|
|
430
|
+
poBlockers.push(makeBlocker(stage, check));
|
|
431
|
+
}
|
|
432
|
+
if (stage.state === 'failing') {
|
|
433
|
+
buildBlockers.push(makeBlocker(stage, check));
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
const intentOk = poBlockers.length === 0;
|
|
438
|
+
return {
|
|
439
|
+
intentComplete: {
|
|
440
|
+
ok: intentOk,
|
|
441
|
+
blockedBy: poBlockers,
|
|
442
|
+
nextAction: intentOk ? null : deriveNextAction(stages, 'productOwner'),
|
|
443
|
+
},
|
|
444
|
+
readyToBuild: {
|
|
445
|
+
ok: freezeable,
|
|
446
|
+
blockedBy: buildBlockers,
|
|
447
|
+
nextAction,
|
|
448
|
+
},
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Fold the `personas` block (ADR-4126, proposal section 3.3).
|
|
454
|
+
* `productOwner.blockers` deep-equals `levels.intentComplete.blockedBy`;
|
|
455
|
+
* `engineer.blockers` is the failing engineer checks in failing stages
|
|
456
|
+
* (the L2 blockedBy minus the PO entries).
|
|
457
|
+
*
|
|
458
|
+
* @param {StageResult[]} stages
|
|
459
|
+
* @param {ReadinessLevels} levels
|
|
460
|
+
* @returns {ReadinessPersonas}
|
|
461
|
+
*/
|
|
462
|
+
export function derivePersonas(stages, levels) {
|
|
463
|
+
const engineerBlockers = levels.readyToBuild.blockedBy.filter((b) => b.persona === 'engineer');
|
|
464
|
+
return {
|
|
465
|
+
productOwner: {
|
|
466
|
+
blockers: levels.intentComplete.blockedBy,
|
|
467
|
+
nextAction: levels.intentComplete.nextAction,
|
|
468
|
+
},
|
|
469
|
+
engineer: {
|
|
470
|
+
blockers: engineerBlockers,
|
|
471
|
+
nextAction: deriveNextAction(stages, 'engineer'),
|
|
472
|
+
},
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Validate a `--level` flag value against the closed set
|
|
478
|
+
* `intent | build`. Returns the value, `null` when the flag was
|
|
479
|
+
* absent, or throws a usage error whose message is formatted exactly
|
|
480
|
+
* like the `--check` usage line so the CLI can forward it verbatim.
|
|
481
|
+
*
|
|
482
|
+
* @param {string | undefined} raw
|
|
483
|
+
* @returns {'intent' | 'build' | null}
|
|
484
|
+
*/
|
|
485
|
+
export function parseLevelFlag(raw) {
|
|
486
|
+
if (raw === undefined || raw === null) return null;
|
|
487
|
+
if (raw === 'intent' || raw === 'build') return raw;
|
|
488
|
+
throw new Error(`unknown --level ${raw} (expected intent | build)`);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Validate a `--persona` flag value against the closed set
|
|
493
|
+
* `productOwner | engineer`. Returns the value, `null` when the flag
|
|
494
|
+
* was absent, or throws a usage error with the same shape.
|
|
495
|
+
*
|
|
496
|
+
* @param {string | undefined} raw
|
|
497
|
+
* @returns {'productOwner' | 'engineer' | null}
|
|
498
|
+
*/
|
|
499
|
+
export function parsePersonaFlag(raw) {
|
|
500
|
+
if (raw === undefined || raw === null) return null;
|
|
501
|
+
if (raw === 'productOwner' || raw === 'engineer') return raw;
|
|
502
|
+
throw new Error(`unknown --persona ${raw} (expected productOwner | engineer)`);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Format the two verdict lines shared by the CLI text report and
|
|
507
|
+
* the Readiness tab (proposal section 2.3). The strings are built
|
|
508
|
+
* from `levels` alone so the two surfaces cannot disagree.
|
|
509
|
+
*
|
|
510
|
+
* @param {ReadinessResult} result
|
|
511
|
+
* @returns {{ intentComplete: string, readyToBuild: string }}
|
|
512
|
+
*/
|
|
513
|
+
export function formatVerdictLines(result) {
|
|
514
|
+
const l = result.levels;
|
|
515
|
+
const intentLine = l.intentComplete.ok
|
|
516
|
+
? 'Intent-complete: yes. Every product-owner question is answered.'
|
|
517
|
+
: (() => {
|
|
518
|
+
const n = l.intentComplete.blockedBy.length;
|
|
519
|
+
const detail = l.intentComplete.blockedBy
|
|
520
|
+
.map((b) => `${b.stage}/${b.check}`)
|
|
521
|
+
.join(', ');
|
|
522
|
+
return `Intent-complete: no; ${n} question${n === 1 ? '' : 's'} for the product owner (${detail}).`;
|
|
523
|
+
})();
|
|
524
|
+
const buildLine = l.readyToBuild.ok
|
|
525
|
+
? `Ready-to-build: yes. Freezeable at ${shortHash(result.tree.currentTreeHash)}.`
|
|
526
|
+
: (() => {
|
|
527
|
+
const blockers = l.readyToBuild.blockedBy;
|
|
528
|
+
const stages = [...new Set(blockers.map((b) => b.stage))].sort();
|
|
529
|
+
const total = blockers.length;
|
|
530
|
+
const p = blockers.filter((b) => b.persona === 'productOwner').length;
|
|
531
|
+
const e = blockers.filter((b) => b.persona === 'engineer').length;
|
|
532
|
+
return `Ready-to-build: no; blocked on ${stages.join(', ')} (${total} check${total === 1 ? '' : 's'}: ${p} product owner, ${e} engineer).`;
|
|
533
|
+
})();
|
|
534
|
+
return { intentComplete: intentLine, readyToBuild: buildLine };
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Short 8-character prefix of a `sha256:<hex>` string, or '(none)' when
|
|
539
|
+
* absent. Kept exported so text formatters share one convention.
|
|
540
|
+
*
|
|
541
|
+
* @param {string | null | undefined} hash
|
|
542
|
+
* @returns {string}
|
|
543
|
+
*/
|
|
544
|
+
export function shortHash(hash) {
|
|
545
|
+
if (typeof hash !== 'string' || !hash.startsWith('sha256:')) return '(none)';
|
|
546
|
+
return hash.slice('sha256:'.length, 'sha256:'.length + 8);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* One-line tree summary matching proposal §6.2:
|
|
551
|
+
* "Frozen at 9f3a2c on 2026-09-20. 4 documents changed since, 11
|
|
552
|
+
* impacted. Build at FBS-031 of 41; 2 FBS impacted."
|
|
553
|
+
* "Unfrozen. 30 documents; the whole tree is the delta."
|
|
554
|
+
*
|
|
555
|
+
* @param {ReadinessResult} result
|
|
556
|
+
* @returns {string}
|
|
557
|
+
*/
|
|
558
|
+
export function formatTreeLine(result) {
|
|
559
|
+
const t = result.tree;
|
|
560
|
+
const d = result.delta;
|
|
561
|
+
if (!t.frozen) {
|
|
562
|
+
const total = d.added.length + d.changed.length;
|
|
563
|
+
return `Unfrozen. ${total} document${total === 1 ? '' : 's'}; the whole tree is the delta.`;
|
|
564
|
+
}
|
|
565
|
+
const changedCount = d.changed.length + d.added.length + d.removed.length;
|
|
566
|
+
const impactedCount = d.impacted.length;
|
|
567
|
+
const buildAt = t.buildAt ?? '(none)';
|
|
568
|
+
const impactedFbs = d.impactedFbs.length;
|
|
569
|
+
const at = t.frozenAt ? t.frozenAt.split('T')[0] : '(no timestamp)';
|
|
570
|
+
return `Frozen at ${shortHash(t.treeHash)} on ${at}. ${changedCount} document${changedCount === 1 ? '' : 's'} changed since, ${impactedCount} impacted. Build at ${buildAt} of ${t.fbsTotal}; ${impactedFbs} FBS impacted.`;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Re-export for CLI convenience.
|
|
574
|
+
export { STAGE_GATES, STAGE_ORDER, STAGE_SHORT_NAMES };
|
package/src/server/index.js
CHANGED
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
// on top; the server itself does not `process.exit`.
|
|
12
12
|
|
|
13
13
|
import { createServer } from 'node:http';
|
|
14
|
+
import { readFile } from 'node:fs/promises';
|
|
14
15
|
import { join } from 'node:path';
|
|
15
16
|
|
|
16
|
-
import { LIVE_CLIENT_PATH, STYLE_CSS_PATH, VENDORED_MERMAID_PATH, renderModelToPage } from '../view/index.js';
|
|
17
|
+
import { LIVE_CLIENT_PATH, PAGE_INIT_PATH, STYLE_CSS_PATH, TEST_HOST_CLIENT_PATH, VENDORED_MERMAID_PATH, renderModelToPage } from '../view/index.js';
|
|
17
18
|
import { watch as defaultWatch } from '../watch/index.js';
|
|
18
19
|
import { createRouter } from './routes.js';
|
|
19
20
|
import { createScopeHandler } from './scope-endpoint.js';
|
|
@@ -28,6 +29,10 @@ import { createSseHub } from './sse.js';
|
|
|
28
29
|
* @property {number} [debounceMs=50]
|
|
29
30
|
* @property {(line: string) => void} [log] - stderr sink
|
|
30
31
|
* @property {typeof defaultWatch} [watchImpl] - injectable watch primitive for tests
|
|
32
|
+
* @property {boolean} [testHost=false] - viewer UI refresh PR 9 (TAC-4134,
|
|
33
|
+
* ADR-4136): when true, mount the wespa host fixture at /test-host.html
|
|
34
|
+
* plus its embed-client script at /test-host.js. Off by default so
|
|
35
|
+
* wespa's proxy allow-list is unaffected.
|
|
31
36
|
*/
|
|
32
37
|
|
|
33
38
|
/**
|
|
@@ -42,6 +47,25 @@ import { createSseHub } from './sse.js';
|
|
|
42
47
|
* rewalk: () => Promise<void>,
|
|
43
48
|
* }>}
|
|
44
49
|
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Issue #248 (0.28.3): read a shipped static asset once and return
|
|
53
|
+
* `{ buffer, size, contentType }`. Any read failure surfaces here so
|
|
54
|
+
* `startServer` can fail fast with an ioFailure before it binds.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} path
|
|
57
|
+
* @param {string} contentType
|
|
58
|
+
*/
|
|
59
|
+
async function loadStaticAsset(path, contentType) {
|
|
60
|
+
// Codex P2-1: derive size from the actual captured buffer, not from
|
|
61
|
+
// a parallel stat that could snapshot a different revision if the
|
|
62
|
+
// file is being replaced concurrently. A wrong content-length header
|
|
63
|
+
// would cause truncation or mis-alignment for the entire server
|
|
64
|
+
// lifetime because we cache both values together.
|
|
65
|
+
const buffer = await readFile(path);
|
|
66
|
+
return { buffer, size: buffer.length, contentType };
|
|
67
|
+
}
|
|
68
|
+
|
|
45
69
|
export async function startServer(args) {
|
|
46
70
|
const projectRoot = args.projectRoot;
|
|
47
71
|
const port = typeof args.port === 'number' ? args.port : 4373;
|
|
@@ -50,6 +74,7 @@ export async function startServer(args) {
|
|
|
50
74
|
const debounceMs = typeof args.debounceMs === 'number' ? args.debounceMs : 50;
|
|
51
75
|
const log = typeof args.log === 'function' ? args.log : () => {};
|
|
52
76
|
const watchImpl = typeof args.watchImpl === 'function' ? args.watchImpl : defaultWatch;
|
|
77
|
+
const testHost = args.testHost === true;
|
|
53
78
|
|
|
54
79
|
/** @type {{ version: number, fullPageHtml: string, contentHtml: string, errors: import('#core/errors').RcfError[] } | null} */
|
|
55
80
|
let state = null;
|
|
@@ -76,6 +101,12 @@ export async function startServer(args) {
|
|
|
76
101
|
fullPageHtml: result.fullPageHtml,
|
|
77
102
|
contentHtml: result.contentHtml,
|
|
78
103
|
errors: result.errors,
|
|
104
|
+
pmPartials: result.pmPartials,
|
|
105
|
+
// Viewer UI refresh PR 7 (TAC-4132, ADR-4134): the ID lookup
|
|
106
|
+
// index is built once per rewalk and snapshotted on `state`
|
|
107
|
+
// so `GET /index.json` serves from memory and the SSE
|
|
108
|
+
// `tree-update` version lets the client invalidate its cache.
|
|
109
|
+
indexJson: result.indexJson,
|
|
79
110
|
};
|
|
80
111
|
sse.broadcast('tree-update', { version, contentHtml: result.contentHtml });
|
|
81
112
|
if (result.errors && result.errors.length > 0) {
|
|
@@ -97,14 +128,42 @@ export async function startServer(args) {
|
|
|
97
128
|
// and the first SSE connect gets a real payload.
|
|
98
129
|
await rewalk();
|
|
99
130
|
|
|
131
|
+
// Issue #248 (0.28.3): snapshot the shipped static assets at startup
|
|
132
|
+
// so a checkout branch switch during the server's lifetime cannot
|
|
133
|
+
// change what gets served. The tree walker is the only path that
|
|
134
|
+
// re-reads disk on rcf/ change; every other asset is fixed at boot.
|
|
135
|
+
const [styleAsset, mermaidAsset, liveClientAsset, pageInitAsset, testHostClientAsset] = await Promise.all([
|
|
136
|
+
loadStaticAsset(STYLE_CSS_PATH, 'text/css; charset=utf-8'),
|
|
137
|
+
loadStaticAsset(VENDORED_MERMAID_PATH, 'application/javascript; charset=utf-8'),
|
|
138
|
+
loadStaticAsset(LIVE_CLIENT_PATH, 'application/javascript; charset=utf-8'),
|
|
139
|
+
loadStaticAsset(PAGE_INIT_PATH, 'application/javascript; charset=utf-8'),
|
|
140
|
+
// Viewer UI refresh PR 9 (TAC-4134): the wespa host fixture client
|
|
141
|
+
// only loads when testHost is on. Keeping the read behind the gate
|
|
142
|
+
// means a startup under default options never touches the file.
|
|
143
|
+
testHost
|
|
144
|
+
? loadStaticAsset(TEST_HOST_CLIENT_PATH, 'application/javascript; charset=utf-8')
|
|
145
|
+
: Promise.resolve(null),
|
|
146
|
+
]);
|
|
147
|
+
|
|
100
148
|
const scopeHandler = createScopeHandler({ projectRoot });
|
|
101
149
|
const router = createRouter({
|
|
102
150
|
currentState: () => state,
|
|
103
151
|
sse,
|
|
152
|
+
// Snapshotted assets take precedence; the legacy path props are
|
|
153
|
+
// still passed so tests that mount the router directly with a
|
|
154
|
+
// path (no asset load) keep working.
|
|
155
|
+
styleAsset,
|
|
156
|
+
mermaidAsset,
|
|
157
|
+
liveClientAsset,
|
|
158
|
+
pageInitAsset,
|
|
104
159
|
stylePath: STYLE_CSS_PATH,
|
|
105
160
|
mermaidPath: VENDORED_MERMAID_PATH,
|
|
106
161
|
liveClientPath: LIVE_CLIENT_PATH,
|
|
162
|
+
pageInitPath: PAGE_INIT_PATH,
|
|
107
163
|
scope: scopeHandler,
|
|
164
|
+
testHost,
|
|
165
|
+
testHostClientAsset,
|
|
166
|
+
testHostClientPath: testHost ? TEST_HOST_CLIENT_PATH : undefined,
|
|
108
167
|
});
|
|
109
168
|
|
|
110
169
|
const server = createServer(router);
|