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.
Files changed (327) hide show
  1. package/CHANGELOG.md +317 -0
  2. package/bin/rcf.js +8 -0
  3. package/fixtures/canary-manifest.json +9 -9
  4. package/guidance/define-draft-shapes.md +89 -0
  5. package/guidance/define-intake.md +97 -0
  6. package/guidance/define-intent.md +107 -0
  7. package/guidance/discover-prototype.md +67 -0
  8. package/guidance/discovery-prototypes.md +69 -0
  9. package/guidance/harness-template.md +34 -0
  10. package/guidance/managed/agent-instructions-block.hash +1 -1
  11. package/guidance/managed/agent-instructions-block.md +34 -0
  12. package/guidance/manifest.json +9 -2
  13. package/package.json +1 -1
  14. package/rcf/adrs/adr-4120-canonical-json-hashing.json +30 -0
  15. package/rcf/adrs/adr-4121-define-sidecars.json +30 -0
  16. package/rcf/adrs/adr-4122-define-gates-blocking-and-optout.json +30 -0
  17. package/rcf/adrs/adr-4123-readiness-single-object.json +30 -0
  18. package/rcf/adrs/adr-4124-define-freeze-verb.json +30 -0
  19. package/rcf/adrs/adr-4126-persona-tagged-blockers-two-levels.json +40 -0
  20. package/rcf/adrs/adr-4127-readiness-tab-order-and-register.json +40 -0
  21. package/rcf/adrs/adr-4129-questions-harness-driven-loop.json +25 -0
  22. package/rcf/adrs/adr-4130-intake-harness-split-tool-scanned.json +20 -0
  23. package/rcf/adrs/adr-4132-prototypes-in-discovery-as-evidence.json +20 -0
  24. package/rcf/adrs/adr-4133-dag-dependency-depth-layout.json +40 -0
  25. package/rcf/adrs/adr-4134-index-json-route.json +35 -0
  26. package/rcf/adrs/adr-4135-readiness-po-register-and-adapter.json +35 -0
  27. package/rcf/adrs/adr-4136-embedding-host-fixture.json +35 -0
  28. package/rcf/code-nodes/cn-700.json +23 -0
  29. package/rcf/code-nodes/cn-701.json +14 -0
  30. package/rcf/code-nodes/cn-702.json +14 -0
  31. package/rcf/code-nodes/cn-703.json +16 -0
  32. package/rcf/code-nodes/cn-720.json +15 -0
  33. package/rcf/code-nodes/cn-721.json +15 -0
  34. package/rcf/code-nodes/cn-722.json +15 -0
  35. package/rcf/code-nodes/cn-723.json +18 -0
  36. package/rcf/code-nodes/cn-724.json +14 -0
  37. package/rcf/code-nodes/cn-725.json +37 -0
  38. package/rcf/code-nodes/cn-726.json +25 -0
  39. package/rcf/code-nodes/cn-727.json +23 -0
  40. package/rcf/code-nodes/cn-728.json +37 -0
  41. package/rcf/code-nodes/cn-729.json +26 -0
  42. package/rcf/code-nodes/cn-730.json +24 -0
  43. package/rcf/code-nodes/cn-731.json +20 -0
  44. package/rcf/code-nodes/cn-732.json +14 -0
  45. package/rcf/code-nodes/cn-733.json +14 -0
  46. package/rcf/code-nodes/cn-734.json +14 -0
  47. package/rcf/code-nodes/cn-735.json +20 -0
  48. package/rcf/code-nodes/cn-736.json +14 -0
  49. package/rcf/code-nodes/cn-737.json +14 -0
  50. package/rcf/code-nodes/cn-738.json +14 -0
  51. package/rcf/code-nodes/cn-739.json +18 -0
  52. package/rcf/code-nodes/cn-740.json +19 -0
  53. package/rcf/code-nodes/cn-741.json +16 -0
  54. package/rcf/code-nodes/cn-742.json +16 -0
  55. package/rcf/code-nodes/cn-743.json +15 -0
  56. package/rcf/code-nodes/cn-744.json +22 -0
  57. package/rcf/code-nodes/cn-745.json +16 -0
  58. package/rcf/code-nodes/cn-746.json +14 -0
  59. package/rcf/code-nodes/cn-747.json +18 -0
  60. package/rcf/code-nodes/cn-748.json +16 -0
  61. package/rcf/code-nodes/cn-749.json +16 -0
  62. package/rcf/code-nodes/cn-750.json +15 -0
  63. package/rcf/code-nodes/cn-751.json +18 -0
  64. package/rcf/code-nodes/cn-752.json +14 -0
  65. package/rcf/fbs/fbs-190.json +37 -0
  66. package/rcf/fbs/fbs-191.json +34 -0
  67. package/rcf/fbs/fbs-192.json +17 -0
  68. package/rcf/fbs/fbs-193.json +37 -0
  69. package/rcf/fbs/fbs-194.json +35 -0
  70. package/rcf/fbs/fbs-195.json +37 -0
  71. package/rcf/fbs/fbs-196.json +45 -0
  72. package/rcf/fbs/fbs-197.json +41 -0
  73. package/rcf/fbs/fbs-198.json +42 -0
  74. package/rcf/fbs/fbs-199.json +59 -0
  75. package/rcf/fbs/fbs-200.json +46 -0
  76. package/rcf/fbs/fbs-201.json +49 -0
  77. package/rcf/fbs/fbs-202.json +110 -0
  78. package/rcf/fbs/fbs-203.json +100 -0
  79. package/rcf/requirements/req-001.json +2 -1
  80. package/rcf/requirements/req-002.json +3 -2
  81. package/rcf/requirements/req-003.json +2 -1
  82. package/rcf/requirements/req-004.json +2 -1
  83. package/rcf/requirements/req-005.json +2 -1
  84. package/rcf/requirements/req-006.json +2 -1
  85. package/rcf/requirements/req-007.json +2 -1
  86. package/rcf/requirements/req-008.json +2 -1
  87. package/rcf/requirements/req-009.json +2 -1
  88. package/rcf/requirements/req-010.json +5 -2
  89. package/rcf/requirements/req-011.json +2 -1
  90. package/rcf/requirements/req-012.json +2 -1
  91. package/rcf/requirements/req-013.json +2 -1
  92. package/rcf/requirements/req-014.json +2 -1
  93. package/rcf/requirements/req-015.json +2 -1
  94. package/rcf/requirements/req-016.json +5 -2
  95. package/rcf/requirements/req-017.json +6 -2
  96. package/rcf/requirements/req-018.json +5 -2
  97. package/rcf/requirements/req-019.json +5 -2
  98. package/rcf/requirements/req-020.json +5 -2
  99. package/rcf/requirements/req-021.json +5 -2
  100. package/rcf/requirements/req-022.json +5 -2
  101. package/rcf/requirements/req-023.json +6 -4
  102. package/rcf/requirements/req-024.json +5 -2
  103. package/rcf/requirements/req-025.json +5 -2
  104. package/rcf/requirements/req-026.json +5 -2
  105. package/rcf/requirements/req-027.json +5 -2
  106. package/rcf/requirements/req-028.json +5 -2
  107. package/rcf/requirements/req-029.json +5 -2
  108. package/rcf/requirements/req-030.json +5 -2
  109. package/rcf/requirements/req-040.json +5 -2
  110. package/rcf/requirements/req-041.json +5 -2
  111. package/rcf/requirements/req-042.json +5 -2
  112. package/rcf/requirements/req-043.json +5 -2
  113. package/rcf/requirements/req-050.json +5 -2
  114. package/rcf/requirements/req-051.json +5 -2
  115. package/rcf/requirements/req-052.json +5 -2
  116. package/rcf/requirements/req-053.json +5 -2
  117. package/rcf/requirements/req-054.json +5 -2
  118. package/rcf/requirements/req-060.json +5 -2
  119. package/rcf/requirements/req-061.json +5 -2
  120. package/rcf/requirements/req-062.json +5 -2
  121. package/rcf/requirements/req-063.json +5 -2
  122. package/rcf/requirements/req-070.json +5 -2
  123. package/rcf/requirements/req-071.json +5 -2
  124. package/rcf/requirements/req-072.json +5 -2
  125. package/rcf/requirements/req-073.json +5 -2
  126. package/rcf/requirements/req-074.json +5 -2
  127. package/rcf/requirements/req-075.json +5 -2
  128. package/rcf/requirements/req-076.json +5 -2
  129. package/rcf/requirements/req-077.json +5 -2
  130. package/rcf/requirements/req-080.json +5 -2
  131. package/rcf/requirements/req-081.json +5 -2
  132. package/rcf/requirements/req-082.json +5 -2
  133. package/rcf/requirements/req-083.json +5 -2
  134. package/rcf/requirements/req-084.json +5 -2
  135. package/rcf/requirements/req-085.json +5 -2
  136. package/rcf/requirements/req-086.json +5 -2
  137. package/rcf/requirements/req-090.json +5 -2
  138. package/rcf/requirements/req-091.json +5 -2
  139. package/rcf/requirements/req-092.json +5 -2
  140. package/rcf/requirements/req-093.json +5 -2
  141. package/rcf/requirements/req-094.json +5 -2
  142. package/rcf/requirements/req-100.json +5 -2
  143. package/rcf/requirements/req-101.json +5 -2
  144. package/rcf/requirements/req-102.json +5 -2
  145. package/rcf/requirements/req-103.json +5 -2
  146. package/rcf/requirements/req-104.json +5 -2
  147. package/rcf/requirements/req-110.json +5 -2
  148. package/rcf/requirements/req-111.json +5 -2
  149. package/rcf/requirements/req-112.json +5 -2
  150. package/rcf/requirements/req-113.json +5 -2
  151. package/rcf/requirements/req-114.json +5 -2
  152. package/rcf/requirements/req-115.json +5 -2
  153. package/rcf/requirements/req-120.json +5 -2
  154. package/rcf/requirements/req-121.json +5 -2
  155. package/rcf/requirements/req-122.json +5 -2
  156. package/rcf/requirements/req-123.json +5 -2
  157. package/rcf/requirements/req-124.json +5 -2
  158. package/rcf/requirements/req-125.json +5 -2
  159. package/rcf/requirements/req-130.json +6 -2
  160. package/rcf/requirements/req-131.json +6 -2
  161. package/rcf/requirements/req-132.json +6 -2
  162. package/rcf/requirements/req-133.json +6 -2
  163. package/rcf/requirements/req-134.json +6 -2
  164. package/rcf/requirements/req-140.json +5 -2
  165. package/rcf/requirements/req-145.json +5 -2
  166. package/rcf/requirements/req-150.json +6 -2
  167. package/rcf/requirements/req-151.json +6 -2
  168. package/rcf/requirements/req-152.json +6 -2
  169. package/rcf/requirements/req-153.json +6 -2
  170. package/rcf/requirements/req-154.json +5 -2
  171. package/rcf/requirements/req-155.json +4 -1
  172. package/rcf/requirements/req-156.json +4 -1
  173. package/rcf/requirements/req-157.json +4 -1
  174. package/rcf/requirements/req-158.json +4 -1
  175. package/rcf/requirements/req-159.json +4 -1
  176. package/rcf/requirements/req-160.json +4 -1
  177. package/rcf/requirements/req-161.json +4 -1
  178. package/rcf/requirements/req-162.json +4 -1
  179. package/rcf/requirements/req-163.json +5 -2
  180. package/rcf/requirements/req-164.json +5 -2
  181. package/rcf/requirements/req-165.json +5 -2
  182. package/rcf/requirements/req-170.json +33 -0
  183. package/rcf/requirements/req-171.json +44 -0
  184. package/rcf/requirements/req-172.json +25 -0
  185. package/rcf/requirements/req-173.json +25 -0
  186. package/rcf/requirements/req-174.json +31 -0
  187. package/rcf/requirements/req-175.json +31 -0
  188. package/rcf/requirements/req-176.json +25 -0
  189. package/rcf/requirements/req-180.json +31 -0
  190. package/rcf/requirements/req-181.json +31 -0
  191. package/rcf/requirements/req-182.json +25 -0
  192. package/rcf/requirements/req-186.json +25 -0
  193. package/rcf/requirements/req-187.json +31 -0
  194. package/rcf/requirements/req-188.json +25 -0
  195. package/rcf/tacs/tac-4120-define-delta-and-freeze.json +21 -0
  196. package/rcf/tacs/tac-4121-define-ledgers.json +35 -0
  197. package/rcf/tacs/tac-4122-define-gates.json +61 -0
  198. package/rcf/tacs/tac-4123-define-readiness.json +61 -0
  199. package/rcf/tacs/tac-4124-define-freeze.json +28 -0
  200. package/rcf/tacs/tac-4127-readiness-view.json +49 -0
  201. package/rcf/tacs/tac-4128-define-guidance.json +53 -0
  202. package/rcf/tacs/tac-4129-define-questions.json +45 -0
  203. package/rcf/tacs/tac-4130-define-intake-brief.json +35 -0
  204. package/rcf/tacs/tac-4131-build-dag-view.json +39 -0
  205. package/rcf/tacs/tac-4132-id-lookup.json +42 -0
  206. package/rcf/tacs/tac-4133-readiness-po-layer.json +37 -0
  207. package/rcf/tacs/tac-4134-wespa-host-fixture.json +54 -0
  208. package/rcf/test-suites/ts-016.json +3 -3
  209. package/rcf/test-suites/ts-204.json +30 -0
  210. package/rcf/test-suites/ts-205.json +22 -0
  211. package/rcf/test-suites/ts-206.json +22 -0
  212. package/rcf/test-suites/ts-207.json +22 -0
  213. package/rcf/test-suites/ts-208.json +22 -0
  214. package/rcf/test-suites/ts-209.json +22 -0
  215. package/rcf/test-suites/ts-210.json +22 -0
  216. package/rcf/test-suites/ts-211.json +22 -0
  217. package/rcf/test-suites/ts-212.json +22 -0
  218. package/rcf/test-suites/ts-213.json +50 -0
  219. package/rcf/test-suites/ts-214.json +74 -0
  220. package/rcf/test-suites/ts-215.json +211 -0
  221. package/rcf/test-suites/ts-216.json +94 -0
  222. package/rcf/test-suites/ts-217.json +62 -0
  223. package/rcf/test-suites/ts-218.json +78 -0
  224. package/rcf/test-suites/ts-219.json +70 -0
  225. package/rcf/test-suites/ts-220.json +78 -0
  226. package/rcf/test-suites/ts-221.json +78 -0
  227. package/rcf/test-suites/ts-222.json +100 -0
  228. package/rcf/test-suites/ts-223.json +94 -0
  229. package/rcf/test-suites/ts-224.json +70 -0
  230. package/rcf/test-suites/ts-225.json +78 -0
  231. package/rcf/test-suites/ts-226.json +158 -0
  232. package/rcf/test-suites/ts-227.json +38 -0
  233. package/rcf/test-suites/ts-228.json +86 -0
  234. package/rcf/test-suites/ts-229.json +83 -0
  235. package/rcf/user-stories/us-15601.json +4 -4
  236. package/rcf/user-stories/us-17001.json +27 -0
  237. package/rcf/user-stories/us-17002.json +22 -0
  238. package/rcf/user-stories/us-17003.json +22 -0
  239. package/rcf/user-stories/us-17004.json +22 -0
  240. package/rcf/user-stories/us-17005.json +22 -0
  241. package/rcf/user-stories/us-17006.json +21 -0
  242. package/rcf/user-stories/us-17007.json +21 -0
  243. package/rcf/user-stories/us-17008.json +22 -0
  244. package/rcf/user-stories/us-17101.json +22 -0
  245. package/rcf/user-stories/us-17102.json +22 -0
  246. package/rcf/user-stories/us-17201.json +38 -0
  247. package/rcf/user-stories/us-17301.json +38 -0
  248. package/rcf/user-stories/us-17302.json +50 -0
  249. package/rcf/user-stories/us-17401.json +70 -0
  250. package/rcf/user-stories/us-17402.json +55 -0
  251. package/rcf/user-stories/us-17403.json +60 -0
  252. package/rcf/user-stories/us-17501.json +48 -0
  253. package/rcf/user-stories/us-17502.json +60 -0
  254. package/rcf/user-stories/us-17503.json +65 -0
  255. package/rcf/user-stories/us-17601.json +58 -0
  256. package/rcf/user-stories/us-18001.json +55 -0
  257. package/rcf/user-stories/us-18002.json +35 -0
  258. package/rcf/user-stories/us-18101.json +35 -0
  259. package/rcf/user-stories/us-18202.json +40 -0
  260. package/rcf/user-stories/us-18601.json +55 -0
  261. package/rcf/user-stories/us-18602.json +50 -0
  262. package/rcf/user-stories/us-18603.json +45 -0
  263. package/rcf/user-stories/us-18701.json +50 -0
  264. package/rcf/user-stories/us-18702.json +50 -0
  265. package/rcf/user-stories/us-18801.json +45 -0
  266. package/rcf/user-stories/us-204.json +59 -0
  267. package/rcf/user-stories/us-205.json +64 -0
  268. package/rcf/user-stories/us-206.json +54 -0
  269. package/releases/releases.yaml +31 -1
  270. package/src/cli/feedback.js +126 -10
  271. package/src/cli/freeze.js +730 -0
  272. package/src/cli/help.js +23 -0
  273. package/src/cli/ledger.js +615 -0
  274. package/src/cli/questions.js +288 -0
  275. package/src/cli/readiness.js +498 -0
  276. package/src/cli/view.js +29 -0
  277. package/src/core/store/writer.js +86 -6
  278. package/src/define/freeze-record.js +247 -0
  279. package/src/define/ledgers.js +712 -0
  280. package/src/feedback/redact.js +105 -19
  281. package/src/intake/index.js +1 -1
  282. package/src/intake/orchestrator.js +117 -0
  283. package/src/mcp/tools.js +354 -0
  284. package/src/query/delta.js +282 -0
  285. package/src/query/gates.js +1107 -0
  286. package/src/query/index.js +50 -0
  287. package/src/query/questions.js +438 -0
  288. package/src/query/readiness.js +574 -0
  289. package/src/server/index.js +60 -1
  290. package/src/server/routes.js +133 -4
  291. package/src/setup/identity-seed.js +17 -0
  292. package/src/view/build-dag.js +563 -0
  293. package/src/view/components/badge.js +41 -0
  294. package/src/view/components/diff.js +72 -0
  295. package/src/view/components/doc-row.js +62 -0
  296. package/src/view/components/empty-state.js +35 -0
  297. package/src/view/components/entity-selector.js +137 -0
  298. package/src/view/components/filter-bar.js +97 -0
  299. package/src/view/components/findings-list.js +49 -0
  300. package/src/view/components/index.js +12 -0
  301. package/src/view/components/pill.js +46 -0
  302. package/src/view/components/sub-tab-strip.js +43 -0
  303. package/src/view/components/toast.js +25 -0
  304. package/src/view/components-fixture.js +265 -0
  305. package/src/view/doc-renderers/architecture.js +304 -0
  306. package/src/view/doc-renderers/build.js +160 -0
  307. package/src/view/doc-renderers/helpers.js +2 -2
  308. package/src/view/doc-renderers/index.js +2 -0
  309. package/src/view/doc-renderers/prd.js +35 -6
  310. package/src/view/doc-renderers/req.js +10 -2
  311. package/src/view/doc-renderers/user-story.js +10 -5
  312. package/src/view/html-page.js +717 -300
  313. package/src/view/index.js +109 -3
  314. package/src/view/live-client.js +61 -8
  315. package/src/view/needs-work.js +79 -0
  316. package/src/view/page-init.js +2193 -0
  317. package/src/view/product-map.js +751 -0
  318. package/src/view/readiness/phrasebook.js +115 -0
  319. package/src/view/readiness/po-layer.js +288 -0
  320. package/src/view/readiness/question-adapter.js +329 -0
  321. package/src/view/readiness/stage-legend.js +71 -0
  322. package/src/view/readiness.js +505 -0
  323. package/src/view/style.css +2279 -266
  324. package/src/view/test-host/embed-client.js +121 -0
  325. package/src/view/test-host/fixture.js +183 -0
  326. package/src/view/test-host/lints.js +220 -0
  327. 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
+ }