rcf-lite 0.0.1 → 0.8.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 (349) hide show
  1. package/CHANGELOG.md +344 -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 +71 -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/admissibility/enforce.js +142 -0
  177. package/src/admissibility/index.js +8 -0
  178. package/src/admissibility/markers.js +104 -0
  179. package/src/admissibility/scope-lint.js +163 -0
  180. package/src/browser-verify/auth-smoke.js +109 -0
  181. package/src/browser-verify/index.js +29 -0
  182. package/src/browser-verify/invariants.js +336 -0
  183. package/src/browser-verify/manifest-writer.js +189 -0
  184. package/src/browser-verify/runner.js +171 -0
  185. package/src/build/bundle.js +198 -0
  186. package/src/build/formatters/json.js +18 -0
  187. package/src/build/formatters/markdown.js +406 -0
  188. package/src/build/index.js +14 -0
  189. package/src/build/mark.js +177 -0
  190. package/src/build/queue.js +285 -0
  191. package/src/cli/browser-verify.js +231 -0
  192. package/src/cli/build.js +584 -0
  193. package/src/cli/coverage.js +219 -0
  194. package/src/cli/create.js +358 -0
  195. package/src/cli/delete.js +127 -0
  196. package/src/cli/design.js +353 -0
  197. package/src/cli/doctor.js +548 -0
  198. package/src/cli/fbs.js +171 -0
  199. package/src/cli/finalise.js +377 -0
  200. package/src/cli/guidance.js +206 -0
  201. package/src/cli/help.js +156 -0
  202. package/src/cli/impact.js +119 -0
  203. package/src/cli/init.js +282 -0
  204. package/src/cli/intake.js +153 -0
  205. package/src/cli/link.js +128 -0
  206. package/src/cli/mcp.js +160 -0
  207. package/src/cli/preflight.js +220 -0
  208. package/src/cli/read.js +168 -0
  209. package/src/cli/req-baseline.js +269 -0
  210. package/src/cli/req-classify.js +135 -0
  211. package/src/cli/review.js +295 -0
  212. package/src/cli/test-suite.js +221 -0
  213. package/src/cli/trace.js +172 -0
  214. package/src/cli/ui-baseline.js +292 -0
  215. package/src/cli/ui-classify.js +108 -0
  216. package/src/cli/update.js +197 -0
  217. package/src/cli/validate.js +168 -0
  218. package/src/cli/view.js +452 -0
  219. package/src/core/baseline-catalog/data/auth.json +42 -0
  220. package/src/core/baseline-catalog/data/http-api.json +42 -0
  221. package/src/core/baseline-catalog/data/notifications.json +33 -0
  222. package/src/core/baseline-catalog/data/persistence.json +33 -0
  223. package/src/core/baseline-catalog/data/web-ui.json +60 -0
  224. package/src/core/baseline-catalog/index.js +121 -0
  225. package/src/core/errors/index.js +167 -0
  226. package/src/core/fixtures/register-canary/canary-prompt-01.json +18 -0
  227. package/src/core/fixtures/register-canary/canary-prompt-02.json +21 -0
  228. package/src/core/fixtures/register-canary/canary-prompt-03.json +17 -0
  229. package/src/core/isolation/index.js +60 -0
  230. package/src/core/mcp/framing.js +103 -0
  231. package/src/core/mcp/index.js +8 -0
  232. package/src/core/mcp/server.js +228 -0
  233. package/src/core/patterns/register-canary.js +209 -0
  234. package/src/core/patterns/req-shapes.js +158 -0
  235. package/src/core/patterns/services.js +358 -0
  236. package/src/core/patterns/ui-shapes.js +166 -0
  237. package/src/core/store/cn-resolve.js +134 -0
  238. package/src/core/store/derive-deps.js +93 -0
  239. package/src/core/store/ids.js +78 -0
  240. package/src/core/store/index.js +20 -0
  241. package/src/core/store/init.js +255 -0
  242. package/src/core/store/loader.js +214 -0
  243. package/src/core/store/tp-resolve.js +176 -0
  244. package/src/core/store/validator.js +191 -0
  245. package/src/core/store/walker.js +944 -0
  246. package/src/core/store/writer.js +1879 -0
  247. package/src/design/index.js +11 -0
  248. package/src/design/writer.js +271 -0
  249. package/src/finalise/detect.js +151 -0
  250. package/src/finalise/index.js +31 -0
  251. package/src/finalise/ingest.js +160 -0
  252. package/src/finalise/install.js +119 -0
  253. package/src/finalise/ship-without-verified.js +131 -0
  254. package/src/finalise/spawn.js +84 -0
  255. package/src/intake/fidelity.js +105 -0
  256. package/src/intake/index.js +6 -0
  257. package/src/intake/manifest-writer.js +100 -0
  258. package/src/intake/orchestrator.js +138 -0
  259. package/src/intake/validate.js +80 -0
  260. package/src/mcp/map-errors.js +131 -0
  261. package/src/mcp/prompts.js +49 -0
  262. package/src/mcp/resources.js +244 -0
  263. package/src/mcp/tools.js +1212 -0
  264. package/src/preflight/design-shapes.js +185 -0
  265. package/src/preflight/index.js +90 -0
  266. package/src/preflight/manifest-writer.js +264 -0
  267. package/src/preflight/scanner.js +206 -0
  268. package/src/preflight/secrets.js +134 -0
  269. package/src/preflight/session.js +246 -0
  270. package/src/query/attestation.js +285 -0
  271. package/src/query/coverage.js +308 -0
  272. package/src/query/formatters/json.js +21 -0
  273. package/src/query/formatters/mermaid.js +209 -0
  274. package/src/query/formatters/table.js +203 -0
  275. package/src/query/impact.js +173 -0
  276. package/src/query/index.js +13 -0
  277. package/src/query/refuse-on-admissibility.js +73 -0
  278. package/src/query/trace.js +345 -0
  279. package/src/register-canary/fixture-loader.js +87 -0
  280. package/src/register-canary/index.js +10 -0
  281. package/src/register-canary/record-writer.js +132 -0
  282. package/src/register-canary/runner.js +156 -0
  283. package/src/req-baseline/gate.js +86 -0
  284. package/src/req-baseline/index.js +27 -0
  285. package/src/req-baseline/open-candidates.js +143 -0
  286. package/src/req-baseline/opt-out.js +195 -0
  287. package/src/req-baseline/sweep.js +230 -0
  288. package/src/req-detection/classifier.js +181 -0
  289. package/src/req-detection/index.js +9 -0
  290. package/src/req-detection/persist.js +55 -0
  291. package/src/review/index.js +325 -0
  292. package/src/review/mutation.js +117 -0
  293. package/src/review/ui-baseline-drift.js +138 -0
  294. package/src/ruleset/index.js +140 -0
  295. package/src/ruleset/ruleset.json +146 -0
  296. package/src/server/index.js +178 -0
  297. package/src/server/routes.js +110 -0
  298. package/src/server/sse.js +118 -0
  299. package/src/setup/agent-setup.js +362 -0
  300. package/src/setup/identity-seed.js +104 -0
  301. package/src/setup/knowledge-seed.js +123 -0
  302. package/src/setup/managed-block.js +193 -0
  303. package/src/setup/managed-gitignore.js +166 -0
  304. package/src/setup/managed-markers.js +49 -0
  305. package/src/ui-baseline/defaults.js +119 -0
  306. package/src/ui-baseline/index.js +25 -0
  307. package/src/ui-baseline/manifest-writer.js +282 -0
  308. package/src/ui-baseline/session.js +178 -0
  309. package/src/ui-detection/classifier.js +192 -0
  310. package/src/verify/chain/index.js +221 -0
  311. package/src/verify/cli/cleanup.js +61 -0
  312. package/src/verify/cli/help.js +56 -0
  313. package/src/verify/cli/mcp.js +98 -0
  314. package/src/verify/cli/provision.js +71 -0
  315. package/src/verify/cli/report.js +71 -0
  316. package/src/verify/cli/run.js +155 -0
  317. package/src/verify/engine/brief.js +87 -0
  318. package/src/verify/engine/index.js +177 -0
  319. package/src/verify/engine/launcher.js +307 -0
  320. package/src/verify/mcp/tools.js +107 -0
  321. package/src/verify/profile/index.js +146 -0
  322. package/src/verify/provision/index.js +256 -0
  323. package/src/verify/report/index.js +139 -0
  324. package/src/verify/report/renderer.js +118 -0
  325. package/src/verify/verdict/index.js +313 -0
  326. package/src/view/doc-renderers/adr.js +44 -0
  327. package/src/view/doc-renderers/build-sequence.js +40 -0
  328. package/src/view/doc-renderers/fbs.js +128 -0
  329. package/src/view/doc-renderers/helpers.js +159 -0
  330. package/src/view/doc-renderers/index.js +12 -0
  331. package/src/view/doc-renderers/prd.js +45 -0
  332. package/src/view/doc-renderers/req.js +43 -0
  333. package/src/view/doc-renderers/tac.js +38 -0
  334. package/src/view/doc-renderers/tad.js +74 -0
  335. package/src/view/doc-renderers/test-suite.js +45 -0
  336. package/src/view/doc-renderers/user-story.js +63 -0
  337. package/src/view/html-page.js +462 -0
  338. package/src/view/index.js +63 -0
  339. package/src/view/live-client.js +338 -0
  340. package/src/view/mermaid-diagram.js +178 -0
  341. package/src/view/style.css +735 -0
  342. package/src/view/tree-model.js +152 -0
  343. package/src/view/vendored/mermaid.min.js +2607 -0
  344. package/src/view-supervisor/index.js +26 -0
  345. package/src/view-supervisor/logs.js +32 -0
  346. package/src/view-supervisor/manifest-writer.js +178 -0
  347. package/src/view-supervisor/persist-until.js +85 -0
  348. package/src/view-supervisor/supervisor.js +276 -0
  349. package/src/watch/index.js +152 -0
