rcf-lite 0.28.1 → 0.28.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/CHANGELOG.md +310 -0
  2. package/bin/rcf.js +4 -0
  3. package/fixtures/canary-manifest.json +9 -9
  4. package/package.json +1 -1
  5. package/rcf/adrs/adr-4120-canonical-json-hashing.json +30 -0
  6. package/rcf/adrs/adr-4121-define-sidecars.json +30 -0
  7. package/rcf/adrs/adr-4122-define-gates-blocking-and-optout.json +30 -0
  8. package/rcf/adrs/adr-4123-readiness-single-object.json +30 -0
  9. package/rcf/code-nodes/cn-602.json +3 -2
  10. package/rcf/code-nodes/cn-700.json +23 -0
  11. package/rcf/code-nodes/cn-701.json +14 -0
  12. package/rcf/code-nodes/cn-702.json +14 -0
  13. package/rcf/code-nodes/cn-703.json +16 -0
  14. package/rcf/code-nodes/cn-720.json +15 -0
  15. package/rcf/code-nodes/cn-721.json +15 -0
  16. package/rcf/code-nodes/cn-722.json +15 -0
  17. package/rcf/code-nodes/cn-723.json +18 -0
  18. package/rcf/code-nodes/cn-724.json +14 -0
  19. package/rcf/code-nodes/cn-725.json +21 -0
  20. package/rcf/code-nodes/cn-726.json +17 -0
  21. package/rcf/code-nodes/cn-727.json +14 -0
  22. package/rcf/code-nodes/cn-728.json +21 -0
  23. package/rcf/code-nodes/cn-729.json +18 -0
  24. package/rcf/code-nodes/cn-730.json +14 -0
  25. package/rcf/fbs/fbs-181.json +3 -2
  26. package/rcf/fbs/fbs-190.json +37 -0
  27. package/rcf/fbs/fbs-191.json +34 -0
  28. package/rcf/fbs/fbs-192.json +17 -0
  29. package/rcf/fbs/fbs-193.json +37 -0
  30. package/rcf/fbs/fbs-194.json +35 -0
  31. package/rcf/fbs/fbs-195.json +37 -0
  32. package/rcf/fbs/fbs-196.json +37 -0
  33. package/rcf/fbs/fbs-197.json +41 -0
  34. package/rcf/requirements/req-001.json +2 -1
  35. package/rcf/requirements/req-002.json +2 -1
  36. package/rcf/requirements/req-003.json +2 -1
  37. package/rcf/requirements/req-004.json +2 -1
  38. package/rcf/requirements/req-005.json +2 -1
  39. package/rcf/requirements/req-006.json +2 -1
  40. package/rcf/requirements/req-007.json +2 -1
  41. package/rcf/requirements/req-008.json +2 -1
  42. package/rcf/requirements/req-009.json +2 -1
  43. package/rcf/requirements/req-010.json +5 -2
  44. package/rcf/requirements/req-011.json +2 -1
  45. package/rcf/requirements/req-012.json +2 -1
  46. package/rcf/requirements/req-013.json +2 -1
  47. package/rcf/requirements/req-014.json +2 -1
  48. package/rcf/requirements/req-015.json +2 -1
  49. package/rcf/requirements/req-016.json +5 -2
  50. package/rcf/requirements/req-017.json +6 -2
  51. package/rcf/requirements/req-018.json +5 -2
  52. package/rcf/requirements/req-019.json +5 -2
  53. package/rcf/requirements/req-020.json +5 -2
  54. package/rcf/requirements/req-021.json +5 -2
  55. package/rcf/requirements/req-022.json +5 -2
  56. package/rcf/requirements/req-023.json +6 -4
  57. package/rcf/requirements/req-024.json +5 -2
  58. package/rcf/requirements/req-025.json +5 -2
  59. package/rcf/requirements/req-026.json +5 -2
  60. package/rcf/requirements/req-027.json +5 -2
  61. package/rcf/requirements/req-028.json +5 -2
  62. package/rcf/requirements/req-029.json +5 -2
  63. package/rcf/requirements/req-030.json +5 -2
  64. package/rcf/requirements/req-040.json +5 -2
  65. package/rcf/requirements/req-041.json +5 -2
  66. package/rcf/requirements/req-042.json +5 -2
  67. package/rcf/requirements/req-043.json +5 -2
  68. package/rcf/requirements/req-050.json +5 -2
  69. package/rcf/requirements/req-051.json +5 -2
  70. package/rcf/requirements/req-052.json +5 -2
  71. package/rcf/requirements/req-053.json +5 -2
  72. package/rcf/requirements/req-054.json +5 -2
  73. package/rcf/requirements/req-060.json +5 -2
  74. package/rcf/requirements/req-061.json +5 -2
  75. package/rcf/requirements/req-062.json +5 -2
  76. package/rcf/requirements/req-063.json +5 -2
  77. package/rcf/requirements/req-070.json +5 -2
  78. package/rcf/requirements/req-071.json +5 -2
  79. package/rcf/requirements/req-072.json +5 -2
  80. package/rcf/requirements/req-073.json +5 -2
  81. package/rcf/requirements/req-074.json +5 -2
  82. package/rcf/requirements/req-075.json +5 -2
  83. package/rcf/requirements/req-076.json +5 -2
  84. package/rcf/requirements/req-077.json +5 -2
  85. package/rcf/requirements/req-080.json +5 -2
  86. package/rcf/requirements/req-081.json +5 -2
  87. package/rcf/requirements/req-082.json +5 -2
  88. package/rcf/requirements/req-083.json +5 -2
  89. package/rcf/requirements/req-084.json +5 -2
  90. package/rcf/requirements/req-085.json +5 -2
  91. package/rcf/requirements/req-086.json +5 -2
  92. package/rcf/requirements/req-090.json +5 -2
  93. package/rcf/requirements/req-091.json +5 -2
  94. package/rcf/requirements/req-092.json +5 -2
  95. package/rcf/requirements/req-093.json +5 -2
  96. package/rcf/requirements/req-094.json +5 -2
  97. package/rcf/requirements/req-100.json +5 -2
  98. package/rcf/requirements/req-101.json +5 -2
  99. package/rcf/requirements/req-102.json +5 -2
  100. package/rcf/requirements/req-103.json +5 -2
  101. package/rcf/requirements/req-104.json +5 -2
  102. package/rcf/requirements/req-110.json +5 -2
  103. package/rcf/requirements/req-111.json +5 -2
  104. package/rcf/requirements/req-112.json +5 -2
  105. package/rcf/requirements/req-113.json +5 -2
  106. package/rcf/requirements/req-114.json +5 -2
  107. package/rcf/requirements/req-115.json +5 -2
  108. package/rcf/requirements/req-120.json +5 -2
  109. package/rcf/requirements/req-121.json +5 -2
  110. package/rcf/requirements/req-122.json +5 -2
  111. package/rcf/requirements/req-123.json +5 -2
  112. package/rcf/requirements/req-124.json +5 -2
  113. package/rcf/requirements/req-125.json +5 -2
  114. package/rcf/requirements/req-130.json +6 -2
  115. package/rcf/requirements/req-131.json +6 -2
  116. package/rcf/requirements/req-132.json +6 -2
  117. package/rcf/requirements/req-133.json +6 -2
  118. package/rcf/requirements/req-134.json +6 -2
  119. package/rcf/requirements/req-140.json +5 -2
  120. package/rcf/requirements/req-145.json +5 -2
  121. package/rcf/requirements/req-150.json +6 -2
  122. package/rcf/requirements/req-151.json +6 -2
  123. package/rcf/requirements/req-152.json +6 -2
  124. package/rcf/requirements/req-153.json +6 -2
  125. package/rcf/requirements/req-154.json +5 -2
  126. package/rcf/requirements/req-155.json +4 -1
  127. package/rcf/requirements/req-156.json +4 -1
  128. package/rcf/requirements/req-157.json +4 -1
  129. package/rcf/requirements/req-158.json +4 -1
  130. package/rcf/requirements/req-159.json +4 -1
  131. package/rcf/requirements/req-160.json +4 -1
  132. package/rcf/requirements/req-161.json +4 -1
  133. package/rcf/requirements/req-162.json +4 -1
  134. package/rcf/requirements/req-163.json +5 -2
  135. package/rcf/requirements/req-164.json +5 -2
  136. package/rcf/requirements/req-165.json +5 -2
  137. package/rcf/requirements/req-170.json +33 -0
  138. package/rcf/requirements/req-171.json +44 -0
  139. package/rcf/requirements/req-172.json +25 -0
  140. package/rcf/requirements/req-173.json +25 -0
  141. package/rcf/requirements/req-174.json +25 -0
  142. package/rcf/requirements/req-175.json +25 -0
  143. package/rcf/tacs/tac-4103-feedback-redact.json +2 -2
  144. package/rcf/tacs/tac-4120-define-delta-and-freeze.json +21 -0
  145. package/rcf/tacs/tac-4121-define-ledgers.json +23 -0
  146. package/rcf/tacs/tac-4122-define-gates.json +25 -0
  147. package/rcf/tacs/tac-4123-define-readiness.json +25 -0
  148. package/rcf/test-suites/ts-188.json +10 -2
  149. package/rcf/test-suites/ts-191.json +19 -5
  150. package/rcf/test-suites/ts-204.json +30 -0
  151. package/rcf/test-suites/ts-205.json +22 -0
  152. package/rcf/test-suites/ts-206.json +22 -0
  153. package/rcf/test-suites/ts-207.json +22 -0
  154. package/rcf/test-suites/ts-208.json +22 -0
  155. package/rcf/test-suites/ts-209.json +22 -0
  156. package/rcf/test-suites/ts-210.json +22 -0
  157. package/rcf/test-suites/ts-211.json +22 -0
  158. package/rcf/test-suites/ts-212.json +22 -0
  159. package/rcf/test-suites/ts-213.json +50 -0
  160. package/rcf/test-suites/ts-214.json +74 -0
  161. package/rcf/test-suites/ts-215.json +67 -0
  162. package/rcf/test-suites/ts-216.json +78 -0
  163. package/rcf/test-suites/ts-217.json +62 -0
  164. package/rcf/user-stories/us-15601.json +10 -1
  165. package/rcf/user-stories/us-15701.json +4 -4
  166. package/rcf/user-stories/us-17001.json +27 -0
  167. package/rcf/user-stories/us-17002.json +22 -0
  168. package/rcf/user-stories/us-17003.json +22 -0
  169. package/rcf/user-stories/us-17004.json +22 -0
  170. package/rcf/user-stories/us-17005.json +22 -0
  171. package/rcf/user-stories/us-17006.json +21 -0
  172. package/rcf/user-stories/us-17007.json +21 -0
  173. package/rcf/user-stories/us-17008.json +22 -0
  174. package/rcf/user-stories/us-17101.json +22 -0
  175. package/rcf/user-stories/us-17102.json +22 -0
  176. package/rcf/user-stories/us-17201.json +38 -0
  177. package/rcf/user-stories/us-17301.json +38 -0
  178. package/rcf/user-stories/us-17401.json +58 -0
  179. package/rcf/user-stories/us-17501.json +48 -0
  180. package/releases/releases.yaml +31 -1
  181. package/src/browser-verify/manifest-writer.js +9 -5
  182. package/src/cli/browser-verify.js +9 -1
  183. package/src/cli/feedback.js +316 -32
  184. package/src/cli/help.js +11 -0
  185. package/src/cli/intake.js +25 -4
  186. package/src/cli/ledger.js +399 -0
  187. package/src/cli/preflight.js +24 -3
  188. package/src/cli/readiness.js +280 -0
  189. package/src/cli/review.js +9 -1
  190. package/src/cli/ui-baseline.js +14 -1
  191. package/src/core/store/index.js +5 -0
  192. package/src/core/store/validate-composed-record.js +95 -0
  193. package/src/core/store/writer.js +86 -6
  194. package/src/define/freeze-record.js +232 -0
  195. package/src/define/ledgers.js +513 -0
  196. package/src/feedback/gh.js +27 -1
  197. package/src/feedback/redact-allowlist.json +7 -0
  198. package/src/feedback/redact.js +126 -11
  199. package/src/feedback/render.js +34 -12
  200. package/src/intake/manifest-writer.js +6 -3
  201. package/src/intake/orchestrator.js +44 -7
  202. package/src/preflight/manifest-writer.js +11 -11
  203. package/src/query/delta.js +276 -0
  204. package/src/query/gates.js +832 -0
  205. package/src/query/index.js +36 -0
  206. package/src/query/readiness.js +333 -0
  207. package/src/req-baseline/sweep.js +34 -5
  208. package/src/server/index.js +37 -0
  209. package/src/server/routes.js +65 -3
  210. package/src/ui-baseline/manifest-writer.js +11 -10
  211. package/src/view/doc-renderers/helpers.js +39 -2
  212. package/src/view/doc-renderers/req.js +10 -2
  213. package/src/view/doc-renderers/tad.js +67 -6
  214. package/src/view/doc-renderers/user-story.js +10 -5
  215. package/src/view/html-page.js +347 -28
  216. package/src/view/index.js +14 -1
  217. package/src/view/live-client.js +22 -2
  218. package/src/view/product-map.js +751 -0
  219. package/src/view/style.css +383 -0
