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,730 @@
|
|
|
1
|
+
// `rcf define freeze` subcommand handler (REQ-176; proposal
|
|
2
|
+
// 2026-09-22 §2.1, §2.5, §3.2 D8, §8.2 v3; Baz 2026-09-24 renumbering
|
|
3
|
+
// puts this verb in 0.29.0).
|
|
4
|
+
//
|
|
5
|
+
// The freeze verb is the only close verb: no change-object is minted,
|
|
6
|
+
// nothing else changes state. It runs `computeReadiness` (REQ-175)
|
|
7
|
+
// once over the live tree, freeze record and four ledgers; refuses
|
|
8
|
+
// (exit 4) on any stage state 'failing' after applying any --ack
|
|
9
|
+
// arguments; and, on success, writes `rcf/define/freeze.json` via
|
|
10
|
+
// `saveFreezeRecord` (REQ-172) plus the proposal §2.5 summary line
|
|
11
|
+
// the operator signs off.
|
|
12
|
+
//
|
|
13
|
+
// D8 queue work (appending new FBS, re-opening re-execute FBS) is
|
|
14
|
+
// the agent's authoring work per the playbook, NOT this verb; the
|
|
15
|
+
// verb only gates on D8's checks in `gates.js`.
|
|
16
|
+
//
|
|
17
|
+
// --ack is accepted only for the three warn-with-ack gates
|
|
18
|
+
// (define.shapes / D3 / shapes, define.crosscut / D5 / crosscut,
|
|
19
|
+
// define.consistency / D6 / consistency) per ADR-4124. --ack on any
|
|
20
|
+
// other gate is a usage refusal (exit 2); --ack without a paired
|
|
21
|
+
// --reason is a usage refusal.
|
|
22
|
+
//
|
|
23
|
+
// Admissibility refusal is informational per ADR-4123 / ADR-4124: the
|
|
24
|
+
// wrap runs first so the ruleset toolScope guard is honoured, and on
|
|
25
|
+
// refuse the CLI prints a stderr [warn] line but the freeze compute
|
|
26
|
+
// still runs and writes when the tree is freezeable.
|
|
27
|
+
|
|
28
|
+
import { parseArgs } from 'node:util';
|
|
29
|
+
import { readFile } from 'node:fs/promises';
|
|
30
|
+
import { createRequire } from 'node:module';
|
|
31
|
+
import { join } from 'node:path';
|
|
32
|
+
|
|
33
|
+
import { resolveTestPointers, walkTree } from '#core/store';
|
|
34
|
+
import { checkCodeNodeResolution } from '#core/store';
|
|
35
|
+
import { findProjectRoot } from '../view/index.js';
|
|
36
|
+
import { loadFreezeRecord, saveFreezeRecord } from '../define/freeze-record.js';
|
|
37
|
+
import { loadAllLedgers } from '../define/ledgers.js';
|
|
38
|
+
import {
|
|
39
|
+
STAGE_ALIASES,
|
|
40
|
+
STAGE_GATES,
|
|
41
|
+
STAGE_ORDER,
|
|
42
|
+
STAGE_SHORT_NAMES,
|
|
43
|
+
parseAcClass,
|
|
44
|
+
} from '../query/gates.js';
|
|
45
|
+
import { computeDelta, hashDocument } from '../query/delta.js';
|
|
46
|
+
import { computeReadiness, shortHash } from '../query/readiness.js';
|
|
47
|
+
import { runWithAdmissibilityGate } from '../query/index.js';
|
|
48
|
+
|
|
49
|
+
const OPTION_SPEC = {
|
|
50
|
+
ack: { type: 'string', multiple: true },
|
|
51
|
+
reason: { type: 'string', multiple: true },
|
|
52
|
+
note: { type: 'string' },
|
|
53
|
+
'litmus-attested': { type: 'string' },
|
|
54
|
+
status: { type: 'boolean' },
|
|
55
|
+
json: { type: 'boolean' },
|
|
56
|
+
help: { type: 'boolean' },
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** Gates for which --ack is legal (proposal §2.5, ADR-4124). */
|
|
60
|
+
const ACKABLE_GATES = new Set([
|
|
61
|
+
STAGE_GATES.D3, STAGE_GATES.D5, STAGE_GATES.D6,
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
/** Reverse alias map: any accepted alias for the three ackable gates. */
|
|
65
|
+
const ACKABLE_ALIASES = /** @type {const} */ (() => {
|
|
66
|
+
/** @type {Record<string, string>} */
|
|
67
|
+
const out = {};
|
|
68
|
+
for (const stage of ['D3', 'D5', 'D6']) {
|
|
69
|
+
const gate = STAGE_GATES[stage];
|
|
70
|
+
const short = STAGE_SHORT_NAMES[stage];
|
|
71
|
+
out[gate] = gate;
|
|
72
|
+
out[stage] = gate;
|
|
73
|
+
out[short] = gate;
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
})();
|
|
77
|
+
|
|
78
|
+
export const HELP = `Usage: rcf define freeze [options]
|
|
79
|
+
|
|
80
|
+
Close a change on the tree by freezing again. Runs computeReadiness
|
|
81
|
+
once, refuses (exit 4) on any failing stage, and writes
|
|
82
|
+
rcf/define/freeze.json with fresh hashes plus the freeze context
|
|
83
|
+
slice-4's bundle --next and slice-5's viewer read.
|
|
84
|
+
|
|
85
|
+
The freeze verb is the only close verb: nothing is minted, nothing
|
|
86
|
+
else changes state. Editing the tree afterwards drives readiness to a
|
|
87
|
+
non-empty delta; closing that delta means running this verb again.
|
|
88
|
+
|
|
89
|
+
D8's queue-authoring work (appending new FBS for added ACs, re-opening
|
|
90
|
+
FBS the delta re-executes) is the agent's authoring work per the
|
|
91
|
+
playbook, not this verb. The verb only gates on D8's checks reported
|
|
92
|
+
by computeReadiness.
|
|
93
|
+
|
|
94
|
+
Options:
|
|
95
|
+
--ack <gate> Acknowledge a warn-with-ack gate's failure
|
|
96
|
+
at the current tree hash. Only three gates
|
|
97
|
+
are ackable: define.shapes (D3 / shapes),
|
|
98
|
+
define.crosscut (D5 / crosscut) and
|
|
99
|
+
define.consistency (D6 / consistency). Ack
|
|
100
|
+
on any other gate is a usage refusal.
|
|
101
|
+
Repeat --ack for multiple gates; every
|
|
102
|
+
--ack must be paired with a --reason.
|
|
103
|
+
--reason <text> Reason for the paired --ack. Repeatable
|
|
104
|
+
alongside --ack; the arities must match.
|
|
105
|
+
--note <text> Optional note to record on the freeze
|
|
106
|
+
(mnemonic for the change; readable via
|
|
107
|
+
--status).
|
|
108
|
+
--litmus-attested <D3,..> Comma-separated D-names (D3 / D6) for
|
|
109
|
+
which the agent attests to having read the
|
|
110
|
+
elicitation prompts in the current session
|
|
111
|
+
(proposal §9 decision 7). Recorded in
|
|
112
|
+
freeze.litmus.attestedAt.
|
|
113
|
+
--status Print the current freeze record and the
|
|
114
|
+
live delta summary; run no gates; write
|
|
115
|
+
nothing. Exits 0 even when unfrozen.
|
|
116
|
+
--json Emit machine-readable JSON. On a freeze
|
|
117
|
+
success emits { record, summary, delta };
|
|
118
|
+
on --status emits { record, delta };
|
|
119
|
+
never printed on a refusal.
|
|
120
|
+
--help Print this help.
|
|
121
|
+
|
|
122
|
+
Exit codes:
|
|
123
|
+
0 Success (record written; --status; --help).
|
|
124
|
+
2 Usage error (unknown --ack gate, --ack without --reason,
|
|
125
|
+
unknown flag).
|
|
126
|
+
4 Refused: at least one stage state is failing and no --ack
|
|
127
|
+
covered it.
|
|
128
|
+
`;
|
|
129
|
+
|
|
130
|
+
/** @typedef {'blocking' | 'warnWithAck'} StagePolicy */
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Public entry.
|
|
134
|
+
* @param {string[]} argv
|
|
135
|
+
* @param {object} [deps]
|
|
136
|
+
* @returns {Promise<number>}
|
|
137
|
+
*/
|
|
138
|
+
export async function main(argv, deps = {}) {
|
|
139
|
+
const stdout = deps.stdout ?? process.stdout;
|
|
140
|
+
const stderr = deps.stderr ?? process.stderr;
|
|
141
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
142
|
+
const now = deps.now ?? (() => new Date());
|
|
143
|
+
|
|
144
|
+
let parsed;
|
|
145
|
+
try {
|
|
146
|
+
parsed = parseArgs({ args: argv, options: OPTION_SPEC, allowPositionals: false, strict: true });
|
|
147
|
+
} catch (err) {
|
|
148
|
+
stderr.write(`[error] usage ${err.message}\n`);
|
|
149
|
+
stderr.write(HELP);
|
|
150
|
+
return 2;
|
|
151
|
+
}
|
|
152
|
+
const flags = parsed.values;
|
|
153
|
+
if (flags.help) {
|
|
154
|
+
stdout.write(HELP);
|
|
155
|
+
return 0;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// --ack / --reason parsing (usage validation before any I/O).
|
|
159
|
+
/** @type {Array<{ gate: string, reason: string }>} */
|
|
160
|
+
let ackPairs;
|
|
161
|
+
try {
|
|
162
|
+
ackPairs = parseAckPairs(flags.ack, flags.reason);
|
|
163
|
+
} catch (err) {
|
|
164
|
+
stderr.write(`[error] usage ${err.message}\n`);
|
|
165
|
+
return 2;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const projectRoot = await findProjectRoot(cwd);
|
|
169
|
+
if (!projectRoot) {
|
|
170
|
+
stderr.write('[error] usage no project root found (no rcf/manifest.json in this directory or any ancestor). Run `npx rcf init` to create and wire a project.\n');
|
|
171
|
+
return 2;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// --status branch: read-only. No gates, no writes.
|
|
175
|
+
if (flags.status) {
|
|
176
|
+
return runStatus({ projectRoot, flags, stdout, stderr });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Freeze branch.
|
|
180
|
+
return runFreeze({
|
|
181
|
+
projectRoot, flags, ackPairs, now, stdout, stderr,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Walk --ack and --reason multi-valued arrays in parallel; refuse on
|
|
187
|
+
* unknown gate names, missing --reason, or count mismatch. Returns
|
|
188
|
+
* the paired list normalised to canonical gate ids.
|
|
189
|
+
*
|
|
190
|
+
* @param {string[]|undefined} acks
|
|
191
|
+
* @param {string[]|undefined} reasons
|
|
192
|
+
* @returns {Array<{ gate: string, reason: string }>}
|
|
193
|
+
*/
|
|
194
|
+
function parseAckPairs(acks, reasons) {
|
|
195
|
+
const ackArr = Array.isArray(acks) ? acks : [];
|
|
196
|
+
const reasonArr = Array.isArray(reasons) ? reasons : [];
|
|
197
|
+
if (ackArr.length !== reasonArr.length) {
|
|
198
|
+
throw new Error(`--ack requires a matching --reason (got ${ackArr.length} ack(s) and ${reasonArr.length} reason(s))`);
|
|
199
|
+
}
|
|
200
|
+
/** @type {Array<{ gate: string, reason: string }>} */
|
|
201
|
+
const out = [];
|
|
202
|
+
const acceptedList = Object.keys(ACKABLE_ALIASES).sort().join(' | ');
|
|
203
|
+
for (let i = 0; i < ackArr.length; i += 1) {
|
|
204
|
+
const raw = ackArr[i];
|
|
205
|
+
const canonical = ACKABLE_ALIASES[raw];
|
|
206
|
+
if (!canonical) {
|
|
207
|
+
throw new Error(`--ack ${raw} is not an ackable gate; accepted: ${acceptedList}`);
|
|
208
|
+
}
|
|
209
|
+
const reason = reasonArr[i];
|
|
210
|
+
if (typeof reason !== 'string' || reason.length === 0) {
|
|
211
|
+
throw new Error(`--ack ${raw} requires a non-empty --reason`);
|
|
212
|
+
}
|
|
213
|
+
out.push({ gate: canonical, reason });
|
|
214
|
+
}
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
// --status branch
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* @param {object} args
|
|
224
|
+
* @param {string} args.projectRoot
|
|
225
|
+
* @param {Record<string, unknown>} args.flags
|
|
226
|
+
* @param {NodeJS.WritableStream} args.stdout
|
|
227
|
+
* @param {NodeJS.WritableStream} args.stderr
|
|
228
|
+
* @returns {Promise<number>}
|
|
229
|
+
*/
|
|
230
|
+
async function runStatus({ projectRoot, flags, stdout, stderr }) {
|
|
231
|
+
let record;
|
|
232
|
+
try {
|
|
233
|
+
record = await loadFreezeRecord({ projectRoot });
|
|
234
|
+
} catch (err) {
|
|
235
|
+
stderr.write(`[error] freeze: ${/** @type {Error} */ (err).message}\n`);
|
|
236
|
+
return 1;
|
|
237
|
+
}
|
|
238
|
+
const { tree } = await walkTree({ projectRoot });
|
|
239
|
+
const ledgers = await loadAllLedgers({ projectRoot });
|
|
240
|
+
const delta = computeDelta(tree, record, ledgers);
|
|
241
|
+
|
|
242
|
+
if (flags.json) {
|
|
243
|
+
stdout.write(`${JSON.stringify({ record, delta }, null, 2)}\n`);
|
|
244
|
+
return 0;
|
|
245
|
+
}
|
|
246
|
+
if (!record) {
|
|
247
|
+
stdout.write('never frozen\n');
|
|
248
|
+
stdout.write(`Delta: ${delta.added.length} added, ${delta.changed.length} changed, ${delta.removed.length} removed (unfrozen tree; every id is 'added').\n`);
|
|
249
|
+
return 0;
|
|
250
|
+
}
|
|
251
|
+
stdout.write(`${formatStatusLine(record)}\n`);
|
|
252
|
+
stdout.write(`Delta: ${delta.changed.length} changed, ${delta.added.length} added, ${delta.removed.length} removed, ${delta.briefSince.length} brief statement(s) since.\n`);
|
|
253
|
+
return 0;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Compose the one-block text summary for `--status`.
|
|
258
|
+
*
|
|
259
|
+
* @param {import('../define/freeze-record.js').default | Record<string, unknown>} record
|
|
260
|
+
* @returns {string}
|
|
261
|
+
*/
|
|
262
|
+
function formatStatusLine(record) {
|
|
263
|
+
const r = /** @type {Record<string, unknown>} */ (record);
|
|
264
|
+
const treeHash = typeof r.treeHash === 'string' ? r.treeHash : null;
|
|
265
|
+
const frozenAt = typeof r.frozenAt === 'string' ? r.frozenAt : '(unknown)';
|
|
266
|
+
const note = typeof r.note === 'string' && r.note.length > 0 ? r.note : '(none)';
|
|
267
|
+
const counts = /** @type {any} */ (r.counts) ?? {};
|
|
268
|
+
const litmus = /** @type {any} */ (r.litmus) ?? {};
|
|
269
|
+
const versions = /** @type {any} */ (r.versions) ?? {};
|
|
270
|
+
const gates = /** @type {any} */ (r.gates) ?? {};
|
|
271
|
+
const acked = Object.entries(gates)
|
|
272
|
+
.filter(([, g]) => g && g.state === 'acknowledged')
|
|
273
|
+
.map(([gate, g]) => `${gate} ("${g.at?.reason ?? ''}")`);
|
|
274
|
+
const ackedLine = acked.length > 0 ? acked.join('; ') : '(none)';
|
|
275
|
+
const attested = Array.isArray(litmus.attestedAt) ? litmus.attestedAt.join(', ') : '';
|
|
276
|
+
const overrideVal = r.override;
|
|
277
|
+
const overrideStr = overrideVal === null || overrideVal === undefined
|
|
278
|
+
? 'null'
|
|
279
|
+
: `${/** @type {any} */ (overrideVal).reason} by ${/** @type {any} */ (overrideVal).by} at ${/** @type {any} */ (overrideVal).at}`;
|
|
280
|
+
const parts = [
|
|
281
|
+
`Frozen at ${shortHash(treeHash)} on ${frozenAt}.`,
|
|
282
|
+
`Note: ${note}.`,
|
|
283
|
+
`Counts: ${counts.req ?? 0} REQ / ${counts.us ?? 0} US / ${counts.ac ?? 0} AC / ${counts.tac ?? 0} TAC / ${counts.adr ?? 0} ADR / ${counts.fbs ?? 0} FBS.`,
|
|
284
|
+
`Acknowledged: ${ackedLine}.`,
|
|
285
|
+
`Litmus: attestedAt [${attested}], readers ${litmus.readers ?? 0}.`,
|
|
286
|
+
`Versions: rcfLite ${versions.rcfLite ?? '?'} / ruleset ${versions.ruleset ?? '?'} / schemas ${versions.schemas ?? '?'}.`,
|
|
287
|
+
`Override: ${overrideStr}.`,
|
|
288
|
+
];
|
|
289
|
+
return parts.join(' ');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
// Freeze branch
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* @param {object} args
|
|
298
|
+
* @param {string} args.projectRoot
|
|
299
|
+
* @param {Record<string, unknown>} args.flags
|
|
300
|
+
* @param {Array<{ gate: string, reason: string }>} args.ackPairs
|
|
301
|
+
* @param {() => Date} args.now
|
|
302
|
+
* @param {NodeJS.WritableStream} args.stdout
|
|
303
|
+
* @param {NodeJS.WritableStream} args.stderr
|
|
304
|
+
* @returns {Promise<number>}
|
|
305
|
+
*/
|
|
306
|
+
async function runFreeze({
|
|
307
|
+
projectRoot, flags, ackPairs, now, stdout, stderr,
|
|
308
|
+
}) {
|
|
309
|
+
const { tree, errors } = await walkTree({ projectRoot });
|
|
310
|
+
const staleErrors = await checkCodeNodeResolution({ projectRoot, tree });
|
|
311
|
+
const validateErrors = [...errors, ...staleErrors];
|
|
312
|
+
|
|
313
|
+
const [priorFreeze, ledgers, testPointers, profileText] = await Promise.all([
|
|
314
|
+
loadFreezeRecord({ projectRoot }),
|
|
315
|
+
loadAllLedgers({ projectRoot }),
|
|
316
|
+
resolveTestPointers({ projectRoot, tree }),
|
|
317
|
+
readProfileText(projectRoot),
|
|
318
|
+
]);
|
|
319
|
+
|
|
320
|
+
const chainRulesetVersion = typeof tree.manifest?.rulesetVersion === 'string'
|
|
321
|
+
? tree.manifest.rulesetVersion
|
|
322
|
+
: null;
|
|
323
|
+
|
|
324
|
+
// Compute the tree hash up front so we can synthesise a freeze
|
|
325
|
+
// context that carries any --ack acknowledgements at the current
|
|
326
|
+
// hash. The readiness compute then folds D3 / D5 / D6 to
|
|
327
|
+
// 'acknowledged' in the same pass, which is what D8's priorStages
|
|
328
|
+
// check reads. Applying acks post-readiness would leave D8 seeing
|
|
329
|
+
// stale prior states and refusing on freeze:priorGates.
|
|
330
|
+
const preHashDelta = computeDelta(tree, priorFreeze, ledgers);
|
|
331
|
+
const currentTreeHash = preHashDelta.currentTreeHash;
|
|
332
|
+
|
|
333
|
+
const ackedReasons = new Map(ackPairs.map((p) => [p.gate, p.reason]));
|
|
334
|
+
const freezeForCompute = ackedReasons.size === 0
|
|
335
|
+
? priorFreeze
|
|
336
|
+
: mergeAckedGates(priorFreeze, ackedReasons, currentTreeHash);
|
|
337
|
+
|
|
338
|
+
const gated = await runWithAdmissibilityGate({
|
|
339
|
+
tree,
|
|
340
|
+
chainRulesetVersion,
|
|
341
|
+
produce: () => computeReadiness(tree, {
|
|
342
|
+
freeze: freezeForCompute,
|
|
343
|
+
ledgers,
|
|
344
|
+
profileText,
|
|
345
|
+
testPointers,
|
|
346
|
+
validateErrors,
|
|
347
|
+
}),
|
|
348
|
+
});
|
|
349
|
+
if (gated.status === 'refused-admissibility') {
|
|
350
|
+
stderr.write(`[warn] freeze: chain admissibility refused (informational; the freeze still ran). ${gated.refusal}\n`);
|
|
351
|
+
}
|
|
352
|
+
/** @type {import('../query/readiness.js').ReadinessResult} */
|
|
353
|
+
const readiness = gated.status === 'refused-admissibility'
|
|
354
|
+
? computeReadiness(tree, {
|
|
355
|
+
freeze: freezeForCompute, ledgers, profileText, testPointers, validateErrors,
|
|
356
|
+
})
|
|
357
|
+
: gated.payload;
|
|
358
|
+
|
|
359
|
+
// The stages array already carries acknowledged states for any acked
|
|
360
|
+
// gate at the current tree hash. Stamp the ack reasons onto the
|
|
361
|
+
// matching stage entries so the freeze record and the summary block
|
|
362
|
+
// carry the reason text.
|
|
363
|
+
const stagesAfterAck = readiness.stages.map((stage) => {
|
|
364
|
+
const reason = ackedReasons.get(stage.gate);
|
|
365
|
+
if (!reason) return stage;
|
|
366
|
+
return { ...stage, reason };
|
|
367
|
+
});
|
|
368
|
+
const freezeableAfterAck = stagesAfterAck.every(
|
|
369
|
+
(s) => s.state === 'passed' || s.state === 'acknowledged' || s.state === 'notApplicable',
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
const preFreezeDelta = readiness.delta;
|
|
373
|
+
|
|
374
|
+
if (!freezeableAfterAck) {
|
|
375
|
+
printFailingBlock(stderr, stagesAfterAck);
|
|
376
|
+
return 4;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Build the freeze record body.
|
|
380
|
+
const frozenAt = now().toISOString();
|
|
381
|
+
// Sanity: the tree hash the readiness compute reports must match the
|
|
382
|
+
// one we pre-computed. Never diverges in practice (both hash the
|
|
383
|
+
// same live tree the same way); the assertion documents the
|
|
384
|
+
// invariant for future maintainers.
|
|
385
|
+
if (readiness.tree.currentTreeHash !== currentTreeHash) {
|
|
386
|
+
stderr.write(`[error] freeze: internal invariant: tree hash mismatch between pre-compute and readiness (${currentTreeHash} vs ${readiness.tree.currentTreeHash})\n`);
|
|
387
|
+
return 1;
|
|
388
|
+
}
|
|
389
|
+
const docHashes = computeDocHashes(tree, ledgers);
|
|
390
|
+
const briefStatements = countBriefStatements(ledgers);
|
|
391
|
+
const gates = buildGatesEntry(stagesAfterAck, currentTreeHash, frozenAt);
|
|
392
|
+
const counts = deriveCounts(tree);
|
|
393
|
+
const litmusAttested = parseLitmusAttested(flags['litmus-attested']);
|
|
394
|
+
const litmus = { attestedAt: litmusAttested, readers: 0 };
|
|
395
|
+
const versions = await readVersions(projectRoot);
|
|
396
|
+
const note = typeof flags.note === 'string' ? flags.note : null;
|
|
397
|
+
|
|
398
|
+
/** @type {Record<string, unknown>} */
|
|
399
|
+
const record = {
|
|
400
|
+
frozenAt,
|
|
401
|
+
treeHash: currentTreeHash,
|
|
402
|
+
docHashes,
|
|
403
|
+
briefStatements,
|
|
404
|
+
gates,
|
|
405
|
+
counts,
|
|
406
|
+
litmus,
|
|
407
|
+
versions,
|
|
408
|
+
note,
|
|
409
|
+
override: null,
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
await saveFreezeRecord({ projectRoot, record: /** @type {any} */ (record) });
|
|
413
|
+
|
|
414
|
+
const summary = formatSummary({
|
|
415
|
+
firstFreeze: priorFreeze === null,
|
|
416
|
+
currentTreeHash,
|
|
417
|
+
preFreezeDelta,
|
|
418
|
+
counts,
|
|
419
|
+
});
|
|
420
|
+
if (flags.json) {
|
|
421
|
+
stdout.write(`${JSON.stringify({ record, summary, delta: preFreezeDelta }, null, 2)}\n`);
|
|
422
|
+
} else {
|
|
423
|
+
stdout.write(`${summary}\n`);
|
|
424
|
+
}
|
|
425
|
+
return 0;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Print the failing-stage block: one line per failing stage, capped
|
|
430
|
+
* at 20 ids per stage; a hint at the bottom naming the readiness
|
|
431
|
+
* command for the operator to inspect further.
|
|
432
|
+
*
|
|
433
|
+
* @param {NodeJS.WritableStream} stderr
|
|
434
|
+
* @param {import('../query/readiness.js').StageResult[]} stages
|
|
435
|
+
*/
|
|
436
|
+
function printFailingBlock(stderr, stages) {
|
|
437
|
+
const failing = stages.filter((s) => s.state === 'failing');
|
|
438
|
+
if (failing.length === 0) return;
|
|
439
|
+
stderr.write('[error] freeze: refused; the following stage(s) are failing:\n');
|
|
440
|
+
for (const s of failing) {
|
|
441
|
+
const firstFailingCheck = s.checks.find((c) => !c.ok);
|
|
442
|
+
const checkName = firstFailingCheck?.name ?? '(no check)';
|
|
443
|
+
const ids = firstFailingCheck?.failing.slice(0, 20).map((f) => f.id) ?? [];
|
|
444
|
+
stderr.write(` ${s.stage} (${s.gate}): failing (${checkName}); failing ids: ${ids.join(', ') || '(none)'}\n`);
|
|
445
|
+
const shortName = STAGE_SHORT_NAMES[s.stage] ?? s.stage;
|
|
446
|
+
stderr.write(` Run \`rcf define readiness --check ${shortName}\` to inspect.\n`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// ---------------------------------------------------------------------------
|
|
451
|
+
// Record-body helpers
|
|
452
|
+
// ---------------------------------------------------------------------------
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Recompute the docHashes map (standalone ids plus ledger:<name>
|
|
456
|
+
* entries) to write into the freeze record. Matches the shape
|
|
457
|
+
* computeDelta uses internally; the treeHash is already the whole
|
|
458
|
+
* readiness result's currentTreeHash.
|
|
459
|
+
*
|
|
460
|
+
* @param {import('#core/store/walker.js').TreeModel} tree
|
|
461
|
+
* @param {import('../query/delta.js').LedgerBundle} ledgers
|
|
462
|
+
* @returns {Record<string, string>}
|
|
463
|
+
*/
|
|
464
|
+
function computeDocHashes(tree, ledgers) {
|
|
465
|
+
/** @type {Record<string, string>} */
|
|
466
|
+
const out = {};
|
|
467
|
+
const ids = tree.byId ? [...tree.byId.keys()].sort() : [];
|
|
468
|
+
for (const id of ids) out[id] = hashDocument(tree.byId.get(id));
|
|
469
|
+
for (const [name, body] of Object.entries(ledgers ?? {})) {
|
|
470
|
+
if (body === undefined || body === null) continue;
|
|
471
|
+
out[`ledger:${name}`] = hashDocument(body);
|
|
472
|
+
}
|
|
473
|
+
return out;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Count of statements in the brief ledger (0 when absent).
|
|
478
|
+
*
|
|
479
|
+
* @param {import('../query/delta.js').LedgerBundle} ledgers
|
|
480
|
+
* @returns {number}
|
|
481
|
+
*/
|
|
482
|
+
function countBriefStatements(ledgers) {
|
|
483
|
+
const brief = /** @type {any} */ (ledgers?.brief);
|
|
484
|
+
if (!brief || !Array.isArray(brief.statements)) return 0;
|
|
485
|
+
return brief.statements.length;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Return a freeze context that layers the requested acknowledgements
|
|
490
|
+
* onto the prior freeze record's `gates` map at the current tree
|
|
491
|
+
* hash. Consumed by `computeReadiness` so its foldState folds D3 / D5
|
|
492
|
+
* / D6 to 'acknowledged' in the same pass. The returned object is a
|
|
493
|
+
* shallow clone of the prior record with the gates map augmented; it
|
|
494
|
+
* is used only for the compute and is not written to disk.
|
|
495
|
+
*
|
|
496
|
+
* When priorFreeze is null (first freeze), a stub `{ gates }` object
|
|
497
|
+
* is returned. computeDelta still treats it as unfrozen (no
|
|
498
|
+
* docHashes / no briefStatements), so the delta is the whole tree
|
|
499
|
+
* exactly as it would be without the stub.
|
|
500
|
+
*
|
|
501
|
+
* @param {any} priorFreeze
|
|
502
|
+
* @param {Map<string, string>} ackedReasons - gate id -> reason
|
|
503
|
+
* @param {string} currentTreeHash
|
|
504
|
+
* @returns {any}
|
|
505
|
+
*/
|
|
506
|
+
function mergeAckedGates(priorFreeze, ackedReasons, currentTreeHash) {
|
|
507
|
+
const priorGates = /** @type {any} */ (priorFreeze?.gates) ?? {};
|
|
508
|
+
/** @type {Record<string, any>} */
|
|
509
|
+
const gates = { ...priorGates };
|
|
510
|
+
for (const [gate, reason] of ackedReasons) {
|
|
511
|
+
gates[gate] = {
|
|
512
|
+
state: 'acknowledged',
|
|
513
|
+
at: { hash: currentTreeHash, reason },
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
if (priorFreeze === null || priorFreeze === undefined) {
|
|
517
|
+
return { gates };
|
|
518
|
+
}
|
|
519
|
+
return { ...priorFreeze, gates };
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Assemble the freeze.gates map. Every stage in D1..D8 is recorded
|
|
524
|
+
* so slice-5 viewer sees a consistent shape. Acknowledged stages
|
|
525
|
+
* store their { hash, at, reason } stamp; passed / notApplicable
|
|
526
|
+
* stages record their state with `at: null`.
|
|
527
|
+
*
|
|
528
|
+
* @param {import('../query/readiness.js').StageResult[]} stages
|
|
529
|
+
* @param {string} currentTreeHash
|
|
530
|
+
* @param {string} frozenAt
|
|
531
|
+
* @returns {Record<string, { state: string, at: null | { hash: string, at: string, reason: string }, failing: Array<{ id: string, why: string }> }>}
|
|
532
|
+
*/
|
|
533
|
+
function buildGatesEntry(stages, currentTreeHash, frozenAt) {
|
|
534
|
+
/** @type {Record<string, any>} */
|
|
535
|
+
const out = {};
|
|
536
|
+
for (const stage of stages) {
|
|
537
|
+
const failing = [];
|
|
538
|
+
for (const check of stage.checks) {
|
|
539
|
+
for (const f of check.failing) failing.push({ id: f.id, why: f.why });
|
|
540
|
+
}
|
|
541
|
+
if (stage.state === 'acknowledged') {
|
|
542
|
+
out[stage.gate] = {
|
|
543
|
+
state: 'acknowledged',
|
|
544
|
+
at: {
|
|
545
|
+
hash: currentTreeHash,
|
|
546
|
+
at: frozenAt,
|
|
547
|
+
reason: stage.reason ?? '(none)',
|
|
548
|
+
},
|
|
549
|
+
failing,
|
|
550
|
+
};
|
|
551
|
+
} else {
|
|
552
|
+
out[stage.gate] = {
|
|
553
|
+
state: stage.state,
|
|
554
|
+
at: null,
|
|
555
|
+
failing,
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return out;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Derive count aggregates from the walker tree. Counts:
|
|
564
|
+
* req / us / ac / acByClass / tac / interfaces (by kind) / adr / fbs
|
|
565
|
+
*
|
|
566
|
+
* @param {import('#core/store/walker.js').TreeModel} tree
|
|
567
|
+
* @returns {Record<string, unknown>}
|
|
568
|
+
*/
|
|
569
|
+
function deriveCounts(tree) {
|
|
570
|
+
const req = (tree.requirements ?? []).length;
|
|
571
|
+
const userStories = tree.userStories ?? [];
|
|
572
|
+
const us = userStories.length;
|
|
573
|
+
let ac = 0;
|
|
574
|
+
const acByClass = { happy: 0, edge: 0, failure: 0, mustNot: 0, nonFunctional: 0, unclassified: 0 };
|
|
575
|
+
for (const story of userStories) {
|
|
576
|
+
const list = Array.isArray(story?.acceptanceCriteria) ? story.acceptanceCriteria : [];
|
|
577
|
+
ac += list.length;
|
|
578
|
+
for (const acEntry of list) {
|
|
579
|
+
const cls = parseAcClass(acEntry);
|
|
580
|
+
switch (cls) {
|
|
581
|
+
case 'happy': acByClass.happy += 1; break;
|
|
582
|
+
case 'edge': acByClass.edge += 1; break;
|
|
583
|
+
case 'failure': acByClass.failure += 1; break;
|
|
584
|
+
case 'must-not': acByClass.mustNot += 1; break;
|
|
585
|
+
case 'non-functional': acByClass.nonFunctional += 1; break;
|
|
586
|
+
default: acByClass.unclassified += 1;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
const tacs = tree.tacs ?? [];
|
|
591
|
+
const tac = tacs.length;
|
|
592
|
+
/** @type {Record<string, number>} */
|
|
593
|
+
const interfaces = {};
|
|
594
|
+
for (const t of tacs) {
|
|
595
|
+
const list = Array.isArray(t?.interfaces) ? t.interfaces : [];
|
|
596
|
+
for (const iface of list) {
|
|
597
|
+
const kind = typeof iface?.kind === 'string' ? iface.kind : 'unknown';
|
|
598
|
+
interfaces[kind] = (interfaces[kind] ?? 0) + 1;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
const adr = (tree.adrs ?? []).length;
|
|
602
|
+
const fbs = (tree.fbsItems ?? []).length;
|
|
603
|
+
return { req, us, ac, acByClass, tac, interfaces, adr, fbs };
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Parse the --litmus-attested comma-separated D-names.
|
|
608
|
+
* @param {unknown} raw
|
|
609
|
+
* @returns {string[]}
|
|
610
|
+
*/
|
|
611
|
+
function parseLitmusAttested(raw) {
|
|
612
|
+
if (typeof raw !== 'string' || raw.length === 0) return [];
|
|
613
|
+
return raw.split(',').map((t) => t.trim()).filter((t) => t.length > 0);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Read rcfLite, ruleset and schemas versions.
|
|
618
|
+
*
|
|
619
|
+
* rcfLite: packages/rcf-lite/package.json (this package). Located via
|
|
620
|
+
* import.meta.url; falls back to '(unknown)' on any failure.
|
|
621
|
+
* ruleset: src/ruleset/ruleset.json's rulesetVersion field when
|
|
622
|
+
* present, else the rcfLite version (NV-BL-SR-02: ruleset version
|
|
623
|
+
* follows the umbrella).
|
|
624
|
+
* schemas: node_modules/@stravica-ai/rcf-schemas/package.json,
|
|
625
|
+
* discovered via createRequire so the resolution obeys the working
|
|
626
|
+
* project's node_modules.
|
|
627
|
+
*
|
|
628
|
+
* @param {string} projectRoot
|
|
629
|
+
* @returns {Promise<{ rcfLite: string, ruleset: string, schemas: string }>}
|
|
630
|
+
*/
|
|
631
|
+
async function readVersions(projectRoot) {
|
|
632
|
+
let rcfLite = '(unknown)';
|
|
633
|
+
try {
|
|
634
|
+
const pkgJson = await readFile(
|
|
635
|
+
new URL('../../package.json', import.meta.url),
|
|
636
|
+
'utf8',
|
|
637
|
+
);
|
|
638
|
+
const pkg = JSON.parse(pkgJson);
|
|
639
|
+
if (typeof pkg.version === 'string') rcfLite = pkg.version;
|
|
640
|
+
} catch { /* leave '(unknown)' */ }
|
|
641
|
+
|
|
642
|
+
let ruleset = rcfLite;
|
|
643
|
+
try {
|
|
644
|
+
const rulesetJson = await readFile(
|
|
645
|
+
new URL('../ruleset/ruleset.json', import.meta.url),
|
|
646
|
+
'utf8',
|
|
647
|
+
);
|
|
648
|
+
const body = JSON.parse(rulesetJson);
|
|
649
|
+
if (typeof body.rulesetVersion === 'string') ruleset = body.rulesetVersion;
|
|
650
|
+
} catch { /* leave rcfLite */ }
|
|
651
|
+
|
|
652
|
+
let schemas = '(unknown)';
|
|
653
|
+
try {
|
|
654
|
+
const req = createRequire(join(projectRoot, 'noop.js'));
|
|
655
|
+
const schemasPkg = req('@stravica-ai/rcf-schemas/package.json');
|
|
656
|
+
if (schemasPkg && typeof schemasPkg.version === 'string') schemas = schemasPkg.version;
|
|
657
|
+
} catch {
|
|
658
|
+
// Fall back to walking up from this module.
|
|
659
|
+
try {
|
|
660
|
+
const req = createRequire(import.meta.url);
|
|
661
|
+
const schemasPkg = req('@stravica-ai/rcf-schemas/package.json');
|
|
662
|
+
if (schemasPkg && typeof schemasPkg.version === 'string') schemas = schemasPkg.version;
|
|
663
|
+
} catch { /* leave '(unknown)' */ }
|
|
664
|
+
}
|
|
665
|
+
return { rcfLite, ruleset, schemas };
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Read `rcf/.identity/profile.md` when it exists; null otherwise.
|
|
670
|
+
* @param {string} projectRoot
|
|
671
|
+
* @returns {Promise<string|null>}
|
|
672
|
+
*/
|
|
673
|
+
async function readProfileText(projectRoot) {
|
|
674
|
+
try {
|
|
675
|
+
return await readFile(join(projectRoot, 'rcf', '.identity', 'profile.md'), 'utf8');
|
|
676
|
+
} catch { return null; }
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Compose the proposal §2.5 summary line.
|
|
681
|
+
*
|
|
682
|
+
* First freeze: "first freeze at <short>: <req> REQ, <us> stories,
|
|
683
|
+
* <ac> criteria, <interfaces> interfaces, <tac> TAC,
|
|
684
|
+
* <adr> ADR, <fbs> FBS."
|
|
685
|
+
*
|
|
686
|
+
* Incremental: "frozen at <short>: <n> REQ, <n> stories, <n> criteria,
|
|
687
|
+
* <n> interfaces added; <amended-ids> amended;
|
|
688
|
+
* <impactedFbs> re-execute."
|
|
689
|
+
*
|
|
690
|
+
* @param {object} args
|
|
691
|
+
* @param {boolean} args.firstFreeze
|
|
692
|
+
* @param {string} args.currentTreeHash
|
|
693
|
+
* @param {import('../query/readiness.js').ReadinessDelta} args.preFreezeDelta
|
|
694
|
+
* @param {Record<string, unknown>} args.counts
|
|
695
|
+
* @returns {string}
|
|
696
|
+
*/
|
|
697
|
+
function formatSummary({
|
|
698
|
+
firstFreeze, currentTreeHash, preFreezeDelta, counts,
|
|
699
|
+
}) {
|
|
700
|
+
const shortH = shortHash(currentTreeHash);
|
|
701
|
+
if (firstFreeze) {
|
|
702
|
+
const ifaces = Object.values(/** @type {any} */ (counts.interfaces) ?? {})
|
|
703
|
+
.reduce((a, b) => a + b, 0);
|
|
704
|
+
return `first freeze at ${shortH}: ${counts.req ?? 0} REQ, ${counts.us ?? 0} stories, ${counts.ac ?? 0} criteria, ${ifaces} interfaces, ${counts.tac ?? 0} TAC, ${counts.adr ?? 0} ADR, ${counts.fbs ?? 0} FBS.`;
|
|
705
|
+
}
|
|
706
|
+
// Incremental: derive added-by-kind from delta.added ids (skipping
|
|
707
|
+
// 'ledger:*' entries and ids the tree does not have a kind for).
|
|
708
|
+
const addedByKind = { req: 0, us: 0, ac: 0, tac: 0, adr: 0, fbs: 0, interfaces: 0 };
|
|
709
|
+
for (const id of preFreezeDelta.added) {
|
|
710
|
+
if (id.startsWith('ledger:')) continue;
|
|
711
|
+
if (id.startsWith('REQ-')) addedByKind.req += 1;
|
|
712
|
+
else if (id.startsWith('US-')) addedByKind.us += 1;
|
|
713
|
+
else if (id.startsWith('AC-')) addedByKind.ac += 1;
|
|
714
|
+
else if (id.startsWith('TAC-')) addedByKind.tac += 1;
|
|
715
|
+
else if (id.startsWith('ADR-')) addedByKind.adr += 1;
|
|
716
|
+
else if (id.startsWith('FBS-')) addedByKind.fbs += 1;
|
|
717
|
+
}
|
|
718
|
+
// AC is inline under US; the proposal example counts criteria too.
|
|
719
|
+
// We estimate criteria-added from the changed/added US: since inline
|
|
720
|
+
// ACs ride inside the parent hash, standalone AC counts here are
|
|
721
|
+
// usually zero unless the walker later exposes them. Keep the
|
|
722
|
+
// proposal's four-kind sequence and treat interfaces-added as 0 for
|
|
723
|
+
// now (interfaces ride inside TAC).
|
|
724
|
+
const amended = preFreezeDelta.changed.filter((id) => !id.startsWith('ledger:'));
|
|
725
|
+
const impactedFbsCount = preFreezeDelta.impactedFbs.length;
|
|
726
|
+
return `frozen at ${shortH}: ${addedByKind.req} REQ, ${addedByKind.us} stories, ${addedByKind.ac} criteria, ${addedByKind.interfaces} interfaces added; ${amended.join(', ') || '(none)'} amended; ${impactedFbsCount} re-execute.`;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Re-export the seams other modules use.
|
|
730
|
+
export { STAGE_GATES, STAGE_ORDER, STAGE_SHORT_NAMES, STAGE_ALIASES };
|