rcf-lite 0.0.1 → 0.7.1

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 (342) hide show
  1. package/CHANGELOG.md +288 -0
  2. package/LICENSE +202 -0
  3. package/README.md +92 -4
  4. package/bin/rcf-verify.js +122 -0
  5. package/bin/rcf.js +174 -0
  6. package/bin/view-supervisor-child.mjs +14 -0
  7. package/fixtures/canary-manifest.json +103 -0
  8. package/guidance/README.md +25 -0
  9. package/guidance/build-cycle-playbook.md +492 -0
  10. package/guidance/build-cycle.md +40 -0
  11. package/guidance/document-model.md +45 -0
  12. package/guidance/elicitation-playbook.md +442 -0
  13. package/guidance/harness-template.md +246 -0
  14. package/guidance/managed/README.md +63 -0
  15. package/guidance/managed/agent-instructions-block.hash +1 -0
  16. package/guidance/managed/agent-instructions-block.md +220 -0
  17. package/guidance/managed/legacy-fragment-hashes.json +22 -0
  18. package/guidance/manifest.json +21 -0
  19. package/guidance/overview.md +48 -0
  20. package/guidance/persona-programme.md +44 -0
  21. package/package.json +69 -8
  22. package/rcf/adrs/adr-001.json +25 -0
  23. package/rcf/adrs/adr-002.json +25 -0
  24. package/rcf/adrs/adr-003.json +25 -0
  25. package/rcf/adrs/adr-004.json +20 -0
  26. package/rcf/adrs/adr-005.json +20 -0
  27. package/rcf/adrs/adr-006.json +25 -0
  28. package/rcf/adrs/adr-007.json +25 -0
  29. package/rcf/adrs/adr-008.json +25 -0
  30. package/rcf/adrs/adr-009.json +25 -0
  31. package/rcf/build-sequence.json +11 -0
  32. package/rcf/code-nodes/cn-001.json +14 -0
  33. package/rcf/code-nodes/cn-002.json +14 -0
  34. package/rcf/code-nodes/cn-003.json +15 -0
  35. package/rcf/code-nodes/cn-004.json +17 -0
  36. package/rcf/code-nodes/cn-005.json +19 -0
  37. package/rcf/code-nodes/cn-006.json +17 -0
  38. package/rcf/code-nodes/cn-007.json +16 -0
  39. package/rcf/code-nodes/cn-008.json +19 -0
  40. package/rcf/code-nodes/cn-009.json +16 -0
  41. package/rcf/code-nodes/cn-010.json +18 -0
  42. package/rcf/code-nodes/cn-011.json +18 -0
  43. package/rcf/code-nodes/cn-012.json +18 -0
  44. package/rcf/code-nodes/cn-013.json +19 -0
  45. package/rcf/code-nodes/cn-014.json +16 -0
  46. package/rcf/code-nodes/cn-015.json +16 -0
  47. package/rcf/code-nodes/cn-016.json +16 -0
  48. package/rcf/code-nodes/cn-017.json +16 -0
  49. package/rcf/code-nodes/cn-018.json +15 -0
  50. package/rcf/code-nodes/cn-019.json +16 -0
  51. package/rcf/code-nodes/cn-020.json +16 -0
  52. package/rcf/code-nodes/cn-021.json +16 -0
  53. package/rcf/code-nodes/cn-022.json +16 -0
  54. package/rcf/code-nodes/cn-023.json +16 -0
  55. package/rcf/code-nodes/cn-024.json +16 -0
  56. package/rcf/code-nodes/cn-025.json +16 -0
  57. package/rcf/code-nodes/cn-026.json +16 -0
  58. package/rcf/code-nodes/cn-027.json +16 -0
  59. package/rcf/code-nodes/cn-028.json +16 -0
  60. package/rcf/code-nodes/cn-029.json +19 -0
  61. package/rcf/code-nodes/cn-030.json +14 -0
  62. package/rcf/code-nodes/cn-031.json +14 -0
  63. package/rcf/code-nodes/cn-032.json +14 -0
  64. package/rcf/code-nodes/cn-033.json +14 -0
  65. package/rcf/code-nodes/cn-034.json +14 -0
  66. package/rcf/code-nodes/cn-035.json +14 -0
  67. package/rcf/code-nodes/cn-036.json +14 -0
  68. package/rcf/code-nodes/cn-037.json +14 -0
  69. package/rcf/code-nodes/cn-038.json +14 -0
  70. package/rcf/code-nodes/cn-039.json +14 -0
  71. package/rcf/code-nodes/cn-040.json +14 -0
  72. package/rcf/code-nodes/cn-041.json +14 -0
  73. package/rcf/code-nodes/cn-042.json +14 -0
  74. package/rcf/code-nodes/cn-043.json +14 -0
  75. package/rcf/code-nodes/cn-044.json +14 -0
  76. package/rcf/code-nodes/cn-045.json +14 -0
  77. package/rcf/code-nodes/cn-046.json +14 -0
  78. package/rcf/code-nodes/cn-047.json +14 -0
  79. package/rcf/code-nodes/cn-048.json +14 -0
  80. package/rcf/code-nodes/cn-049.json +14 -0
  81. package/rcf/code-nodes/cn-050.json +14 -0
  82. package/rcf/code-nodes/cn-051.json +14 -0
  83. package/rcf/code-nodes/cn-052.json +14 -0
  84. package/rcf/code-nodes/cn-053.json +14 -0
  85. package/rcf/code-nodes/cn-054.json +14 -0
  86. package/rcf/code-nodes/cn-055.json +14 -0
  87. package/rcf/code-nodes/cn-056.json +14 -0
  88. package/rcf/code-nodes/cn-057.json +14 -0
  89. package/rcf/fbs/fbs-001.json +49 -0
  90. package/rcf/fbs/fbs-002.json +42 -0
  91. package/rcf/fbs/fbs-003.json +37 -0
  92. package/rcf/fbs/fbs-004.json +39 -0
  93. package/rcf/fbs/fbs-005.json +38 -0
  94. package/rcf/fbs/fbs-006.json +48 -0
  95. package/rcf/fbs/fbs-007.json +39 -0
  96. package/rcf/fbs/fbs-008.json +40 -0
  97. package/rcf/fbs/fbs-009.json +36 -0
  98. package/rcf/fbs/fbs-010.json +41 -0
  99. package/rcf/fbs/fbs-011.json +36 -0
  100. package/rcf/fbs/fbs-012.json +46 -0
  101. package/rcf/fbs/fbs-013.json +42 -0
  102. package/rcf/fbs/fbs-014.json +49 -0
  103. package/rcf/fbs/fbs-015.json +32 -0
  104. package/rcf/manifest.json +17 -0
  105. package/rcf/prd.json +47 -0
  106. package/rcf/requirements/req-001.json +19 -0
  107. package/rcf/requirements/req-002.json +19 -0
  108. package/rcf/requirements/req-003.json +19 -0
  109. package/rcf/requirements/req-004.json +19 -0
  110. package/rcf/requirements/req-005.json +19 -0
  111. package/rcf/requirements/req-006.json +19 -0
  112. package/rcf/requirements/req-007.json +19 -0
  113. package/rcf/requirements/req-008.json +19 -0
  114. package/rcf/requirements/req-009.json +19 -0
  115. package/rcf/tacs/tac-001.json +45 -0
  116. package/rcf/tacs/tac-002.json +109 -0
  117. package/rcf/tacs/tac-003.json +40 -0
  118. package/rcf/tacs/tac-004.json +51 -0
  119. package/rcf/tacs/tac-005.json +52 -0
  120. package/rcf/tacs/tac-006.json +104 -0
  121. package/rcf/tacs/tac-007.json +38 -0
  122. package/rcf/tacs/tac-008.json +51 -0
  123. package/rcf/tad.json +51 -0
  124. package/rcf/test-suites/PENDING.md +23 -0
  125. package/rcf/test-suites/ts-001.json +38 -0
  126. package/rcf/test-suites/ts-002.json +38 -0
  127. package/rcf/test-suites/ts-003.json +43 -0
  128. package/rcf/test-suites/ts-004.json +44 -0
  129. package/rcf/test-suites/ts-005.json +30 -0
  130. package/rcf/test-suites/ts-006.json +36 -0
  131. package/rcf/test-suites/ts-007.json +43 -0
  132. package/rcf/test-suites/ts-008.json +37 -0
  133. package/rcf/test-suites/ts-009.json +38 -0
  134. package/rcf/test-suites/ts-010.json +38 -0
  135. package/rcf/test-suites/ts-011.json +44 -0
  136. package/rcf/test-suites/ts-012.json +36 -0
  137. package/rcf/test-suites/ts-013.json +38 -0
  138. package/rcf/test-suites/ts-014.json +38 -0
  139. package/rcf/test-suites/ts-015.json +38 -0
  140. package/rcf/test-suites/ts-016.json +37 -0
  141. package/rcf/test-suites/ts-017.json +38 -0
  142. package/rcf/test-suites/ts-018.json +38 -0
  143. package/rcf/test-suites/ts-019.json +37 -0
  144. package/rcf/test-suites/ts-020.json +46 -0
  145. package/rcf/test-suites/ts-021.json +46 -0
  146. package/rcf/test-suites/ts-022.json +46 -0
  147. package/rcf/test-suites/ts-023.json +46 -0
  148. package/rcf/test-suites/ts-024.json +46 -0
  149. package/rcf/test-suites/ts-025.json +52 -0
  150. package/rcf/user-stories/us-101.json +40 -0
  151. package/rcf/user-stories/us-102.json +40 -0
  152. package/rcf/user-stories/us-201.json +40 -0
  153. package/rcf/user-stories/us-202.json +40 -0
  154. package/rcf/user-stories/us-203.json +32 -0
  155. package/rcf/user-stories/us-301.json +40 -0
  156. package/rcf/user-stories/us-302.json +40 -0
  157. package/rcf/user-stories/us-303.json +40 -0
  158. package/rcf/user-stories/us-304.json +40 -0
  159. package/rcf/user-stories/us-401.json +40 -0
  160. package/rcf/user-stories/us-402.json +40 -0
  161. package/rcf/user-stories/us-403.json +40 -0
  162. package/rcf/user-stories/us-501.json +40 -0
  163. package/rcf/user-stories/us-502.json +40 -0
  164. package/rcf/user-stories/us-503.json +40 -0
  165. package/rcf/user-stories/us-601.json +40 -0
  166. package/rcf/user-stories/us-602.json +40 -0
  167. package/rcf/user-stories/us-701.json +40 -0
  168. package/rcf/user-stories/us-702.json +40 -0
  169. package/rcf/user-stories/us-801.json +49 -0
  170. package/rcf/user-stories/us-802.json +49 -0
  171. package/rcf/user-stories/us-803.json +49 -0
  172. package/rcf/user-stories/us-804.json +49 -0
  173. package/rcf/user-stories/us-805.json +49 -0
  174. package/rcf/user-stories/us-901.json +40 -0
  175. package/src/.gitkeep +0 -0
  176. package/src/browser-verify/auth-smoke.js +109 -0
  177. package/src/browser-verify/index.js +29 -0
  178. package/src/browser-verify/invariants.js +336 -0
  179. package/src/browser-verify/manifest-writer.js +189 -0
  180. package/src/browser-verify/runner.js +171 -0
  181. package/src/build/bundle.js +198 -0
  182. package/src/build/formatters/json.js +18 -0
  183. package/src/build/formatters/markdown.js +406 -0
  184. package/src/build/index.js +14 -0
  185. package/src/build/mark.js +177 -0
  186. package/src/build/queue.js +285 -0
  187. package/src/cli/browser-verify.js +231 -0
  188. package/src/cli/build.js +584 -0
  189. package/src/cli/coverage.js +219 -0
  190. package/src/cli/create.js +355 -0
  191. package/src/cli/delete.js +127 -0
  192. package/src/cli/design.js +353 -0
  193. package/src/cli/doctor.js +548 -0
  194. package/src/cli/fbs.js +171 -0
  195. package/src/cli/finalise.js +377 -0
  196. package/src/cli/guidance.js +206 -0
  197. package/src/cli/help.js +156 -0
  198. package/src/cli/impact.js +119 -0
  199. package/src/cli/init.js +282 -0
  200. package/src/cli/intake.js +153 -0
  201. package/src/cli/link.js +128 -0
  202. package/src/cli/mcp.js +160 -0
  203. package/src/cli/preflight.js +220 -0
  204. package/src/cli/read.js +162 -0
  205. package/src/cli/req-baseline.js +269 -0
  206. package/src/cli/req-classify.js +135 -0
  207. package/src/cli/review.js +295 -0
  208. package/src/cli/test-suite.js +216 -0
  209. package/src/cli/trace.js +172 -0
  210. package/src/cli/ui-baseline.js +292 -0
  211. package/src/cli/ui-classify.js +108 -0
  212. package/src/cli/update.js +197 -0
  213. package/src/cli/validate.js +168 -0
  214. package/src/cli/view.js +452 -0
  215. package/src/core/baseline-catalog/data/auth.json +42 -0
  216. package/src/core/baseline-catalog/data/http-api.json +42 -0
  217. package/src/core/baseline-catalog/data/notifications.json +33 -0
  218. package/src/core/baseline-catalog/data/persistence.json +33 -0
  219. package/src/core/baseline-catalog/data/web-ui.json +60 -0
  220. package/src/core/baseline-catalog/index.js +121 -0
  221. package/src/core/errors/index.js +167 -0
  222. package/src/core/fixtures/register-canary/canary-prompt-01.json +18 -0
  223. package/src/core/fixtures/register-canary/canary-prompt-02.json +21 -0
  224. package/src/core/fixtures/register-canary/canary-prompt-03.json +17 -0
  225. package/src/core/isolation/index.js +60 -0
  226. package/src/core/mcp/framing.js +103 -0
  227. package/src/core/mcp/index.js +8 -0
  228. package/src/core/mcp/server.js +228 -0
  229. package/src/core/patterns/register-canary.js +209 -0
  230. package/src/core/patterns/req-shapes.js +158 -0
  231. package/src/core/patterns/services.js +358 -0
  232. package/src/core/patterns/ui-shapes.js +166 -0
  233. package/src/core/store/cn-resolve.js +134 -0
  234. package/src/core/store/derive-deps.js +93 -0
  235. package/src/core/store/ids.js +78 -0
  236. package/src/core/store/index.js +20 -0
  237. package/src/core/store/init.js +255 -0
  238. package/src/core/store/loader.js +211 -0
  239. package/src/core/store/tp-resolve.js +176 -0
  240. package/src/core/store/validator.js +191 -0
  241. package/src/core/store/walker.js +898 -0
  242. package/src/core/store/writer.js +1849 -0
  243. package/src/design/index.js +11 -0
  244. package/src/design/writer.js +271 -0
  245. package/src/finalise/detect.js +129 -0
  246. package/src/finalise/index.js +17 -0
  247. package/src/finalise/ingest.js +119 -0
  248. package/src/finalise/install.js +119 -0
  249. package/src/finalise/ship-without-verified.js +131 -0
  250. package/src/finalise/spawn.js +84 -0
  251. package/src/intake/fidelity.js +105 -0
  252. package/src/intake/index.js +6 -0
  253. package/src/intake/manifest-writer.js +100 -0
  254. package/src/intake/orchestrator.js +138 -0
  255. package/src/intake/validate.js +80 -0
  256. package/src/mcp/map-errors.js +131 -0
  257. package/src/mcp/prompts.js +49 -0
  258. package/src/mcp/resources.js +244 -0
  259. package/src/mcp/tools.js +1204 -0
  260. package/src/preflight/design-shapes.js +185 -0
  261. package/src/preflight/index.js +90 -0
  262. package/src/preflight/manifest-writer.js +264 -0
  263. package/src/preflight/scanner.js +206 -0
  264. package/src/preflight/secrets.js +134 -0
  265. package/src/preflight/session.js +246 -0
  266. package/src/query/attestation.js +285 -0
  267. package/src/query/coverage.js +308 -0
  268. package/src/query/formatters/json.js +21 -0
  269. package/src/query/formatters/mermaid.js +209 -0
  270. package/src/query/formatters/table.js +203 -0
  271. package/src/query/impact.js +173 -0
  272. package/src/query/index.js +9 -0
  273. package/src/query/trace.js +345 -0
  274. package/src/register-canary/fixture-loader.js +87 -0
  275. package/src/register-canary/index.js +10 -0
  276. package/src/register-canary/record-writer.js +132 -0
  277. package/src/register-canary/runner.js +156 -0
  278. package/src/req-baseline/gate.js +86 -0
  279. package/src/req-baseline/index.js +27 -0
  280. package/src/req-baseline/open-candidates.js +143 -0
  281. package/src/req-baseline/opt-out.js +195 -0
  282. package/src/req-baseline/sweep.js +230 -0
  283. package/src/req-detection/classifier.js +181 -0
  284. package/src/req-detection/index.js +9 -0
  285. package/src/req-detection/persist.js +55 -0
  286. package/src/review/index.js +325 -0
  287. package/src/review/mutation.js +117 -0
  288. package/src/review/ui-baseline-drift.js +138 -0
  289. package/src/server/index.js +178 -0
  290. package/src/server/routes.js +110 -0
  291. package/src/server/sse.js +118 -0
  292. package/src/setup/agent-setup.js +362 -0
  293. package/src/setup/identity-seed.js +104 -0
  294. package/src/setup/knowledge-seed.js +123 -0
  295. package/src/setup/managed-block.js +193 -0
  296. package/src/setup/managed-gitignore.js +166 -0
  297. package/src/setup/managed-markers.js +49 -0
  298. package/src/ui-baseline/defaults.js +119 -0
  299. package/src/ui-baseline/index.js +25 -0
  300. package/src/ui-baseline/manifest-writer.js +282 -0
  301. package/src/ui-baseline/session.js +178 -0
  302. package/src/ui-detection/classifier.js +192 -0
  303. package/src/verify/chain/index.js +190 -0
  304. package/src/verify/cli/cleanup.js +61 -0
  305. package/src/verify/cli/help.js +56 -0
  306. package/src/verify/cli/mcp.js +98 -0
  307. package/src/verify/cli/provision.js +71 -0
  308. package/src/verify/cli/report.js +71 -0
  309. package/src/verify/cli/run.js +155 -0
  310. package/src/verify/engine/brief.js +87 -0
  311. package/src/verify/engine/index.js +177 -0
  312. package/src/verify/engine/launcher.js +307 -0
  313. package/src/verify/mcp/tools.js +107 -0
  314. package/src/verify/profile/index.js +146 -0
  315. package/src/verify/provision/index.js +256 -0
  316. package/src/verify/report/index.js +139 -0
  317. package/src/verify/report/renderer.js +118 -0
  318. package/src/verify/verdict/index.js +246 -0
  319. package/src/view/doc-renderers/adr.js +44 -0
  320. package/src/view/doc-renderers/build-sequence.js +40 -0
  321. package/src/view/doc-renderers/fbs.js +128 -0
  322. package/src/view/doc-renderers/helpers.js +159 -0
  323. package/src/view/doc-renderers/index.js +12 -0
  324. package/src/view/doc-renderers/prd.js +45 -0
  325. package/src/view/doc-renderers/req.js +43 -0
  326. package/src/view/doc-renderers/tac.js +38 -0
  327. package/src/view/doc-renderers/tad.js +74 -0
  328. package/src/view/doc-renderers/test-suite.js +45 -0
  329. package/src/view/doc-renderers/user-story.js +63 -0
  330. package/src/view/html-page.js +462 -0
  331. package/src/view/index.js +63 -0
  332. package/src/view/live-client.js +338 -0
  333. package/src/view/mermaid-diagram.js +178 -0
  334. package/src/view/style.css +735 -0
  335. package/src/view/tree-model.js +152 -0
  336. package/src/view/vendored/mermaid.min.js +2607 -0
  337. package/src/view-supervisor/index.js +26 -0
  338. package/src/view-supervisor/logs.js +32 -0
  339. package/src/view-supervisor/manifest-writer.js +178 -0
  340. package/src/view-supervisor/persist-until.js +85 -0
  341. package/src/view-supervisor/supervisor.js +276 -0
  342. package/src/watch/index.js +152 -0