@@ -0,0 +1,232 @@
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
+ // The record is EXTENSIBLE: later slices add fields (2 gates the freeze
31
+ // verb wrote at freeze time, 4 the `bundle --next` refusal read at
32
+ // override time). This slice validates the fields it consumes and
33
+ // tolerates the rest; unknown properties survive a load/save
34
+ // round-trip so a newer rcf-lite writing the file does not lose them
35
+ // to an older reader mid-migration.
36
+
37
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
38
+ import { dirname, join } from 'node:path';
39
+
40
+ /**
41
+ * Relative path (under the project root) of the freeze record. Used by
42
+ * the walker-invisibility invariant test and by every caller that
43
+ * wants to log the path in an error message.
44
+ */
45
+ export const FREEZE_RECORD_REL_PATH = 'rcf/define/freeze.json';
46
+
47
+ /**
48
+ * Absolute path of the freeze record for a project root.
49
+ *
50
+ * @param {string} projectRoot
51
+ * @returns {string}
52
+ */
53
+ export function freezeRecordPath(projectRoot) {
54
+ return join(projectRoot, FREEZE_RECORD_REL_PATH);
55
+ }
56
+
57
+ /**
58
+ * Structured error thrown on a malformed freeze record. Carries the
59
+ * file path and a short reason. `code` fixes the class so callers can
60
+ * distinguish parse failures from schema failures without matching on
61
+ * the message.
62
+ */
63
+ export class FreezeRecordError extends Error {
64
+ /**
65
+ * @param {string} message
66
+ * @param {object} [opts]
67
+ * @param {string} [opts.filePath]
68
+ * @param {'parseFailure' | 'schemaFailure' | 'ioFailure'} [opts.code]
69
+ * @param {string} [opts.field]
70
+ */
71
+ constructor(message, opts = {}) {
72
+ super(message);
73
+ this.name = 'FreezeRecordError';
74
+ this.filePath = opts.filePath;
75
+ this.code = opts.code ?? 'schemaFailure';
76
+ this.field = opts.field;
77
+ }
78
+ }
79
+
80
+ const SHA256_RE = /^sha256:[0-9a-f]{64}$/;
81
+ const ISO_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
82
+
83
+ /**
84
+ * Validate the parsed body against the local schema described at the
85
+ * top of this file. Throws `FreezeRecordError` on the first violation
86
+ * (fail fast; the record is one file).
87
+ *
88
+ * @param {unknown} raw
89
+ * @param {string} [filePath]
90
+ * @returns {import('../query/delta.js').FreezeRecord & Record<string, unknown>}
91
+ */
92
+ export function validateFreezeRecord(raw, filePath) {
93
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
94
+ throw new FreezeRecordError('Freeze record must be a JSON object.', {
95
+ filePath, field: '(root)',
96
+ });
97
+ }
98
+ const body = /** @type {Record<string, unknown>} */ (raw);
99
+ const require = /** @type {(field: string) => void} */ ((field) => {
100
+ if (!Object.prototype.hasOwnProperty.call(body, field)) {
101
+ throw new FreezeRecordError(`Freeze record missing required field '${field}'.`, {
102
+ filePath, field,
103
+ });
104
+ }
105
+ });
106
+
107
+ require('frozenAt');
108
+ if (typeof body.frozenAt !== 'string' || !ISO_RE.test(body.frozenAt)) {
109
+ throw new FreezeRecordError(
110
+ "Freeze record 'frozenAt' must be an ISO-8601 timestamp string.",
111
+ { filePath, field: 'frozenAt' },
112
+ );
113
+ }
114
+
115
+ require('treeHash');
116
+ if (typeof body.treeHash !== 'string' || !SHA256_RE.test(body.treeHash)) {
117
+ throw new FreezeRecordError(
118
+ "Freeze record 'treeHash' must be a 'sha256:<64-hex>' string.",
119
+ { filePath, field: 'treeHash' },
120
+ );
121
+ }
122
+
123
+ require('docHashes');
124
+ const docHashes = body.docHashes;
125
+ if (docHashes === null || typeof docHashes !== 'object' || Array.isArray(docHashes)) {
126
+ throw new FreezeRecordError(
127
+ "Freeze record 'docHashes' must be an object mapping ids to sha256:<hex> strings.",
128
+ { filePath, field: 'docHashes' },
129
+ );
130
+ }
131
+ for (const [id, hash] of Object.entries(docHashes)) {
132
+ if (typeof hash !== 'string' || !SHA256_RE.test(hash)) {
133
+ throw new FreezeRecordError(
134
+ `Freeze record docHashes['${id}'] must be a 'sha256:<64-hex>' string.`,
135
+ { filePath, field: `docHashes.${id}` },
136
+ );
137
+ }
138
+ }
139
+
140
+ require('briefStatements');
141
+ if (
142
+ typeof body.briefStatements !== 'number'
143
+ || !Number.isInteger(body.briefStatements)
144
+ || body.briefStatements < 0
145
+ ) {
146
+ throw new FreezeRecordError(
147
+ "Freeze record 'briefStatements' must be a non-negative integer.",
148
+ { filePath, field: 'briefStatements' },
149
+ );
150
+ }
151
+
152
+ if (Object.prototype.hasOwnProperty.call(body, 'override')) {
153
+ const override = body.override;
154
+ if (override !== null) {
155
+ if (override === undefined || typeof override !== 'object' || Array.isArray(override)) {
156
+ throw new FreezeRecordError(
157
+ "Freeze record 'override' must be null or { reason, by, at }.",
158
+ { filePath, field: 'override' },
159
+ );
160
+ }
161
+ const o = /** @type {Record<string, unknown>} */ (override);
162
+ for (const f of ['reason', 'by', 'at']) {
163
+ if (typeof o[f] !== 'string' || o[f].length === 0) {
164
+ throw new FreezeRecordError(
165
+ `Freeze record 'override.${f}' must be a non-empty string.`,
166
+ { filePath, field: `override.${f}` },
167
+ );
168
+ }
169
+ }
170
+ }
171
+ }
172
+
173
+ return /** @type {import('../query/delta.js').FreezeRecord & Record<string, unknown>} */ (body);
174
+ }
175
+
176
+ /**
177
+ * Load and validate the freeze record for a project. Returns null
178
+ * when the file does not exist; throws `FreezeRecordError` on parse
179
+ * or schema failure.
180
+ *
181
+ * @param {object} args
182
+ * @param {string} args.projectRoot
183
+ * @returns {Promise<(import('../query/delta.js').FreezeRecord & Record<string, unknown>) | null>}
184
+ */
185
+ export async function loadFreezeRecord({ projectRoot }) {
186
+ const filePath = freezeRecordPath(projectRoot);
187
+ const relPath = FREEZE_RECORD_REL_PATH;
188
+ let raw;
189
+ try {
190
+ raw = await readFile(filePath, 'utf8');
191
+ } catch (err) {
192
+ if (/** @type {NodeJS.ErrnoException} */ (err).code === 'ENOENT') return null;
193
+ throw new FreezeRecordError(
194
+ `Failed to read ${relPath}: ${/** @type {Error} */ (err).message}`,
195
+ { filePath: relPath, code: 'ioFailure' },
196
+ );
197
+ }
198
+ let parsed;
199
+ try {
200
+ parsed = JSON.parse(raw);
201
+ } catch (err) {
202
+ throw new FreezeRecordError(
203
+ `Freeze record parse failed: ${/** @type {Error} */ (err).message}`,
204
+ { filePath: relPath, code: 'parseFailure' },
205
+ );
206
+ }
207
+ return validateFreezeRecord(parsed, relPath);
208
+ }
209
+
210
+ /**
211
+ * Write the freeze record. Validates first (so a bad in-memory record
212
+ * never lands as JSON on disk), creates `rcf/define/` if needed, and
213
+ * writes pretty-printed JSON with a trailing newline (matches the
214
+ * neighbouring rcf/*.json files' shape).
215
+ *
216
+ * The slice-3 `rcf define freeze` verb is the intended caller; this
217
+ * slice exposes the writer so that verb can be built without another
218
+ * refactor. Every direct caller is expected to have run
219
+ * `computeReadiness` first and only write when the tree is freezeable.
220
+ *
221
+ * @param {object} args
222
+ * @param {string} args.projectRoot
223
+ * @param {import('../query/delta.js').FreezeRecord & Record<string, unknown>} args.record
224
+ * @returns {Promise<{ filePath: string }>}
225
+ */
226
+ export async function saveFreezeRecord({ projectRoot, record }) {
227
+ const validated = validateFreezeRecord(record, FREEZE_RECORD_REL_PATH);
228
+ const filePath = freezeRecordPath(projectRoot);
229
+ await mkdir(dirname(filePath), { recursive: true });
230
+ await writeFile(filePath, `${JSON.stringify(validated, null, 2)}\n`, 'utf8');
231
+ return { filePath };
232
+ }