rcf-lite 0.28.1 → 0.28.4
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 +310 -0
- package/bin/rcf.js +4 -0
- package/fixtures/canary-manifest.json +9 -9
- 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/code-nodes/cn-602.json +3 -2
- 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 +21 -0
- package/rcf/code-nodes/cn-726.json +17 -0
- package/rcf/code-nodes/cn-727.json +14 -0
- package/rcf/code-nodes/cn-728.json +21 -0
- package/rcf/code-nodes/cn-729.json +18 -0
- package/rcf/code-nodes/cn-730.json +14 -0
- package/rcf/fbs/fbs-181.json +3 -2
- 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 +37 -0
- package/rcf/fbs/fbs-197.json +41 -0
- package/rcf/requirements/req-001.json +2 -1
- package/rcf/requirements/req-002.json +2 -1
- 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 +25 -0
- package/rcf/requirements/req-175.json +25 -0
- package/rcf/tacs/tac-4103-feedback-redact.json +2 -2
- package/rcf/tacs/tac-4120-define-delta-and-freeze.json +21 -0
- package/rcf/tacs/tac-4121-define-ledgers.json +23 -0
- package/rcf/tacs/tac-4122-define-gates.json +25 -0
- package/rcf/tacs/tac-4123-define-readiness.json +25 -0
- package/rcf/test-suites/ts-188.json +10 -2
- package/rcf/test-suites/ts-191.json +19 -5
- 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 +67 -0
- package/rcf/test-suites/ts-216.json +78 -0
- package/rcf/test-suites/ts-217.json +62 -0
- package/rcf/user-stories/us-15601.json +10 -1
- package/rcf/user-stories/us-15701.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-17401.json +58 -0
- package/rcf/user-stories/us-17501.json +48 -0
- package/releases/releases.yaml +31 -1
- package/src/browser-verify/manifest-writer.js +9 -5
- package/src/cli/browser-verify.js +9 -1
- package/src/cli/feedback.js +316 -32
- package/src/cli/help.js +11 -0
- package/src/cli/intake.js +25 -4
- package/src/cli/ledger.js +399 -0
- package/src/cli/preflight.js +24 -3
- package/src/cli/readiness.js +280 -0
- package/src/cli/review.js +9 -1
- package/src/cli/ui-baseline.js +14 -1
- package/src/core/store/index.js +5 -0
- package/src/core/store/validate-composed-record.js +95 -0
- package/src/core/store/writer.js +86 -6
- package/src/define/freeze-record.js +232 -0
- package/src/define/ledgers.js +513 -0
- package/src/feedback/gh.js +27 -1
- package/src/feedback/redact-allowlist.json +7 -0
- package/src/feedback/redact.js +126 -11
- package/src/feedback/render.js +34 -12
- package/src/intake/manifest-writer.js +6 -3
- package/src/intake/orchestrator.js +44 -7
- package/src/preflight/manifest-writer.js +11 -11
- package/src/query/delta.js +276 -0
- package/src/query/gates.js +832 -0
- package/src/query/index.js +36 -0
- package/src/query/readiness.js +333 -0
- package/src/req-baseline/sweep.js +34 -5
- package/src/server/index.js +37 -0
- package/src/server/routes.js +65 -3
- package/src/ui-baseline/manifest-writer.js +11 -10
- package/src/view/doc-renderers/helpers.js +39 -2
- package/src/view/doc-renderers/req.js +10 -2
- package/src/view/doc-renderers/tad.js +67 -6
- package/src/view/doc-renderers/user-story.js +10 -5
- package/src/view/html-page.js +347 -28
- package/src/view/index.js +14 -1
- package/src/view/live-client.js +22 -2
- package/src/view/product-map.js +751 -0
- package/src/view/style.css +383 -0
package/src/query/index.js
CHANGED
|
@@ -4,6 +4,42 @@
|
|
|
4
4
|
export { computeCoverage, classifyCoverageScope } from './coverage.js';
|
|
5
5
|
export { computeTrace, kindOf } from './trace.js';
|
|
6
6
|
export { computeImpact, labelFor } from './impact.js';
|
|
7
|
+
// Detection model (REQ-172, proposal 2026-09-22 §2.2 v3). Pure hashes
|
|
8
|
+
// over the parsed tree; every downstream DEFINE gate reads this delta.
|
|
9
|
+
export {
|
|
10
|
+
canonicaliseJson,
|
|
11
|
+
hashDocument,
|
|
12
|
+
computeTreeHash,
|
|
13
|
+
computeDelta,
|
|
14
|
+
} from './delta.js';
|
|
15
|
+
// DEFINE stage gates (REQ-174; proposal §2.4, §3.2 v3). One check
|
|
16
|
+
// function per stage; foldState + parseAcClass helpers.
|
|
17
|
+
export {
|
|
18
|
+
STAGE_ORDER,
|
|
19
|
+
STAGE_GATES,
|
|
20
|
+
STAGE_SHORT_NAMES,
|
|
21
|
+
STAGE_ALIASES,
|
|
22
|
+
INTERFACE_KINDS,
|
|
23
|
+
stagePolicy,
|
|
24
|
+
parseAcClass,
|
|
25
|
+
foldState,
|
|
26
|
+
runStage,
|
|
27
|
+
checkD1Brief,
|
|
28
|
+
checkD2Skeleton,
|
|
29
|
+
checkD3Shapes,
|
|
30
|
+
checkD4Stories,
|
|
31
|
+
checkD5Crosscut,
|
|
32
|
+
checkD6Consistency,
|
|
33
|
+
checkD7Decisions,
|
|
34
|
+
checkD8Freeze,
|
|
35
|
+
} from './gates.js';
|
|
36
|
+
// Readiness composer (REQ-175; proposal §2.4, §6 v3).
|
|
37
|
+
export {
|
|
38
|
+
computeReadiness,
|
|
39
|
+
deriveNextAction,
|
|
40
|
+
shortHash,
|
|
41
|
+
formatTreeLine,
|
|
42
|
+
} from './readiness.js';
|
|
7
43
|
export { formatTable } from './formatters/table.js';
|
|
8
44
|
export { formatJson } from './formatters/json.js';
|
|
9
45
|
export { formatMermaid } from './formatters/mermaid.js';
|
|
@@ -0,0 +1,333 @@
|
|
|
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
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @typedef {object} StageResult
|
|
56
|
+
* @property {string} stage
|
|
57
|
+
* @property {string} gate
|
|
58
|
+
* @property {'passed'|'failing'|'acknowledged'|'notApplicable'} state
|
|
59
|
+
* @property {Array<{ name: string, ok: boolean, over: 'delta'|'tree', pass: number, total: number, failing: Array<{ id: string, why: string }> }>} checks
|
|
60
|
+
* @property {string} [reason]
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @typedef {object} NextAction
|
|
65
|
+
* @property {string} stage
|
|
66
|
+
* @property {string} check
|
|
67
|
+
* @property {string[]} ids
|
|
68
|
+
* @property {string} command
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @typedef {object} ReadinessResult
|
|
73
|
+
* @property {ReadinessTree} tree
|
|
74
|
+
* @property {ReadinessDelta} delta
|
|
75
|
+
* @property {StageResult[]} stages
|
|
76
|
+
* @property {NextAction | null} nextAction
|
|
77
|
+
* @property {{ tree: import('./coverage.js').CoverageResult, delta: import('./coverage.js').CoverageResult[] }} coverage
|
|
78
|
+
* @property {Array<Record<string, unknown>>} decisions
|
|
79
|
+
* @property {boolean} freezeable
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Compute the readiness object.
|
|
84
|
+
*
|
|
85
|
+
* Pure over its inputs. Callers supply the ledgers bundle, freeze
|
|
86
|
+
* record, testPointers map (resolveTestPointers output) and optional
|
|
87
|
+
* profile switches; nothing else touches the filesystem.
|
|
88
|
+
*
|
|
89
|
+
* @param {import('#core/store/walker.js').TreeModel} tree
|
|
90
|
+
* @param {object} args
|
|
91
|
+
* @param {import('./delta.js').FreezeRecord | null | undefined} [args.freeze]
|
|
92
|
+
* @param {import('./delta.js').LedgerBundle | undefined} [args.ledgers]
|
|
93
|
+
* @param {{ skipReviewFor?: string } | undefined} [args.profile]
|
|
94
|
+
* @param {string | null | undefined} [args.profileText] rcf/.identity/profile.md contents
|
|
95
|
+
* @param {Map<string, unknown> | undefined} [args.testPointers]
|
|
96
|
+
* @param {Array<unknown> | undefined} [args.validateErrors] tree-wide validate + walker errors
|
|
97
|
+
* @returns {ReadinessResult}
|
|
98
|
+
*/
|
|
99
|
+
export function computeReadiness(tree, args = {}) {
|
|
100
|
+
const {
|
|
101
|
+
freeze = null,
|
|
102
|
+
ledgers = {},
|
|
103
|
+
profile = undefined,
|
|
104
|
+
profileText = null,
|
|
105
|
+
testPointers = undefined,
|
|
106
|
+
validateErrors = [],
|
|
107
|
+
} = args;
|
|
108
|
+
|
|
109
|
+
// 1. Delta.
|
|
110
|
+
const delta = computeDelta(tree, freeze, ledgers);
|
|
111
|
+
|
|
112
|
+
// 2. Fan-out via computeImpact per id in changed union added.
|
|
113
|
+
// Standalone-document ids only; ledger:<name> entries have no fan-out.
|
|
114
|
+
const pivotIds = [
|
|
115
|
+
...delta.changed.filter((id) => !id.startsWith('ledger:')),
|
|
116
|
+
...delta.added.filter((id) => !id.startsWith('ledger:')),
|
|
117
|
+
];
|
|
118
|
+
const pivotSet = new Set(pivotIds);
|
|
119
|
+
/** @type {import('./impact.js').ImpactNode[]} */
|
|
120
|
+
const impactedNodes = [];
|
|
121
|
+
const impactedSeen = new Set();
|
|
122
|
+
const impactedFbsSeen = new Set();
|
|
123
|
+
// Skip fan-out when the tree is unfrozen: the delta IS the whole tree
|
|
124
|
+
// (proposal §2.2 "before the first freeze the delta is the whole
|
|
125
|
+
// tree"), so a per-id fan-out is quadratic for no signal.
|
|
126
|
+
const runFanOut = delta.frozen === true;
|
|
127
|
+
if (runFanOut) {
|
|
128
|
+
for (const id of pivotIds) {
|
|
129
|
+
const impact = computeImpact(tree, { id });
|
|
130
|
+
if (!impact.found || !Array.isArray(impact.nodes)) continue;
|
|
131
|
+
for (const node of impact.nodes) {
|
|
132
|
+
if (node.role === 'pivot') continue;
|
|
133
|
+
if (pivotSet.has(node.id)) continue;
|
|
134
|
+
if (!impactedSeen.has(node.id)) {
|
|
135
|
+
impactedSeen.add(node.id);
|
|
136
|
+
impactedNodes.push(node);
|
|
137
|
+
}
|
|
138
|
+
if (node.kind === 'fbs' && node.actionNeeded === 're-execute') {
|
|
139
|
+
impactedFbsSeen.add(node.id);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const impactedFbs = [...impactedFbsSeen].sort();
|
|
145
|
+
|
|
146
|
+
// 3. Compose scope = changed union added union impacted (dedup, sort).
|
|
147
|
+
const scopeSet = new Set([
|
|
148
|
+
...delta.changed.filter((id) => !id.startsWith('ledger:')),
|
|
149
|
+
...delta.added.filter((id) => !id.startsWith('ledger:')),
|
|
150
|
+
...impactedSeen,
|
|
151
|
+
]);
|
|
152
|
+
|
|
153
|
+
// 4. Coverage: tree-wide plus per REQ ancestor of the delta.
|
|
154
|
+
const coverageTree = computeCoverage(tree, { testPointers, strict: true });
|
|
155
|
+
const deltaReqIds = collectDeltaReqAncestors(tree, pivotIds);
|
|
156
|
+
const coverageDelta = [];
|
|
157
|
+
for (const reqId of deltaReqIds) {
|
|
158
|
+
coverageDelta.push(computeCoverage(tree, { testPointers, strict: true, scopeId: reqId }));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 5. Run the eight gate check functions in order. D8 reads the
|
|
162
|
+
// prior stage states, so compose sequentially.
|
|
163
|
+
/** @type {StageResult[]} */
|
|
164
|
+
const stages = [];
|
|
165
|
+
const currentTreeHash = delta.currentTreeHash;
|
|
166
|
+
/** @type {import('./gates.js').StageContext} */
|
|
167
|
+
const ctx = {
|
|
168
|
+
tree,
|
|
169
|
+
ledgers,
|
|
170
|
+
delta,
|
|
171
|
+
freeze,
|
|
172
|
+
scope: scopeSet,
|
|
173
|
+
validateErrors,
|
|
174
|
+
// Prefer the caller-supplied probe count when it is passed; falls
|
|
175
|
+
// back to the ledger scan inside checkD6Consistency.
|
|
176
|
+
probeOpenCount: undefined,
|
|
177
|
+
coverageTree,
|
|
178
|
+
profileText,
|
|
179
|
+
profile,
|
|
180
|
+
currentTreeHash,
|
|
181
|
+
priorStages: undefined,
|
|
182
|
+
};
|
|
183
|
+
stages.push(checkD1Brief(ctx));
|
|
184
|
+
stages.push(checkD2Skeleton(ctx));
|
|
185
|
+
stages.push(checkD3Shapes(ctx));
|
|
186
|
+
stages.push(checkD4Stories(ctx));
|
|
187
|
+
stages.push(checkD5Crosscut(ctx));
|
|
188
|
+
stages.push(checkD6Consistency(ctx));
|
|
189
|
+
stages.push(checkD7Decisions(ctx));
|
|
190
|
+
stages.push(checkD8Freeze({ ...ctx, priorStages: stages.map((s) => ({ stage: s.stage, state: s.state })) }));
|
|
191
|
+
|
|
192
|
+
// 6. Open decisions in the #241 format for the surface consumer.
|
|
193
|
+
const decisions = /** @type {any} */ (ledgers?.decisions);
|
|
194
|
+
const decisionEntries = Array.isArray(decisions?.decisions) ? decisions.decisions : [];
|
|
195
|
+
const openDecisions = decisionEntries.filter((d) => d?.status === 'open');
|
|
196
|
+
|
|
197
|
+
// 7. Fold freezeable + nextAction.
|
|
198
|
+
const freezeable = stages.every((s) => s.state === 'passed' || s.state === 'acknowledged' || s.state === 'notApplicable');
|
|
199
|
+
const nextAction = freezeable ? null : deriveNextAction(stages);
|
|
200
|
+
|
|
201
|
+
// 8. Build the tree summary line inputs.
|
|
202
|
+
const queue = computeQueue(tree);
|
|
203
|
+
const fbsTotal = (tree.fbsItems ?? []).length;
|
|
204
|
+
|
|
205
|
+
/** @type {ReadinessResult} */
|
|
206
|
+
return {
|
|
207
|
+
tree: {
|
|
208
|
+
frozen: delta.frozen,
|
|
209
|
+
frozenAt: delta.frozenAt,
|
|
210
|
+
treeHash: delta.treeHash,
|
|
211
|
+
currentTreeHash: delta.currentTreeHash,
|
|
212
|
+
buildAt: queue.nextActionable ?? null,
|
|
213
|
+
fbsTotal,
|
|
214
|
+
},
|
|
215
|
+
delta: {
|
|
216
|
+
changed: delta.changed,
|
|
217
|
+
added: delta.added,
|
|
218
|
+
removed: delta.removed,
|
|
219
|
+
briefSince: delta.briefSince,
|
|
220
|
+
impacted: impactedNodes,
|
|
221
|
+
impactedFbs,
|
|
222
|
+
},
|
|
223
|
+
stages,
|
|
224
|
+
nextAction,
|
|
225
|
+
coverage: { tree: coverageTree, delta: coverageDelta },
|
|
226
|
+
decisions: openDecisions,
|
|
227
|
+
freezeable,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Walk back from every delta pivot to its REQ ancestor and collect
|
|
233
|
+
* the unique REQ ids. The walker records parent-child edges under
|
|
234
|
+
* `parentByChild`; the walk terminates at the first REQ hit.
|
|
235
|
+
*
|
|
236
|
+
* @param {import('#core/store/walker.js').TreeModel} tree
|
|
237
|
+
* @param {string[]} pivotIds
|
|
238
|
+
* @returns {string[]}
|
|
239
|
+
*/
|
|
240
|
+
function collectDeltaReqAncestors(tree, pivotIds) {
|
|
241
|
+
const seen = new Set();
|
|
242
|
+
for (const id of pivotIds) {
|
|
243
|
+
let cursor = id;
|
|
244
|
+
let hops = 0;
|
|
245
|
+
while (cursor && hops < 32) {
|
|
246
|
+
hops += 1;
|
|
247
|
+
const kind = tree.kindById?.get(cursor);
|
|
248
|
+
if (kind === 'req') {
|
|
249
|
+
seen.add(cursor);
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
// Some kinds carry an explicit reqId; use that when the parent
|
|
253
|
+
// chain skips the REQ (e.g. a TC leaf whose parent is a TS).
|
|
254
|
+
const doc = tree.byId?.get(cursor);
|
|
255
|
+
if (doc && typeof doc === 'object' && typeof /** @type {any} */ (doc).reqId === 'string') {
|
|
256
|
+
seen.add(/** @type {any} */ (doc).reqId);
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
const parent = tree.parentByChild?.get(cursor);
|
|
260
|
+
if (!parent) break;
|
|
261
|
+
cursor = parent;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return [...seen].sort();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Pick the first failing stage in D1..D8 order and the first failing
|
|
269
|
+
* check inside it, then compose a `NextAction`.
|
|
270
|
+
*
|
|
271
|
+
* @param {StageResult[]} stages
|
|
272
|
+
* @returns {NextAction | null}
|
|
273
|
+
*/
|
|
274
|
+
export function deriveNextAction(stages) {
|
|
275
|
+
const byStage = new Map(stages.map((s) => [s.stage, s]));
|
|
276
|
+
for (const stageId of STAGE_ORDER) {
|
|
277
|
+
const s = byStage.get(stageId);
|
|
278
|
+
if (!s || s.state !== 'failing') continue;
|
|
279
|
+
// First failing check; tie-break by the largest failing list so
|
|
280
|
+
// the operator sees the most impactful gap first.
|
|
281
|
+
const failing = s.checks.filter((c) => !c.ok);
|
|
282
|
+
if (failing.length === 0) continue;
|
|
283
|
+
const check = failing.reduce((a, b) => (b.failing.length > a.failing.length ? b : a), failing[0]);
|
|
284
|
+
const ids = [...new Set(check.failing.map((f) => f.id))].slice(0, 20);
|
|
285
|
+
const shortName = STAGE_SHORT_NAMES[stageId] ?? stageId;
|
|
286
|
+
return {
|
|
287
|
+
stage: stageId,
|
|
288
|
+
check: check.name,
|
|
289
|
+
ids,
|
|
290
|
+
command: `rcf define readiness --check ${shortName}`,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Short 8-character prefix of a `sha256:<hex>` string, or '(none)' when
|
|
298
|
+
* absent. Kept exported so text formatters share one convention.
|
|
299
|
+
*
|
|
300
|
+
* @param {string | null | undefined} hash
|
|
301
|
+
* @returns {string}
|
|
302
|
+
*/
|
|
303
|
+
export function shortHash(hash) {
|
|
304
|
+
if (typeof hash !== 'string' || !hash.startsWith('sha256:')) return '(none)';
|
|
305
|
+
return hash.slice('sha256:'.length, 'sha256:'.length + 8);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* One-line tree summary matching proposal §6.2:
|
|
310
|
+
* "Frozen at 9f3a2c on 2026-09-20. 4 documents changed since, 11
|
|
311
|
+
* impacted. Build at FBS-031 of 41; 2 FBS impacted."
|
|
312
|
+
* "Unfrozen. 30 documents; the whole tree is the delta."
|
|
313
|
+
*
|
|
314
|
+
* @param {ReadinessResult} result
|
|
315
|
+
* @returns {string}
|
|
316
|
+
*/
|
|
317
|
+
export function formatTreeLine(result) {
|
|
318
|
+
const t = result.tree;
|
|
319
|
+
const d = result.delta;
|
|
320
|
+
if (!t.frozen) {
|
|
321
|
+
const total = d.added.length + d.changed.length;
|
|
322
|
+
return `Unfrozen. ${total} document${total === 1 ? '' : 's'}; the whole tree is the delta.`;
|
|
323
|
+
}
|
|
324
|
+
const changedCount = d.changed.length + d.added.length + d.removed.length;
|
|
325
|
+
const impactedCount = d.impacted.length;
|
|
326
|
+
const buildAt = t.buildAt ?? '(none)';
|
|
327
|
+
const impactedFbs = d.impactedFbs.length;
|
|
328
|
+
const at = t.frozenAt ? t.frozenAt.split('T')[0] : '(no timestamp)';
|
|
329
|
+
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.`;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Re-export for CLI convenience.
|
|
333
|
+
export { STAGE_GATES, STAGE_ORDER, STAGE_SHORT_NAMES };
|
|
@@ -87,10 +87,20 @@ export async function applySweepDecisions({ projectRoot, tree, decisions, now =
|
|
|
87
87
|
const usDoc = tree.byId.get(usId);
|
|
88
88
|
if (!usDoc) continue;
|
|
89
89
|
|
|
90
|
+
// 0.28.2 (issue #231): a per-US allocator seeded ONCE from the
|
|
91
|
+
// current maxN on `usDoc.acceptanceCriteria`, then handed to each
|
|
92
|
+
// `composeBaselineAc` for the run. `nextAcId` was stateless
|
|
93
|
+
// across iterations, so every candidate accepted for one US
|
|
94
|
+
// received the identical `AC-<usNum>-<maxN+1>` and the writer's
|
|
95
|
+
// post-write validation refused the tree with a duplicate-id
|
|
96
|
+
// breakage. Ordinal allocation is monotone within a batch and
|
|
97
|
+
// across batches (a later sweep on the same US resumes from the
|
|
98
|
+
// new maxN in the tree).
|
|
99
|
+
const allocator = makeAcIdAllocator(usDoc);
|
|
90
100
|
const acsToAppend = [];
|
|
91
101
|
for (const d of list) {
|
|
92
102
|
if (d.action === 'accept') {
|
|
93
|
-
const ac = composeBaselineAc({ usDoc, candidate: d.candidate, now });
|
|
103
|
+
const ac = composeBaselineAc({ usDoc, candidate: d.candidate, now, idOverride: allocator() });
|
|
94
104
|
acsToAppend.push(ac);
|
|
95
105
|
writtenAcIds.push(ac.id);
|
|
96
106
|
accepted += 1;
|
|
@@ -172,9 +182,9 @@ export async function applyPendingBaselinesForUs({ tree, usId }) {
|
|
|
172
182
|
* @param {Date} args.now
|
|
173
183
|
* @returns {object}
|
|
174
184
|
*/
|
|
175
|
-
export function composeBaselineAc({ usDoc, candidate, now }) {
|
|
185
|
+
export function composeBaselineAc({ usDoc, candidate, now, idOverride = null }) {
|
|
176
186
|
const isoNow = now.toISOString();
|
|
177
|
-
const id = nextAcId(usDoc);
|
|
187
|
+
const id = typeof idOverride === 'string' && idOverride.length > 0 ? idOverride : nextAcId(usDoc);
|
|
178
188
|
const canonicalText = candidate.canonicalText;
|
|
179
189
|
const gwt = candidate.given || candidate.when || candidate.then
|
|
180
190
|
? { given: candidate.given ?? '', when: candidate.when ?? '', then: candidate.then ?? '' }
|
|
@@ -200,8 +210,22 @@ export function composeBaselineAc({ usDoc, candidate, now }) {
|
|
|
200
210
|
const AC_HIER_ID_RE = /^AC-(\d{3,})-(\d+)$/;
|
|
201
211
|
|
|
202
212
|
function nextAcId(usDoc) {
|
|
213
|
+
const alloc = makeAcIdAllocator(usDoc);
|
|
214
|
+
return alloc();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* 0.28.2 (issue #231): return a monotone AC-id allocator seeded once
|
|
219
|
+
* from `usDoc.acceptanceCriteria`. Every subsequent call returns
|
|
220
|
+
* `AC-<usNum>-<n++>`; a caller that accepts N baseline candidates for
|
|
221
|
+
* one US in one sweep now gets N distinct ids, and the writer's
|
|
222
|
+
* duplicate-id refusal on `updateDocument` no longer bites.
|
|
223
|
+
*
|
|
224
|
+
* @param {object} usDoc
|
|
225
|
+
* @returns {() => string}
|
|
226
|
+
*/
|
|
227
|
+
export function makeAcIdAllocator(usDoc) {
|
|
203
228
|
const acs = Array.isArray(usDoc.acceptanceCriteria) ? usDoc.acceptanceCriteria : [];
|
|
204
|
-
// Prefer hierarchical form matching the US id, e.g. US-201 → AC-201-1.
|
|
205
229
|
const usNum = (usDoc.usId ?? '').match(/^US-(\d+)$/)?.[1];
|
|
206
230
|
let maxN = 0;
|
|
207
231
|
for (const ac of acs) {
|
|
@@ -212,7 +236,12 @@ function nextAcId(usDoc) {
|
|
|
212
236
|
const n = Number.parseInt(tail, 10);
|
|
213
237
|
if (Number.isFinite(n) && n > maxN) maxN = n;
|
|
214
238
|
}
|
|
215
|
-
|
|
239
|
+
let counter = maxN;
|
|
240
|
+
return () => {
|
|
241
|
+
counter += 1;
|
|
242
|
+
if (!usNum) return `AC-${(Math.floor(Math.random() * 999)).toString().padStart(3, '0')}-${counter}`;
|
|
243
|
+
return `AC-${usNum}-${counter}`;
|
|
244
|
+
};
|
|
216
245
|
}
|
|
217
246
|
|
|
218
247
|
function extractGwt(text) {
|
package/src/server/index.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
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
17
|
import { LIVE_CLIENT_PATH, STYLE_CSS_PATH, VENDORED_MERMAID_PATH, renderModelToPage } from '../view/index.js';
|
|
@@ -42,6 +43,25 @@ import { createSseHub } from './sse.js';
|
|
|
42
43
|
* rewalk: () => Promise<void>,
|
|
43
44
|
* }>}
|
|
44
45
|
*/
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Issue #248 (0.28.3): read a shipped static asset once and return
|
|
49
|
+
* `{ buffer, size, contentType }`. Any read failure surfaces here so
|
|
50
|
+
* `startServer` can fail fast with an ioFailure before it binds.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} path
|
|
53
|
+
* @param {string} contentType
|
|
54
|
+
*/
|
|
55
|
+
async function loadStaticAsset(path, contentType) {
|
|
56
|
+
// Codex P2-1: derive size from the actual captured buffer, not from
|
|
57
|
+
// a parallel stat that could snapshot a different revision if the
|
|
58
|
+
// file is being replaced concurrently. A wrong content-length header
|
|
59
|
+
// would cause truncation or mis-alignment for the entire server
|
|
60
|
+
// lifetime because we cache both values together.
|
|
61
|
+
const buffer = await readFile(path);
|
|
62
|
+
return { buffer, size: buffer.length, contentType };
|
|
63
|
+
}
|
|
64
|
+
|
|
45
65
|
export async function startServer(args) {
|
|
46
66
|
const projectRoot = args.projectRoot;
|
|
47
67
|
const port = typeof args.port === 'number' ? args.port : 4373;
|
|
@@ -76,6 +96,7 @@ export async function startServer(args) {
|
|
|
76
96
|
fullPageHtml: result.fullPageHtml,
|
|
77
97
|
contentHtml: result.contentHtml,
|
|
78
98
|
errors: result.errors,
|
|
99
|
+
pmPartials: result.pmPartials,
|
|
79
100
|
};
|
|
80
101
|
sse.broadcast('tree-update', { version, contentHtml: result.contentHtml });
|
|
81
102
|
if (result.errors && result.errors.length > 0) {
|
|
@@ -97,10 +118,26 @@ export async function startServer(args) {
|
|
|
97
118
|
// and the first SSE connect gets a real payload.
|
|
98
119
|
await rewalk();
|
|
99
120
|
|
|
121
|
+
// Issue #248 (0.28.3): snapshot the shipped static assets at startup
|
|
122
|
+
// so a checkout branch switch during the server's lifetime cannot
|
|
123
|
+
// change what gets served. The tree walker is the only path that
|
|
124
|
+
// re-reads disk on rcf/ change; every other asset is fixed at boot.
|
|
125
|
+
const [styleAsset, mermaidAsset, liveClientAsset] = await Promise.all([
|
|
126
|
+
loadStaticAsset(STYLE_CSS_PATH, 'text/css; charset=utf-8'),
|
|
127
|
+
loadStaticAsset(VENDORED_MERMAID_PATH, 'application/javascript; charset=utf-8'),
|
|
128
|
+
loadStaticAsset(LIVE_CLIENT_PATH, 'application/javascript; charset=utf-8'),
|
|
129
|
+
]);
|
|
130
|
+
|
|
100
131
|
const scopeHandler = createScopeHandler({ projectRoot });
|
|
101
132
|
const router = createRouter({
|
|
102
133
|
currentState: () => state,
|
|
103
134
|
sse,
|
|
135
|
+
// Snapshotted assets take precedence; the legacy path props are
|
|
136
|
+
// still passed so tests that mount the router directly with a
|
|
137
|
+
// path (no asset load) keep working.
|
|
138
|
+
styleAsset,
|
|
139
|
+
mermaidAsset,
|
|
140
|
+
liveClientAsset,
|
|
104
141
|
stylePath: STYLE_CSS_PATH,
|
|
105
142
|
mermaidPath: VENDORED_MERMAID_PATH,
|
|
106
143
|
liveClientPath: LIVE_CLIENT_PATH,
|
package/src/server/routes.js
CHANGED
|
@@ -12,6 +12,22 @@
|
|
|
12
12
|
|
|
13
13
|
import { readFile, stat } from 'node:fs/promises';
|
|
14
14
|
|
|
15
|
+
// Issue #248 (0.28.3): the shipped static assets (style.css,
|
|
16
|
+
// mermaid.min.js, live-client.js) MUST be resolved once at server
|
|
17
|
+
// startup, not read from disk on every HTTP request. If the checkout
|
|
18
|
+
// switches branches or the file mutates while the server runs, per-
|
|
19
|
+
// request reads return the new bytes even though the HTML that
|
|
20
|
+
// references them was rendered from a walk taken at startup. The
|
|
21
|
+
// asymmetry produced the 2026-09-22 Product Map "unstyled preview"
|
|
22
|
+
// regression: HTML from the store walker was old, style.css was
|
|
23
|
+
// current, and the browser combined them.
|
|
24
|
+
//
|
|
25
|
+
// `deps.styleAsset`, `deps.mermaidAsset` and `deps.liveClientAsset`
|
|
26
|
+
// carry `{ buffer, size, contentType }` snapshotted at startup. The
|
|
27
|
+
// legacy `stylePath`/`mermaidPath`/`liveClientPath` props are kept
|
|
28
|
+
// for backwards compatibility with tests that pass a path; the
|
|
29
|
+
// router reads on-demand in that fallback path.
|
|
30
|
+
|
|
15
31
|
const MIME = {
|
|
16
32
|
html: 'text/html; charset=utf-8',
|
|
17
33
|
css: 'text/css; charset=utf-8',
|
|
@@ -68,15 +84,33 @@ export function createRouter(deps) {
|
|
|
68
84
|
return;
|
|
69
85
|
}
|
|
70
86
|
if (path === '/style.css') {
|
|
71
|
-
|
|
87
|
+
serveAsset(res, deps.styleAsset, deps.stylePath, MIME.css).catch((err) => fail(res, err));
|
|
72
88
|
return;
|
|
73
89
|
}
|
|
74
90
|
if (path === '/mermaid.min.js') {
|
|
75
|
-
|
|
91
|
+
serveAsset(res, deps.mermaidAsset, deps.mermaidPath, MIME.js).catch((err) => fail(res, err));
|
|
76
92
|
return;
|
|
77
93
|
}
|
|
78
94
|
if (path === '/live-client.js') {
|
|
79
|
-
|
|
95
|
+
serveAsset(res, deps.liveClientAsset, deps.liveClientPath, MIME.js).catch((err) => fail(res, err));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (path.startsWith('/product-map/')) {
|
|
99
|
+
const state = deps.currentState();
|
|
100
|
+
if (!state || !state.pmPartials) {
|
|
101
|
+
res.writeHead(503, { 'content-type': MIME.txt });
|
|
102
|
+
res.end('view server initialising\n');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const group = path.slice('/product-map/'.length);
|
|
106
|
+
const html = state.pmPartials[group];
|
|
107
|
+
if (typeof html !== 'string') {
|
|
108
|
+
res.writeHead(404, { 'content-type': MIME.txt });
|
|
109
|
+
res.end('unknown grouping\n');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
res.writeHead(200, { 'content-type': MIME.html, 'cache-control': 'no-store' });
|
|
113
|
+
res.end(html);
|
|
80
114
|
return;
|
|
81
115
|
}
|
|
82
116
|
if (path === '/scope.json') {
|
|
@@ -94,6 +128,34 @@ export function createRouter(deps) {
|
|
|
94
128
|
};
|
|
95
129
|
}
|
|
96
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Issue #248 (0.28.3): serve a snapshotted asset when the server has
|
|
133
|
+
* one, otherwise fall through to the legacy per-request read. The
|
|
134
|
+
* happy path never touches the filesystem after startup, so a checkout
|
|
135
|
+
* switch during the server's lifetime cannot change what gets served.
|
|
136
|
+
*
|
|
137
|
+
* @param {import('node:http').ServerResponse} res
|
|
138
|
+
* @param {{ buffer: Buffer, size: number, contentType?: string } | undefined | null} asset
|
|
139
|
+
* @param {string | undefined} fallbackPath
|
|
140
|
+
* @param {string} contentType
|
|
141
|
+
*/
|
|
142
|
+
async function serveAsset(res, asset, fallbackPath, contentType) {
|
|
143
|
+
if (asset && asset.buffer) {
|
|
144
|
+
res.writeHead(200, {
|
|
145
|
+
'content-type': asset.contentType ?? contentType,
|
|
146
|
+
'content-length': String(asset.size),
|
|
147
|
+
});
|
|
148
|
+
res.end(asset.buffer);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (fallbackPath) {
|
|
152
|
+
await serveFile(res, fallbackPath, contentType);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
res.writeHead(404, { 'content-type': MIME.txt });
|
|
156
|
+
res.end('not found\n');
|
|
157
|
+
}
|
|
158
|
+
|
|
97
159
|
async function serveFile(res, path, contentType) {
|
|
98
160
|
let body;
|
|
99
161
|
let size;
|
|
@@ -18,7 +18,7 @@ import { mkdir, rename, unlink, writeFile } from 'node:fs/promises';
|
|
|
18
18
|
import { dirname, join } from 'node:path';
|
|
19
19
|
|
|
20
20
|
import { rcfError } from '#core/errors';
|
|
21
|
-
import { validateDocument } from '#core/store';
|
|
21
|
+
import { buildNextManifest, validateDocument } from '#core/store';
|
|
22
22
|
|
|
23
23
|
import { composeDefaults, deepGet, deepSet } from './defaults.js';
|
|
24
24
|
import { CATALOGUE_V1 } from '../preflight/design-shapes.js';
|
|
@@ -150,15 +150,16 @@ export function preflightSeamOverrides(manifest, catalogue = CATALOGUE_V1) {
|
|
|
150
150
|
export async function writeUiBaselineRecord({
|
|
151
151
|
projectRoot, tree, record, options = {},
|
|
152
152
|
}) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
153
|
+
// 0.28.2 (Codex review follow-up on the shared helper): compose via
|
|
154
|
+
// the shared `buildNextManifest` helper so write and dry-run
|
|
155
|
+
// produce the same shape. `options.reset` still drives the
|
|
156
|
+
// history-move branch inside the helper.
|
|
157
|
+
const nextManifest = buildNextManifest({
|
|
158
|
+
manifest: tree.manifest ?? {},
|
|
159
|
+
record,
|
|
160
|
+
verb: 'uiBaselineInit',
|
|
161
|
+
options: { reset: Boolean(options.reset) },
|
|
162
|
+
});
|
|
162
163
|
|
|
163
164
|
const relPath = 'rcf/manifest.json';
|
|
164
165
|
const validation = validateDocument({ doc: nextManifest, kind: 'manifest', filePath: relPath });
|