@@ -0,0 +1,108 @@
1
+ // `rcf ui-classify <fbs-id>` subcommand handler
2
+ // (ui-design-gate-0.7.0-spec §4.2).
3
+ //
4
+ // Runs the UI-bearing classifier on demand and prints the verdict. No
5
+ // side effects: the classifier does not write to the FBS document; the
6
+ // operator ratifies via `rcf update <fbs-id> --set uiBearing=true|false`
7
+ // when they want the verdict recorded (§4.4).
8
+
9
+ import { parseArgs } from 'node:util';
10
+ import process from 'node:process';
11
+
12
+ import { walkTree } from '#core/store';
13
+
14
+ import { findProjectRoot } from '../view/index.js';
15
+ import { classifyFbs } from '../ui-detection/classifier.js';
16
+
17
+ const OPTION_SPEC = {
18
+ json: { type: 'boolean' },
19
+ help: { type: 'boolean' },
20
+ };
21
+
22
+ export const HELP = `Usage: rcf ui-classify <fbs-id> [options]
23
+
24
+ Run the UI-bearing classifier on one FBS and print the verdict. Does
25
+ not write to the FBS document; ratify with:
26
+ rcf update <fbs-id> --set uiBearing=true
27
+
28
+ Options:
29
+ --json Emit the uiClassification block as JSON.
30
+ --help Print this help.
31
+
32
+ Exit codes:
33
+ 0 classified (verdict printed)
34
+ 1 IO / unexpected runtime failure
35
+ 2 usage error
36
+ 3 tree validation failure
37
+ `;
38
+
39
+ /**
40
+ * @param {string[]} argv
41
+ * @param {object} [deps]
42
+ * @returns {Promise<number>}
43
+ */
44
+ export async function main(argv, deps = {}) {
45
+ const stdout = deps.stdout ?? process.stdout;
46
+ const stderr = deps.stderr ?? process.stderr;
47
+ const cwd = deps.cwd ?? process.cwd();
48
+
49
+ let parsed;
50
+ try {
51
+ parsed = parseArgs({ args: argv, options: OPTION_SPEC, allowPositionals: true, strict: true });
52
+ } catch (err) {
53
+ stderr.write(`[error] usage ${err.message}\n`);
54
+ stderr.write(HELP);
55
+ return 2;
56
+ }
57
+ const flags = parsed.values;
58
+ if (flags.help) { stdout.write(HELP); return 0; }
59
+ if (parsed.positionals.length !== 1) {
60
+ stderr.write('[error] usage ui-classify: expected exactly one <fbs-id> positional\n');
61
+ return 2;
62
+ }
63
+ const fbsId = parsed.positionals[0];
64
+ if (!/^FBS-\d{3,}$/.test(fbsId)) {
65
+ stderr.write(`[error] usage ui-classify: expected an FBS id like FBS-011, got '${fbsId}'\n`);
66
+ return 2;
67
+ }
68
+
69
+ const projectRoot = await findProjectRoot(cwd);
70
+ if (!projectRoot) {
71
+ stderr.write('[error] usage no project root found (no rcf/manifest.json in this directory or any ancestor). Run `rcf init` first.\n');
72
+ return 2;
73
+ }
74
+ const { tree, errors } = await walkTree({ projectRoot });
75
+ if (errors.length > 0) {
76
+ stderr.write(`[warn] tree has ${errors.length} pre-existing issue(s); classification proceeds against what could be loaded\n`);
77
+ }
78
+ if (!tree.byId.has(fbsId) || tree.kindById.get(fbsId) !== 'fbs') {
79
+ stderr.write(`[error] usage ui-classify: ${fbsId} not found or not an FBS\n`);
80
+ return 2;
81
+ }
82
+
83
+ const now = deps.now ? new Date(deps.now()) : new Date();
84
+ const block = classifyFbs(tree, fbsId, { now });
85
+
86
+ if (flags.json) {
87
+ stdout.write(`${JSON.stringify(block, null, 2)}\n`);
88
+ return 0;
89
+ }
90
+
91
+ stdout.write(`ui-classify ${fbsId}: verdict=${block.verdict} reason=${block.reason}\n`);
92
+ const signals = Array.isArray(block.signals) ? block.signals : [];
93
+ if (signals.length > 0) {
94
+ stdout.write(` signals (${signals.length}):\n`);
95
+ for (const s of signals) {
96
+ const anchor = s.acId ? `${s.source}[${s.acId}]` : s.source;
97
+ stdout.write(` - ${anchor}: ${s.match}\n`);
98
+ }
99
+ }
100
+ if (block.verdict === 'ui') {
101
+ stdout.write(' Ratify with: rcf update ' + fbsId + ' --set uiBearing=true\n');
102
+ } else if (block.verdict === 'notUi' && signals.length === 0) {
103
+ stdout.write(' No UI signals detected. Override with: rcf update ' + fbsId + ' --set uiBearing=true\n');
104
+ } else if (block.verdict === 'operatorOverride') {
105
+ stdout.write(' Operator ruling recorded on FBS.uiBearing wins over the classifier.\n');
106
+ }
107
+ return 0;
108
+ }
@@ -0,0 +1,197 @@
1
+ // `rcf update <id>` subcommand handler. Parses --set dot-path lists into
2
+ // a patch array, optionally deep-merges a --from-file body, and calls
3
+ // writer.updateDocument. Phase 4 §D8 (with inline-id + root-singleton
4
+ // support).
5
+
6
+ import { readFile } from 'node:fs/promises';
7
+ import { parseArgs } from 'node:util';
8
+
9
+ import { isRcfError, writeUnexpectedFailure } from '#core/errors';
10
+ import { splitCnPath, updateDocument, walkTree } from '#core/store';
11
+ import { deriveFileDeps, mapDerivedDepsToCnIds } from '#core/store/derive-deps.js';
12
+ import { findProjectRoot } from '../view/index.js';
13
+ // Track C+D §4.4: reclassify a REQ when its description-carrying fields
14
+ // change so downstream tooling sees a fresh shapeClassification.
15
+ import { classifyAndPersistReq } from '../req-detection/index.js';
16
+
17
+ const OPTION_SPEC = {
18
+ set: { type: 'string', multiple: true },
19
+ 'from-file': { type: 'string' },
20
+ json: { type: 'boolean' },
21
+ 'dry-run': { type: 'boolean' },
22
+ quiet: { type: 'boolean' },
23
+ help: { type: 'boolean' },
24
+ // Phase 10 (X2 CodeNode bridge, D5): re-derive a CN's file-level
25
+ // dependencies via dependency-cruiser, merged into dependencies[].
26
+ 'derive-deps': { type: 'boolean' },
27
+ };
28
+
29
+ export const HELP = `Usage: rcf update <id> [options]
30
+
31
+ Options:
32
+ --set <dotPath>=<value> Set a field; repeatable
33
+ --from-file <path> Merge body fields from a JSON file
34
+ (deep merge; arrays replace)
35
+ --json Parse --set values as JSON (default: string)
36
+ --dry-run Print intended writes without executing
37
+ --quiet Suppress non-error stdout
38
+ --derive-deps CN only: re-derive file-level dependencies via
39
+ dependency-cruiser and merge into dependencies[]
40
+ (dev-time assist; never a runtime dependency)
41
+ --help Print this help
42
+ `;
43
+
44
+ /**
45
+ * @param {string[]} argv - argv slice after `update`
46
+ * @param {object} [deps]
47
+ * @returns {Promise<number>}
48
+ */
49
+ export async function main(argv, deps = {}) {
50
+ const stdout = deps.stdout ?? process.stdout;
51
+ const stderr = deps.stderr ?? process.stderr;
52
+ const cwd = deps.cwd ?? process.cwd();
53
+
54
+ let parsed;
55
+ try {
56
+ parsed = parseArgs({ args: argv, options: OPTION_SPEC, allowPositionals: true, strict: true });
57
+ } catch (err) {
58
+ stderr.write(`[error] usage ${err.message}\n`);
59
+ stderr.write(HELP);
60
+ return 2;
61
+ }
62
+ const flags = parsed.values;
63
+ const positionals = parsed.positionals;
64
+ if (flags.help) { stdout.write(HELP); return 0; }
65
+ if (positionals.length !== 1) {
66
+ stderr.write('[error] usage update: expected exactly one <id>\n');
67
+ stderr.write(HELP);
68
+ return 2;
69
+ }
70
+ const id = positionals[0];
71
+
72
+ const projectRoot = await findProjectRoot(cwd);
73
+ if (!projectRoot) {
74
+ 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');
75
+ return 2;
76
+ }
77
+
78
+ const walkResult = await walkTree({ projectRoot });
79
+ // B5: pre-existing tree breakage no longer blocks write verbs - the
80
+ // update is gated on the POST-write tree state inside the writer, so
81
+ // repairing a broken doc is exactly what this verb is now for.
82
+ if (walkResult.errors.length > 0) {
83
+ stderr.write(`[warn] tree has ${walkResult.errors.length} pre-existing issue(s); proceeding - writes are validated against the post-write state (run 'rcf validate' for details)\n`);
84
+ }
85
+
86
+ const sets = [];
87
+ const useJson = Boolean(flags.json);
88
+ for (const rawSet of flags.set ?? []) {
89
+ const eq = rawSet.indexOf('=');
90
+ if (eq < 0) {
91
+ stderr.write(`[error] usage update: bad --set '${rawSet}' (expected <path>=<value>)\n`);
92
+ return 2;
93
+ }
94
+ const path = rawSet.slice(0, eq);
95
+ const raw = rawSet.slice(eq + 1);
96
+ let value = raw;
97
+ if (useJson) {
98
+ try { value = JSON.parse(raw); } catch (err) {
99
+ stderr.write(`[error] usage update: --json parse failed on '${raw}': ${err.message}\n`);
100
+ return 2;
101
+ }
102
+ }
103
+ sets.push({ path, value });
104
+ }
105
+ let patch = null;
106
+ if (flags['from-file']) {
107
+ try {
108
+ patch = JSON.parse(await readFile(flags['from-file'], 'utf8'));
109
+ } catch (err) {
110
+ stderr.write(`[error] usage update: cannot read --from-file: ${err.message}\n`);
111
+ return 2;
112
+ }
113
+ }
114
+ if (flags['derive-deps']) {
115
+ if (walkResult.tree.kindById.get(id) !== 'codeNode') {
116
+ stderr.write('[error] usage update: --derive-deps only applies to cn ids\n');
117
+ return 2;
118
+ }
119
+ const setPath = sets.find((s) => s.path === 'path')?.value;
120
+ const currentPath = typeof setPath === 'string' ? setPath : walkResult.tree.byId.get(id)?.path;
121
+ const { file } = splitCnPath(currentPath ?? '');
122
+ const derived = await deriveFileDeps({ projectRoot, filePath: file });
123
+ if (!derived.ok) {
124
+ stderr.write(`[error] usage update: --derive-deps: ${derived.message}\n`);
125
+ return 2;
126
+ }
127
+ const { cnIds, unmatched } = mapDerivedDepsToCnIds(walkResult.tree, derived.deps);
128
+ const existing = walkResult.tree.byId.get(id)?.dependencies ?? [];
129
+ const merged = [...new Set([...existing, ...cnIds])].filter((d) => d !== id).sort();
130
+ sets.push({ path: 'dependencies', value: merged });
131
+ if (unmatched.length > 0 && !flags.quiet) {
132
+ stdout.write(`[info] --derive-deps: ${unmatched.length} file-level import(s) have no matching CN yet, skipped: ${unmatched.join(', ')}\n`);
133
+ }
134
+ }
135
+ if (sets.length === 0 && !patch) {
136
+ stderr.write('[error] usage update: at least one --set or --from-file is required\n');
137
+ return 2;
138
+ }
139
+ const result = await updateDocument({
140
+ projectRoot, tree: walkResult.tree, id, patch, sets,
141
+ options: { dryRun: Boolean(flags['dry-run']) },
142
+ walkErrors: walkResult.errors,
143
+ });
144
+ if (isRcfError(result)) return handleWriterError(result, stderr);
145
+ if (flags['dry-run']) {
146
+ if (!flags.quiet) stdout.write(`[dry-run] would update ${result.id} at ${result.filePath}\n`);
147
+ return 0;
148
+ }
149
+ if (!flags.quiet) stdout.write(`${result.id} updated at ${result.filePath}\n`);
150
+
151
+ // Track C+D §4.4: reclassify a REQ when the update touched a
152
+ // shape-signal-carrying field (title / description / rationale).
153
+ const touched = new Set();
154
+ for (const s of sets) {
155
+ const head = String(s.path ?? '').split('.')[0];
156
+ if (head) touched.add(head);
157
+ }
158
+ if (patch && typeof patch === 'object') {
159
+ for (const key of Object.keys(patch)) touched.add(key);
160
+ }
161
+ const kindOfDoc = walkResult.tree.kindById.get(id);
162
+ const shouldReclassify = kindOfDoc === 'requirement'
163
+ && (touched.has('title') || touched.has('description') || touched.has('rationale'));
164
+ if (shouldReclassify) {
165
+ try {
166
+ const postWalk = await walkTree({ projectRoot });
167
+ const classifyOutcome = await classifyAndPersistReq({
168
+ projectRoot,
169
+ tree: postWalk.tree,
170
+ reqId: id,
171
+ });
172
+ if (classifyOutcome && classifyOutcome.ok === false) {
173
+ stderr.write(`[warn] ${classifyOutcome.message}\n`);
174
+ } else if (!flags.quiet && classifyOutcome?.changed && classifyOutcome.block?.shapes?.length > 0) {
175
+ const shapes = classifyOutcome.block.shapes.join(', ');
176
+ stdout.write(`${id} shapeClassification: [${shapes}] (${classifyOutcome.block.reason})\n`);
177
+ }
178
+ } catch (err) {
179
+ stderr.write(`[warn] req-classify (post-update): ${err.message}\n`);
180
+ }
181
+ }
182
+ return 0;
183
+ }
184
+
185
+ function handleWriterError(err, stderr) {
186
+ const kind = err.kind;
187
+ // BUG-007 fix: spec §D15 mandates exit-1 emit
188
+ // `[rcf] unexpected failure: <msg>\n<stack>` — even under --quiet.
189
+ if (kind === 'ioFailure') {
190
+ writeUnexpectedFailure(err, stderr);
191
+ return 1;
192
+ }
193
+ stderr.write(`[error] ${kind} ${err.message}\n`);
194
+ if (kind === 'usage') return 2;
195
+ if (kind === 'validation' || kind === 'brokenReference') return 3;
196
+ return 1;
197
+ }
@@ -0,0 +1,168 @@
1
+ // `rcf validate` subcommand handler. Wraps walker + validator + broken-
2
+ // reference machinery. Exits 0 clean, 2 on usage failure, 3 on validation
3
+ // or broken-references. Phase 4 §D3.
4
+
5
+ import { parseArgs } from 'node:util';
6
+
7
+ import { formatErrors } from '#core/errors';
8
+ import { checkCodeNodeResolution, walkTree } from '#core/store';
9
+ import { findProjectRoot } from '../view/index.js';
10
+
11
+ const OPTION_SPEC = {
12
+ quiet: { type: 'boolean' },
13
+ json: { type: 'boolean' },
14
+ help: { type: 'boolean' },
15
+ // Phase 10 (X2 CodeNode bridge, spec D8): skip the Code Node staleness
16
+ // pass (spec-graph checks only). The default remains full validation -
17
+ // the staleCode floor is only a floor if it runs by default.
18
+ 'no-code': { type: 'boolean' },
19
+ };
20
+
21
+ export const HELP = `Usage: rcf validate [options]
22
+
23
+ Walk the rcf/ tree and report schema-validation and broken-reference
24
+ issues. Exits 0 when clean, 3 on issues.
25
+
26
+ Options:
27
+ --quiet Only summary line + first 3 issues
28
+ --json Emit machine-readable envelope
29
+ --no-code Skip the Code Node staleness pass (spec-graph
30
+ checks only; the default runs full validation)
31
+ --help Print this help
32
+ `;
33
+
34
+ // Case-insensitive whole-word TODO match. Word-bounded so incidental
35
+ // substrings (e.g. "autodoc") do not trip the scan.
36
+ const TODO_RE = /\btodo\b/i;
37
+
38
+ /**
39
+ * Recursively collect dot-paths of string fields containing TODO
40
+ * placeholder text.
41
+ * @param {unknown} value
42
+ * @param {string} path
43
+ * @param {string[]} out
44
+ */
45
+ function findTodoFields(value, path, out) {
46
+ if (typeof value === 'string') {
47
+ if (TODO_RE.test(value)) out.push(path);
48
+ return;
49
+ }
50
+ if (Array.isArray(value)) {
51
+ value.forEach((v, i) => findTodoFields(v, `${path}[${i}]`, out));
52
+ return;
53
+ }
54
+ if (value && typeof value === 'object') {
55
+ for (const [k, v] of Object.entries(value)) {
56
+ findTodoFields(v, path ? `${path}.${k}` : k, out);
57
+ }
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Scan every loaded document (roots + children; inline ACs / TCs ride
63
+ * inside their parent doc's fields) for TODO placeholder text left over
64
+ * from `rcf init` scaffolding. Cheap by design (B4): one string sweep,
65
+ * one entry per affected doc.
66
+ * @param {object} tree - walker TreeModel
67
+ * @returns {Array<{ id: string, fields: string[] }>}
68
+ */
69
+ function collectTodoNotices(tree) {
70
+ const docs = [
71
+ ...(tree.prd ? [{ id: tree.prd.prdId, doc: tree.prd }] : []),
72
+ ...(tree.tad ? [{ id: tree.tad.tadId, doc: tree.tad }] : []),
73
+ ...(tree.bs ? [{ id: tree.bs.bsId, doc: tree.bs }] : []),
74
+ ...tree.requirements.map((d) => ({ id: d.reqId, doc: d })),
75
+ ...tree.userStories.map((d) => ({ id: d.usId, doc: d })),
76
+ ...tree.tacs.map((d) => ({ id: d.tacId, doc: d })),
77
+ ...tree.adrs.map((d) => ({ id: d.adrId, doc: d })),
78
+ ...tree.fbsItems.map((d) => ({ id: d.fbsId, doc: d })),
79
+ ...tree.testSuites.map((d) => ({ id: d.id, doc: d })),
80
+ ];
81
+ const notices = [];
82
+ for (const { id, doc } of docs) {
83
+ /** @type {string[]} */
84
+ const fields = [];
85
+ findTodoFields(doc, '', fields);
86
+ if (fields.length > 0) notices.push({ id: id ?? '(unknown id)', fields });
87
+ }
88
+ return notices;
89
+ }
90
+
91
+ /**
92
+ * @param {string[]} argv - argv slice after `validate`
93
+ * @param {object} [deps]
94
+ * @returns {Promise<number>}
95
+ */
96
+ export async function main(argv, deps = {}) {
97
+ const stdout = deps.stdout ?? process.stdout;
98
+ const stderr = deps.stderr ?? process.stderr;
99
+ const cwd = deps.cwd ?? process.cwd();
100
+
101
+ let parsed;
102
+ try {
103
+ parsed = parseArgs({ args: argv, options: OPTION_SPEC, allowPositionals: true, strict: true });
104
+ } catch (err) {
105
+ stderr.write(`[error] usage ${err.message}\n`);
106
+ stderr.write(HELP);
107
+ return 2;
108
+ }
109
+ const flags = parsed.values;
110
+ if (flags.help) {
111
+ stdout.write(HELP);
112
+ return 0;
113
+ }
114
+ const projectRoot = await findProjectRoot(cwd);
115
+ if (!projectRoot) {
116
+ 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');
117
+ return 2;
118
+ }
119
+ const { tree, errors } = await walkTree({ projectRoot });
120
+ // Phase 10 (X2 CodeNode bridge, spec D6): staleness pass. Every Code
121
+ // Node's declared path/symbol is checked against the working tree;
122
+ // unresolved ones surface as `staleCode` errors folded into the same
123
+ // exit path as schema / referential-integrity errors. This is the X2
124
+ // detection claim: links that break visibly.
125
+ if (!flags['no-code']) {
126
+ const staleErrors = await checkCodeNodeResolution({ projectRoot, tree });
127
+ errors.push(...staleErrors);
128
+ }
129
+ if (flags.json) {
130
+ const issues = errors.map((e) => ({
131
+ id: e.documentId ?? null,
132
+ kind: e.kind,
133
+ rule: e.rule ?? null,
134
+ filePath: e.filePath ?? null,
135
+ field: e.field ?? null,
136
+ message: e.message,
137
+ }));
138
+ stdout.write(`${JSON.stringify({ ok: errors.length === 0, issues }, null, 2)}\n`);
139
+ return errors.length === 0 ? 0 : 3;
140
+ }
141
+ if (errors.length === 0) {
142
+ if (!flags.quiet) {
143
+ stdout.write('rcf validate: tree is clean.\n');
144
+ // B4 (E2E matrix 2026-07-06-003): scaffold TODO placeholders were
145
+ // surviving to otherwise-valid trees unflagged. Non-blocking notice
146
+ // only - the exit code is unchanged.
147
+ const notices = collectTodoNotices(tree);
148
+ if (notices.length > 0) {
149
+ stdout.write(`notice: ${notices.length} document(s) still carry scaffold TODO placeholder text (informational; exit code unaffected):\n`);
150
+ for (const n of notices) {
151
+ stdout.write(` ${n.id}: ${n.fields.join(', ')}\n`);
152
+ }
153
+ }
154
+ }
155
+ return 0;
156
+ }
157
+ if (flags.quiet) {
158
+ const first = errors.slice(0, 3);
159
+ // BUG-004 fix: `--quiet` used to render `errors.length` (== 3) as the
160
+ // summary count. Pass the true total through so the summary reads
161
+ // "N errors found" for the whole tree, not just the shown subset.
162
+ stderr.write(`${formatErrors(first, { verbose: false, strict: false, total: errors.length })}\n`);
163
+ if (errors.length > 3) stderr.write(`... ${errors.length - 3} more issue(s) suppressed by --quiet\n`);
164
+ } else {
165
+ stderr.write(`${formatErrors(errors, { verbose: false, strict: false })}\n`);
166
+ }
167
+ return 3;
168
+ }