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,247 @@
|
|
|
1
|
+
// Freeze record I/O (REQ-172; proposal §2.1 v3).
|
|
2
|
+
//
|
|
3
|
+
// One file: `rcf/define/freeze.json`. Overwritten on every freeze; git
|
|
4
|
+
// holds the history. Owned by rcf-lite, outside the shared rcf-schemas
|
|
5
|
+
// package until 0.7 (ADR-4121). This module is the seam the freeze
|
|
6
|
+
// verb calls in slice 3; the verb itself is not built yet.
|
|
7
|
+
//
|
|
8
|
+
// A missing file returns `null` (a fresh tree; the delta model treats
|
|
9
|
+
// null as unfrozen, proposal §2.2). A malformed file (parse failure,
|
|
10
|
+
// or a body that fails the local schema) raises a clear error
|
|
11
|
+
// naming the file and the failing field. Silence is never the answer
|
|
12
|
+
// to a broken freeze record: the DEFINE gates read this file on every
|
|
13
|
+
// invocation and any downstream check would be lying if it kept going.
|
|
14
|
+
//
|
|
15
|
+
// Local schema (this file, not @stravica-ai/rcf-schemas):
|
|
16
|
+
//
|
|
17
|
+
// {
|
|
18
|
+
// frozenAt: ISO-8601 timestamp string,
|
|
19
|
+
// treeHash: "sha256:<64 hex>",
|
|
20
|
+
// docHashes: { <docId>: "sha256:<64 hex>", ... }, // required
|
|
21
|
+
// briefStatements: integer >= 0, // required
|
|
22
|
+
// gates: { [gate]: { state, at, failing?, reason? } }, // optional
|
|
23
|
+
// counts: { req, us, ac, ... } (open object, integers, optional)
|
|
24
|
+
// litmus: { attestedAt: string[], readers?: number }, // optional
|
|
25
|
+
// versions: { rcfLite, ruleset, schemas }, // optional
|
|
26
|
+
// note: string | null, // optional
|
|
27
|
+
// override: null | { reason, by, at } // required (nullable)
|
|
28
|
+
// }
|
|
29
|
+
//
|
|
30
|
+
// `override` is required-nullable: the validator throws when the key
|
|
31
|
+
// is missing; every writer stamps `override: null` on a fresh freeze.
|
|
32
|
+
// The loader tolerates older records that lack the key by defaulting
|
|
33
|
+
// to null on read, so a pre-slice-3 freeze.json survives a load
|
|
34
|
+
// without a migration step (the next write converges the on-disk
|
|
35
|
+
// shape).
|
|
36
|
+
//
|
|
37
|
+
// The record is EXTENSIBLE: later slices add fields (2 gates the freeze
|
|
38
|
+
// verb wrote at freeze time, 4 the `bundle --next` refusal read at
|
|
39
|
+
// override time). This slice validates the fields it consumes and
|
|
40
|
+
// tolerates the rest; unknown properties survive a load/save
|
|
41
|
+
// round-trip so a newer rcf-lite writing the file does not lose them
|
|
42
|
+
// to an older reader mid-migration.
|
|
43
|
+
|
|
44
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
45
|
+
import { dirname, join } from 'node:path';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Relative path (under the project root) of the freeze record. Used by
|
|
49
|
+
* the walker-invisibility invariant test and by every caller that
|
|
50
|
+
* wants to log the path in an error message.
|
|
51
|
+
*/
|
|
52
|
+
export const FREEZE_RECORD_REL_PATH = 'rcf/define/freeze.json';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Absolute path of the freeze record for a project root.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} projectRoot
|
|
58
|
+
* @returns {string}
|
|
59
|
+
*/
|
|
60
|
+
export function freezeRecordPath(projectRoot) {
|
|
61
|
+
return join(projectRoot, FREEZE_RECORD_REL_PATH);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Structured error thrown on a malformed freeze record. Carries the
|
|
66
|
+
* file path and a short reason. `code` fixes the class so callers can
|
|
67
|
+
* distinguish parse failures from schema failures without matching on
|
|
68
|
+
* the message.
|
|
69
|
+
*/
|
|
70
|
+
export class FreezeRecordError extends Error {
|
|
71
|
+
/**
|
|
72
|
+
* @param {string} message
|
|
73
|
+
* @param {object} [opts]
|
|
74
|
+
* @param {string} [opts.filePath]
|
|
75
|
+
* @param {'parseFailure' | 'schemaFailure' | 'ioFailure'} [opts.code]
|
|
76
|
+
* @param {string} [opts.field]
|
|
77
|
+
*/
|
|
78
|
+
constructor(message, opts = {}) {
|
|
79
|
+
super(message);
|
|
80
|
+
this.name = 'FreezeRecordError';
|
|
81
|
+
this.filePath = opts.filePath;
|
|
82
|
+
this.code = opts.code ?? 'schemaFailure';
|
|
83
|
+
this.field = opts.field;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const SHA256_RE = /^sha256:[0-9a-f]{64}$/;
|
|
88
|
+
const ISO_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Validate the parsed body against the local schema described at the
|
|
92
|
+
* top of this file. Throws `FreezeRecordError` on the first violation
|
|
93
|
+
* (fail fast; the record is one file).
|
|
94
|
+
*
|
|
95
|
+
* @param {unknown} raw
|
|
96
|
+
* @param {string} [filePath]
|
|
97
|
+
* @returns {import('../query/delta.js').FreezeRecord & Record<string, unknown>}
|
|
98
|
+
*/
|
|
99
|
+
export function validateFreezeRecord(raw, filePath) {
|
|
100
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
101
|
+
throw new FreezeRecordError('Freeze record must be a JSON object.', {
|
|
102
|
+
filePath, field: '(root)',
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
const body = /** @type {Record<string, unknown>} */ (raw);
|
|
106
|
+
const require = /** @type {(field: string) => void} */ ((field) => {
|
|
107
|
+
if (!Object.prototype.hasOwnProperty.call(body, field)) {
|
|
108
|
+
throw new FreezeRecordError(`Freeze record missing required field '${field}'.`, {
|
|
109
|
+
filePath, field,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
require('frozenAt');
|
|
115
|
+
if (typeof body.frozenAt !== 'string' || !ISO_RE.test(body.frozenAt)) {
|
|
116
|
+
throw new FreezeRecordError(
|
|
117
|
+
"Freeze record 'frozenAt' must be an ISO-8601 timestamp string.",
|
|
118
|
+
{ filePath, field: 'frozenAt' },
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
require('treeHash');
|
|
123
|
+
if (typeof body.treeHash !== 'string' || !SHA256_RE.test(body.treeHash)) {
|
|
124
|
+
throw new FreezeRecordError(
|
|
125
|
+
"Freeze record 'treeHash' must be a 'sha256:<64-hex>' string.",
|
|
126
|
+
{ filePath, field: 'treeHash' },
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
require('docHashes');
|
|
131
|
+
const docHashes = body.docHashes;
|
|
132
|
+
if (docHashes === null || typeof docHashes !== 'object' || Array.isArray(docHashes)) {
|
|
133
|
+
throw new FreezeRecordError(
|
|
134
|
+
"Freeze record 'docHashes' must be an object mapping ids to sha256:<hex> strings.",
|
|
135
|
+
{ filePath, field: 'docHashes' },
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
for (const [id, hash] of Object.entries(docHashes)) {
|
|
139
|
+
if (typeof hash !== 'string' || !SHA256_RE.test(hash)) {
|
|
140
|
+
throw new FreezeRecordError(
|
|
141
|
+
`Freeze record docHashes['${id}'] must be a 'sha256:<64-hex>' string.`,
|
|
142
|
+
{ filePath, field: `docHashes.${id}` },
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
require('briefStatements');
|
|
148
|
+
if (
|
|
149
|
+
typeof body.briefStatements !== 'number'
|
|
150
|
+
|| !Number.isInteger(body.briefStatements)
|
|
151
|
+
|| body.briefStatements < 0
|
|
152
|
+
) {
|
|
153
|
+
throw new FreezeRecordError(
|
|
154
|
+
"Freeze record 'briefStatements' must be a non-negative integer.",
|
|
155
|
+
{ filePath, field: 'briefStatements' },
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
require('override');
|
|
160
|
+
const override = body.override;
|
|
161
|
+
if (override !== null) {
|
|
162
|
+
if (override === undefined || typeof override !== 'object' || Array.isArray(override)) {
|
|
163
|
+
throw new FreezeRecordError(
|
|
164
|
+
"Freeze record 'override' must be null or { reason, by, at }.",
|
|
165
|
+
{ filePath, field: 'override' },
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
const o = /** @type {Record<string, unknown>} */ (override);
|
|
169
|
+
for (const f of ['reason', 'by', 'at']) {
|
|
170
|
+
if (typeof o[f] !== 'string' || o[f].length === 0) {
|
|
171
|
+
throw new FreezeRecordError(
|
|
172
|
+
`Freeze record 'override.${f}' must be a non-empty string.`,
|
|
173
|
+
{ filePath, field: `override.${f}` },
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return /** @type {import('../query/delta.js').FreezeRecord & Record<string, unknown>} */ (body);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Load and validate the freeze record for a project. Returns null
|
|
184
|
+
* when the file does not exist; throws `FreezeRecordError` on parse
|
|
185
|
+
* or schema failure.
|
|
186
|
+
*
|
|
187
|
+
* @param {object} args
|
|
188
|
+
* @param {string} args.projectRoot
|
|
189
|
+
* @returns {Promise<(import('../query/delta.js').FreezeRecord & Record<string, unknown>) | null>}
|
|
190
|
+
*/
|
|
191
|
+
export async function loadFreezeRecord({ projectRoot }) {
|
|
192
|
+
const filePath = freezeRecordPath(projectRoot);
|
|
193
|
+
const relPath = FREEZE_RECORD_REL_PATH;
|
|
194
|
+
let raw;
|
|
195
|
+
try {
|
|
196
|
+
raw = await readFile(filePath, 'utf8');
|
|
197
|
+
} catch (err) {
|
|
198
|
+
if (/** @type {NodeJS.ErrnoException} */ (err).code === 'ENOENT') return null;
|
|
199
|
+
throw new FreezeRecordError(
|
|
200
|
+
`Failed to read ${relPath}: ${/** @type {Error} */ (err).message}`,
|
|
201
|
+
{ filePath: relPath, code: 'ioFailure' },
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
let parsed;
|
|
205
|
+
try {
|
|
206
|
+
parsed = JSON.parse(raw);
|
|
207
|
+
} catch (err) {
|
|
208
|
+
throw new FreezeRecordError(
|
|
209
|
+
`Freeze record parse failed: ${/** @type {Error} */ (err).message}`,
|
|
210
|
+
{ filePath: relPath, code: 'parseFailure' },
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
// Back-compat: older freeze records were written before override
|
|
214
|
+
// became a required-nullable field. Fill in null on read so a
|
|
215
|
+
// pre-slice-3 record survives; the writer stamps override: null on
|
|
216
|
+
// every fresh freeze so the on-disk shape converges on the current
|
|
217
|
+
// schema without a migration step.
|
|
218
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
219
|
+
&& !Object.prototype.hasOwnProperty.call(parsed, 'override')) {
|
|
220
|
+
/** @type {Record<string, unknown>} */ (parsed).override = null;
|
|
221
|
+
}
|
|
222
|
+
return validateFreezeRecord(parsed, relPath);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Write the freeze record. Validates first (so a bad in-memory record
|
|
227
|
+
* never lands as JSON on disk), creates `rcf/define/` if needed, and
|
|
228
|
+
* writes pretty-printed JSON with a trailing newline (matches the
|
|
229
|
+
* neighbouring rcf/*.json files' shape).
|
|
230
|
+
*
|
|
231
|
+
* The slice-3 `rcf define freeze` verb is the intended caller; this
|
|
232
|
+
* slice exposes the writer so that verb can be built without another
|
|
233
|
+
* refactor. Every direct caller is expected to have run
|
|
234
|
+
* `computeReadiness` first and only write when the tree is freezeable.
|
|
235
|
+
*
|
|
236
|
+
* @param {object} args
|
|
237
|
+
* @param {string} args.projectRoot
|
|
238
|
+
* @param {import('../query/delta.js').FreezeRecord & Record<string, unknown>} args.record
|
|
239
|
+
* @returns {Promise<{ filePath: string }>}
|
|
240
|
+
*/
|
|
241
|
+
export async function saveFreezeRecord({ projectRoot, record }) {
|
|
242
|
+
const validated = validateFreezeRecord(record, FREEZE_RECORD_REL_PATH);
|
|
243
|
+
const filePath = freezeRecordPath(projectRoot);
|
|
244
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
245
|
+
await writeFile(filePath, `${JSON.stringify(validated, null, 2)}\n`, 'utf8');
|
|
246
|
+
return { filePath };
|
|
247
|
+
}
|