@@ -0,0 +1,127 @@
1
+ // `rcf delete <id>` subcommand handler. Refuse-by-default when the doc
2
+ // has dependents; --cascade opts in. Orphan-refuse pre-plan check on
3
+ // REQ / US / AC cascade paths (Phase 4 §D9 amendment). Phase 4 §D9.
4
+
5
+ import { parseArgs } from 'node:util';
6
+
7
+ import { isRcfError, writeUnexpectedFailure } from '#core/errors';
8
+ import { deleteDocument, walkTree } from '#core/store';
9
+ import { findProjectRoot } from '../view/index.js';
10
+
11
+ const OPTION_SPEC = {
12
+ cascade: { type: 'boolean' },
13
+ 'dry-run': { type: 'boolean' },
14
+ quiet: { type: 'boolean' },
15
+ help: { type: 'boolean' },
16
+ };
17
+
18
+ export const HELP = `Usage: rcf delete <id> [options]
19
+
20
+ Options:
21
+ --cascade Also delete dependents and drop backrefs
22
+ (dependents discovered via computed maps)
23
+ --dry-run Print the plan without executing
24
+ --quiet Suppress non-error stdout
25
+ --help Print this help
26
+ `;
27
+
28
+ /**
29
+ * @param {string[]} argv - argv slice after `delete`
30
+ * @param {object} [deps]
31
+ * @returns {Promise<number>}
32
+ */
33
+ export async function main(argv, deps = {}) {
34
+ const stdout = deps.stdout ?? process.stdout;
35
+ const stderr = deps.stderr ?? process.stderr;
36
+ const cwd = deps.cwd ?? process.cwd();
37
+
38
+ let parsed;
39
+ try {
40
+ parsed = parseArgs({ args: argv, options: OPTION_SPEC, allowPositionals: true, strict: true });
41
+ } catch (err) {
42
+ stderr.write(`[error] usage ${err.message}\n`);
43
+ stderr.write(HELP);
44
+ return 2;
45
+ }
46
+ const flags = parsed.values;
47
+ const positionals = parsed.positionals;
48
+ if (flags.help) { stdout.write(HELP); return 0; }
49
+ if (positionals.length !== 1) {
50
+ stderr.write('[error] usage delete: expected exactly one <id>\n');
51
+ stderr.write(HELP);
52
+ return 2;
53
+ }
54
+ const id = positionals[0];
55
+
56
+ const projectRoot = await findProjectRoot(cwd);
57
+ if (!projectRoot) {
58
+ 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');
59
+ return 2;
60
+ }
61
+ const walkResult = await walkTree({ projectRoot });
62
+ // B5 (amends the §D9 broken-tree-before-dependents precedence): a
63
+ // broken tree no longer blocks delete - deleting the offending doc is
64
+ // the canonical repair. The writer gates on the POST-write tree state;
65
+ // only net-new breakage refuses.
66
+ if (walkResult.errors.length > 0) {
67
+ 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`);
68
+ }
69
+
70
+ const result = await deleteDocument({
71
+ projectRoot, tree: walkResult.tree, id,
72
+ options: {
73
+ cascade: Boolean(flags.cascade),
74
+ dryRun: Boolean(flags['dry-run']),
75
+ },
76
+ walkErrors: walkResult.errors,
77
+ });
78
+ if (isRcfError(result)) return handleDeleteError(result, stderr);
79
+ // BUG-005 fix: split the header text between dry-run (future tense,
80
+ // "Would delete …") and executed (past tense, "Deleted …"). Previously
81
+ // both paths shared the future-tense header, making an executed delete
82
+ // visually indistinguishable from a plan at the summary line.
83
+ const dryRun = Boolean(flags['dry-run']);
84
+ const label = dryRun ? '[dry-run] would ' : '';
85
+ if (!flags.quiet) {
86
+ if (dryRun) {
87
+ stdout.write(
88
+ `Would delete ${result.deleted.length} file(s) and mutate ${result.mutated.length} doc(s). (dry-run)\n`,
89
+ );
90
+ } else {
91
+ stdout.write(
92
+ `Deleted ${result.deleted.length} file(s), mutated ${result.mutated.length} doc(s).\n`,
93
+ );
94
+ }
95
+ for (const line of result.plan) stdout.write(` ${label}${line}\n`);
96
+ }
97
+ return 0;
98
+ }
99
+
100
+ /**
101
+ * `rcfError.kind === 'usage'` from writer.deleteDocument overloads two
102
+ * exit codes: the plain "unknown id / already-clean" cases stay exit 2,
103
+ * while a `rule` of `dependents` / `wouldOrphan` maps to exit 4.
104
+ *
105
+ * BUG-007 fix: `ioFailure` emits the spec §D15 `[rcf] unexpected failure`
106
+ * block (message + stack), not the structured `[error] ioFailure` line.
107
+ *
108
+ * BUG-008 fix: exit-4 refusals are labelled `[error] refused …` so the
109
+ * prefix matches the exit-code semantics, instead of the misleading
110
+ * `[error] usage …` prefix used when the underlying `rcfError.kind`
111
+ * happens to be `usage` with a `dependents` / `wouldOrphan` rule.
112
+ */
113
+ function handleDeleteError(err, stderr) {
114
+ const kind = err.kind;
115
+ if (kind === 'ioFailure') {
116
+ writeUnexpectedFailure(err, stderr);
117
+ return 1;
118
+ }
119
+ if (kind === 'usage' && (err.rule === 'dependents' || err.rule === 'wouldOrphan')) {
120
+ stderr.write(`[error] refused ${err.message}\n`);
121
+ return 4;
122
+ }
123
+ stderr.write(`[error] ${kind} ${err.message}\n`);
124
+ if (kind === 'usage') return 2;
125
+ if (kind === 'validation' || kind === 'brokenReference') return 3;
126
+ return 1;
127
+ }
@@ -0,0 +1,353 @@
1
+ // `rcf design <fbs-id> [<sub-verb> ...]` handler
2
+ // (ui-design-gate-0.7.0-spec §5.5).
3
+ //
4
+ // Verb shapes:
5
+ // rcf design <fbs-id> Dispatch the Design worker (no-op stub in v1; injection point).
6
+ // rcf design <fbs-id> journeys add ... Append a journey.
7
+ // rcf design <fbs-id> nav set ... Overwrite navModel.
8
+ // rcf design <fbs-id> theme-a11y set ... Overwrite themeAndA11y.
9
+ // rcf design <fbs-id> --mark-complete Set designStageComplete: true.
10
+ //
11
+ // Sub-verb positional-grammar constraint (spec §5.5): FBS ids match
12
+ // `/^FBS-\d+$/`, so no operator-typed slug can collide with a sub-verb
13
+ // noun. The parser enforces the pattern on the FBS positional: any
14
+ // positional in slot 0 that does not match /^FBS-\d+$/ is a usage
15
+ // error, not an FBS lookup miss. This closes the door on future
16
+ // refactors that accept slugs whose namespace could overlap with the
17
+ // sub-verb set.
18
+ //
19
+ // Design-worker dispatch: v1 does not spawn a subagent in-process (the
20
+ // dispatch pattern is deferred to the CLI harness or the operator);
21
+ // `rcf design <fbs-id>` with no sub-verb prints the current state and
22
+ // the operator's next-move options. This matches the Track A pattern
23
+ // where `rcf review` runs a deterministic audit and dispatches the
24
+ // mutation runner only via an injected dependency; wiring the Design
25
+ // worker's live dispatcher would enlarge scope beyond spec §5.3.
26
+
27
+ import { parseArgs } from 'node:util';
28
+ import process from 'node:process';
29
+
30
+ import { walkTree } from '#core/store';
31
+ import { writeUnexpectedFailure, rcfError } from '#core/errors';
32
+
33
+ import { findProjectRoot } from '../view/index.js';
34
+ import {
35
+ missingDesignStageArtefacts,
36
+ writeJourneyAdd,
37
+ writeMarkComplete,
38
+ writeNavSet,
39
+ writeThemeA11ySet,
40
+ } from '../design/index.js';
41
+
42
+ const KNOWN_SUB_VERBS = new Set(['journeys', 'nav', 'theme-a11y']);
43
+ const FBS_ID_PATTERN = /^FBS-\d+$/;
44
+
45
+ const OPTION_SPEC = {
46
+ // journeys add
47
+ id: { type: 'string' },
48
+ actor: { type: 'string' },
49
+ goal: { type: 'string' },
50
+ step: { type: 'string', multiple: true },
51
+ // nav set
52
+ shape: { type: 'string' },
53
+ route: { type: 'string', multiple: true },
54
+ 'signed-in-as-affordance': { type: 'string' },
55
+ notes: { type: 'string' },
56
+ // theme-a11y set
57
+ mode: { type: 'string' },
58
+ tokens: { type: 'string' },
59
+ 'contrast-targets': { type: 'string' },
60
+ 'contrast-test': { type: 'string' },
61
+ 'contrast-before-palette': { type: 'string' },
62
+ // mark-complete
63
+ 'mark-complete': { type: 'boolean' },
64
+ // shared
65
+ 'dry-run': { type: 'boolean' },
66
+ json: { type: 'boolean' },
67
+ quiet: { type: 'boolean' },
68
+ help: { type: 'boolean' },
69
+ };
70
+
71
+ export const HELP = `Usage: rcf design <fbs-id> [<sub-verb> ...] [options]
72
+
73
+ Design substage verbs for a UI-bearing FBS. FBS positional is required
74
+ and must match /^FBS-\\d+$/ (any other positional in that slot is a
75
+ usage error, not an FBS lookup miss).
76
+
77
+ Verb shapes:
78
+ rcf design <fbs-id> Print the current design
79
+ state and next-move options.
80
+ rcf design <fbs-id> journeys add ... Append a journey.
81
+ rcf design <fbs-id> nav set ... Overwrite navModel.
82
+ rcf design <fbs-id> theme-a11y set ... Overwrite themeAndA11y.
83
+ rcf design <fbs-id> --mark-complete Set designStageComplete: true.
84
+
85
+ journeys add options:
86
+ --id <slug> Lowercase slug (a-z0-9-), unique within the FBS.
87
+ --actor "..." Short actor label.
88
+ --goal "..." One-line goal statement.
89
+ --step "..." (2-8 times) Walk-through steps.
90
+
91
+ nav set options:
92
+ --shape <shape> shared-persistent | shared-per-section | none-single-page | operator-declared-other
93
+ --route <path=label:auth> One entry per route (auth is true|false); repeatable.
94
+ --signed-in-as-affordance true|false Whether authenticated routes show signed-in-as.
95
+ --notes "..." Free text; required in practice for operator-declared-other.
96
+
97
+ theme-a11y set options:
98
+ --mode <themeMode> light-default-with-toggle | dark-default-with-toggle | single-theme-declared
99
+ --tokens <path> Project-relative path to the design tokens module.
100
+ --contrast-targets "..." Short target statement (default: WCAG AA).
101
+ --contrast-test <path> Project-relative path to the contrast test.
102
+ --contrast-before-palette true|false Operator attestation on ordering.
103
+
104
+ --mark-complete options:
105
+ --dry-run Print the intended change; do not write.
106
+
107
+ Common options:
108
+ --json Emit the FBS's designStage block as JSON on success.
109
+ --quiet Suppress non-error confirmations.
110
+ --help Print this help.
111
+
112
+ Refusal shapes (spec §6.2):
113
+ * FBS.uiBearing is true but no uiBaseline record on the manifest.
114
+ * designStage baseline-vs-designStage disagreement without an opt-out.
115
+ * --mark-complete when any of the three artefacts is missing or empty.
116
+
117
+ Exit codes:
118
+ 0 success
119
+ 1 IO / unexpected runtime failure
120
+ 2 usage error
121
+ 3 schema or tree validation failure
122
+ 4 refused (mark-complete or baseline disagreement)
123
+ `;
124
+
125
+ /**
126
+ * @param {string[]} argv
127
+ * @param {object} [deps]
128
+ * @returns {Promise<number>}
129
+ */
130
+ export async function main(argv, deps = {}) {
131
+ const stdout = deps.stdout ?? process.stdout;
132
+ const stderr = deps.stderr ?? process.stderr;
133
+ const cwd = deps.cwd ?? process.cwd();
134
+
135
+ if (argv.length === 0) {
136
+ stderr.write('[error] usage design: expected an <fbs-id> positional (e.g. FBS-016)\n');
137
+ return 2;
138
+ }
139
+ if (argv[0] === '--help' || argv[0] === '-h') { stdout.write(HELP); return 0; }
140
+
141
+ // Sub-verb positional grammar (§5.5): slot 0 MUST be an FBS id or
142
+ // an error. Sub-verb collision is guarded by the FBS-id pattern
143
+ // never matching a lowercase sub-verb noun.
144
+ const fbsId = argv[0];
145
+ if (!FBS_ID_PATTERN.test(fbsId)) {
146
+ stderr.write(`[error] usage design: expected an FBS id like FBS-011 as the first positional, got '${fbsId}' (sub-verbs like ${[...KNOWN_SUB_VERBS].join(' | ')} follow the FBS id).\n`);
147
+ return 2;
148
+ }
149
+
150
+ // Parse remainder. `parseArgs` handles the sub-verb + sub-sub-verb
151
+ // positionals uniformly.
152
+ let parsed;
153
+ try {
154
+ parsed = parseArgs({ args: argv.slice(1), options: OPTION_SPEC, allowPositionals: true, strict: true });
155
+ } catch (err) {
156
+ stderr.write(`[error] usage ${err.message}\n`);
157
+ stderr.write(HELP);
158
+ return 2;
159
+ }
160
+ const flags = parsed.values;
161
+ if (flags.help) { stdout.write(HELP); return 0; }
162
+ const subs = parsed.positionals;
163
+
164
+ const projectRoot = await findProjectRoot(cwd);
165
+ if (!projectRoot) {
166
+ stderr.write('[error] usage no project root found (no rcf/manifest.json in this directory or any ancestor). Run `rcf init` first.\n');
167
+ return 2;
168
+ }
169
+ const walkResult = await walkTree({ projectRoot });
170
+ if (walkResult.errors.length > 0) {
171
+ stderr.write(`[warn] tree has ${walkResult.errors.length} pre-existing issue(s); proceeding\n`);
172
+ }
173
+ const { tree } = walkResult;
174
+ const fbs = tree.byId.get(fbsId);
175
+ if (!fbs || tree.kindById.get(fbsId) !== 'fbs') {
176
+ stderr.write(`[error] usage design: ${fbsId} not found or not an FBS\n`);
177
+ return 2;
178
+ }
179
+ if (fbs.uiBearing !== true) {
180
+ stderr.write(`[warn] design: ${fbsId} is not uiBearing (fbs.uiBearing is ${JSON.stringify(fbs.uiBearing)}); the Design substage is a no-op for non-UI FBS. Set uiBearing=true first (see 'rcf ui-classify ${fbsId}').\n`);
181
+ }
182
+ // Refuse Design substage writes when the baseline is missing (§6.2).
183
+ if (fbs.uiBearing === true && !tree.manifest?.uiBaseline && subs.length > 0) {
184
+ stderr.write(`[error] refused: ${fbsId} is uiBearing but no uiBaseline record exists on the manifest. Run: rcf ui-baseline init\n`);
185
+ return 4;
186
+ }
187
+
188
+ const now = deps.now ? new Date(deps.now()) : new Date();
189
+
190
+ if (flags['mark-complete']) {
191
+ return await runMarkComplete({ tree, projectRoot, fbs, fbsId, stdout, stderr, flags, now });
192
+ }
193
+ if (subs.length === 0) return runShow({ tree, fbs, fbsId, stdout, flags });
194
+
195
+ const first = subs[0];
196
+ const second = subs[1];
197
+ if (!KNOWN_SUB_VERBS.has(first)) {
198
+ stderr.write(`[error] usage design: unknown sub-verb '${first}' (expected ${[...KNOWN_SUB_VERBS].join(' | ')}, or --mark-complete)\n`);
199
+ return 2;
200
+ }
201
+ if (first === 'journeys' && second === 'add') {
202
+ return await runJourneysAdd({ tree, projectRoot, fbsId, stdout, stderr, flags, now });
203
+ }
204
+ if (first === 'nav' && second === 'set') {
205
+ return await runNavSet({ tree, projectRoot, fbsId, stdout, stderr, flags, now });
206
+ }
207
+ if (first === 'theme-a11y' && second === 'set') {
208
+ return await runThemeA11ySet({ tree, projectRoot, fbsId, stdout, stderr, flags, now });
209
+ }
210
+ stderr.write(`[error] usage design: unknown sub-verb combination '${[first, second].filter(Boolean).join(' ')}'\n`);
211
+ return 2;
212
+ }
213
+
214
+ function runShow({ tree, fbs, fbsId, stdout, flags }) {
215
+ const stage = fbs.designStage ?? null;
216
+ if (flags.json) {
217
+ stdout.write(`${JSON.stringify(stage, null, 2)}\n`);
218
+ return 0;
219
+ }
220
+ stdout.write(`design ${fbsId}: uiBearing=${JSON.stringify(fbs.uiBearing)} designStageComplete=${JSON.stringify(fbs.designStageComplete)}\n`);
221
+ const missing = missingDesignStageArtefacts(fbs);
222
+ if (!stage) {
223
+ stdout.write(' designStage: (none)\n');
224
+ } else {
225
+ stdout.write(` journeys: ${(stage.journeys ?? []).length}\n`);
226
+ stdout.write(` navModel: ${stage.navModel ? stage.navModel.shape + ' (' + (stage.navModel.routes ?? []).length + ' routes)' : '(none)'}\n`);
227
+ stdout.write(` themeAndA11y: ${stage.themeAndA11y ? stage.themeAndA11y.themeMode : '(none)'}\n`);
228
+ }
229
+ if (missing.length > 0) {
230
+ stdout.write(` next: author the missing artefact(s): ${missing.join(', ')}\n`);
231
+ } else if (fbs.designStageComplete !== true) {
232
+ stdout.write(' next: rcf design ' + fbsId + ' --mark-complete\n');
233
+ }
234
+ const baseline = tree.manifest?.uiBaseline;
235
+ if (fbs.uiBearing === true && !baseline) {
236
+ stdout.write(' refused (once you begin authoring): rcf ui-baseline init must run first.\n');
237
+ }
238
+ return 0;
239
+ }
240
+
241
+ async function runJourneysAdd({ tree, projectRoot, fbsId, stdout, stderr, flags, now }) {
242
+ const journey = {
243
+ id: flags.id,
244
+ actor: flags.actor,
245
+ goal: flags.goal,
246
+ steps: Array.isArray(flags.step) ? flags.step : [],
247
+ };
248
+ const result = await writeJourneyAdd({ projectRoot, tree, fbsId, journey, now });
249
+ return handleWriteResult({ result, stdout, stderr, quiet: Boolean(flags.quiet), verb: 'journeys add', ok: `journey '${journey.id}' added to ${fbsId}` });
250
+ }
251
+
252
+ async function runNavSet({ tree, projectRoot, fbsId, stdout, stderr, flags, now }) {
253
+ const routes = parseRouteEntries(flags.route ?? []);
254
+ if ('error' in routes) {
255
+ stderr.write(`[error] usage design nav set: ${routes.error}\n`);
256
+ return 2;
257
+ }
258
+ let signedInAsAffordance;
259
+ if (typeof flags['signed-in-as-affordance'] === 'string') {
260
+ if (flags['signed-in-as-affordance'] === 'true') signedInAsAffordance = true;
261
+ else if (flags['signed-in-as-affordance'] === 'false') signedInAsAffordance = false;
262
+ else {
263
+ stderr.write(`[error] usage design nav set: --signed-in-as-affordance must be true or false, got '${flags['signed-in-as-affordance']}'\n`);
264
+ return 2;
265
+ }
266
+ }
267
+ const result = await writeNavSet({
268
+ projectRoot, tree, fbsId,
269
+ shape: flags.shape,
270
+ routes: routes.routes,
271
+ signedInAsAffordance,
272
+ notes: flags.notes,
273
+ now,
274
+ });
275
+ return handleWriteResult({ result, stdout, stderr, quiet: Boolean(flags.quiet), verb: 'nav set', ok: `navModel written on ${fbsId} (${routes.routes.length} route(s))` });
276
+ }
277
+
278
+ async function runThemeA11ySet({ tree, projectRoot, fbsId, stdout, stderr, flags, now }) {
279
+ let cbp;
280
+ if (typeof flags['contrast-before-palette'] === 'string') {
281
+ if (flags['contrast-before-palette'] === 'true') cbp = true;
282
+ else if (flags['contrast-before-palette'] === 'false') cbp = false;
283
+ else {
284
+ stderr.write(`[error] usage design theme-a11y set: --contrast-before-palette must be true or false, got '${flags['contrast-before-palette']}'\n`);
285
+ return 2;
286
+ }
287
+ }
288
+ const result = await writeThemeA11ySet({
289
+ projectRoot, tree, fbsId,
290
+ themeMode: flags.mode,
291
+ themeTokensModule: flags.tokens,
292
+ contrastTargets: flags['contrast-targets'],
293
+ contrastTestPath: flags['contrast-test'],
294
+ contrastTestAuthoredBeforePalette: cbp,
295
+ now,
296
+ });
297
+ return handleWriteResult({ result, stdout, stderr, quiet: Boolean(flags.quiet), verb: 'theme-a11y set', ok: `themeAndA11y written on ${fbsId} (mode=${flags.mode})` });
298
+ }
299
+
300
+ async function runMarkComplete({ tree, projectRoot, fbs, fbsId, stdout, stderr, flags, now }) {
301
+ if (flags['dry-run']) {
302
+ const missing = missingDesignStageArtefacts(fbs);
303
+ if (missing.length > 0) {
304
+ stdout.write(`[dry-run] design --mark-complete: would refuse (${fbsId} designStage missing: ${missing.join(', ')})\n`);
305
+ return 0;
306
+ }
307
+ stdout.write(`[dry-run] design --mark-complete: would set designStageComplete=true on ${fbsId}\n`);
308
+ return 0;
309
+ }
310
+ const result = await writeMarkComplete({ projectRoot, tree, fbsId, now });
311
+ if (result && 'kind' in result && 'message' in result) {
312
+ if (result.kind === 'ioFailure') { writeUnexpectedFailure(result, stderr); return 1; }
313
+ stderr.write(`[error] ${result.kind} ${result.message}\n`);
314
+ if (result.kind === 'usage') return 4; // refused per §6.2
315
+ if (result.kind === 'validation') return 3;
316
+ return 1;
317
+ }
318
+ if (!flags.quiet) stdout.write(`design --mark-complete: ${fbsId} designStageComplete=true.\n`);
319
+ return 0;
320
+ }
321
+
322
+ function handleWriteResult({ result, stdout, stderr, quiet, verb, ok }) {
323
+ if (result && 'kind' in result && 'message' in result) {
324
+ if (result.kind === 'ioFailure') { writeUnexpectedFailure(result, stderr); return 1; }
325
+ stderr.write(`[error] ${result.kind} ${result.message}\n`);
326
+ if (result.kind === 'usage') return 2;
327
+ if (result.kind === 'validation' || result.kind === 'brokenReference') return 3;
328
+ return 1;
329
+ }
330
+ if (!quiet) stdout.write(`design ${verb}: ${ok}.\n`);
331
+ return 0;
332
+ }
333
+
334
+ function parseRouteEntries(entries) {
335
+ const routes = [];
336
+ for (const raw of entries) {
337
+ // Shape: path=label:authRequired. `path` can contain `/`; `label`
338
+ // MUST NOT contain `:`, and `authRequired` is the last colon-slice.
339
+ const eq = raw.indexOf('=');
340
+ if (eq < 0) return { error: `bad --route '${raw}'; expected path=label:authRequired` };
341
+ const path = raw.slice(0, eq);
342
+ const rhs = raw.slice(eq + 1);
343
+ const lastColon = rhs.lastIndexOf(':');
344
+ if (lastColon < 0) return { error: `bad --route '${raw}'; expected path=label:authRequired` };
345
+ const label = rhs.slice(0, lastColon);
346
+ const authString = rhs.slice(lastColon + 1);
347
+ if (authString !== 'true' && authString !== 'false') {
348
+ return { error: `bad --route '${raw}'; authRequired must be true or false` };
349
+ }
350
+ routes.push({ path, label, authRequired: authString === 'true' });
351
+ }
352
+ return { routes };
353
+ }