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,221 @@
1
+ // `rcf test-suite <ts-id> <verb> [options]` subcommand handler
2
+ // (verification-integrity-cluster-spec §5.1, §7).
3
+ //
4
+ // Two verbs:
5
+ // provenance author runtimeProvenance on TS/TC (§5.1)
6
+ // approve promote TS.status draft -> approved (§7 Option A)
7
+ //
8
+ // Both go through the store's `updateDocument` writer so the post-write
9
+ // validation gate and reference-integrity checks fire uniformly.
10
+
11
+ import { parseArgs } from 'node:util';
12
+
13
+ import { isRcfError, writeUnexpectedFailure } from '#core/errors';
14
+ import { updateDocument, walkTree } from '#core/store';
15
+
16
+ import { findProjectRoot } from '../view/index.js';
17
+
18
+ const PROVENANCE_OPTION_SPEC = {
19
+ tc: { type: 'string' },
20
+ profile: { type: 'string' },
21
+ 'env-var': { type: 'string', multiple: true },
22
+ host: { type: 'string', multiple: true },
23
+ notes: { type: 'string' },
24
+ force: { type: 'boolean' },
25
+ quiet: { type: 'boolean' },
26
+ help: { type: 'boolean' },
27
+ };
28
+
29
+ const APPROVE_OPTION_SPEC = {
30
+ force: { type: 'boolean' },
31
+ quiet: { type: 'boolean' },
32
+ help: { type: 'boolean' },
33
+ };
34
+
35
+ const VALID_PROFILES = new Set(['mock', 'stub', 'fixture', 'live', 'mixed']);
36
+
37
+ export const HELP = `Usage: rcf test-suite <ts-id> <verb> [options]
38
+
39
+ Verbs:
40
+ provenance Author runtimeProvenance on the TS or a specific TC
41
+ approve Promote the TS's authoringStatus to 'approved'
42
+
43
+ rcf test-suite <ts-id> provenance [--tc <tc-id>] --profile <mock|stub|fixture|live|mixed>
44
+ [--env-var VAR ...] [--host host ...] [--notes "..."] [--force]
45
+
46
+ Records the runtime the test actually addressed on a specific TC (or
47
+ every TC in the TS when --tc is omitted and no existing block would be
48
+ overwritten). Refuses to overwrite an existing block without --force.
49
+
50
+ rcf test-suite <ts-id> approve [--force]
51
+
52
+ Writes authoringStatus: approved on the TS. Refuses without --force
53
+ when the TS is 'superseded'; --force also allows re-approval after
54
+ needsRevision. Stage-4 auto-promotion invokes this verb after
55
+ coverage --strict + tests pass; the operator only needs it for manual
56
+ override.
57
+
58
+ Options common:
59
+ --quiet Suppress non-error stdout
60
+ --help Print this help
61
+ `;
62
+
63
+ /**
64
+ * @param {string[]} argv - argv slice after `test-suite`
65
+ * @param {object} [deps]
66
+ * @returns {Promise<number>}
67
+ */
68
+ export async function main(argv, deps = {}) {
69
+ const stdout = deps.stdout ?? process.stdout;
70
+ const stderr = deps.stderr ?? process.stderr;
71
+ const cwd = deps.cwd ?? process.cwd();
72
+
73
+ if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
74
+ stdout.write(HELP);
75
+ return argv[0] ? 0 : 2;
76
+ }
77
+
78
+ const tsId = argv[0];
79
+ const verb = argv[1];
80
+ const rest = argv.slice(2);
81
+ if (!verb) {
82
+ stderr.write('[error] usage test-suite: verb required (provenance | approve)\n');
83
+ return 2;
84
+ }
85
+ // 0.8.0 slug-train (w-2026-07-28-012 landmine 3, consumer-path
86
+ // straggler): widened `\d{3}` -> `\d{3,}` in lockstep with rcf-schemas
87
+ // 0.4.3's TS pattern. The previous shape hard-refused any TS >= 1000 --
88
+ // silent from the operator's perspective (the tsId matched the schema
89
+ // but the CLI verb refused with a usage error citing "TS-015").
90
+ if (!/^TS-\d{3,}$/.test(tsId)) {
91
+ stderr.write(`[error] usage test-suite: expected a TS id like TS-015 or TS-1000, got '${tsId}'\n`);
92
+ return 2;
93
+ }
94
+
95
+ const projectRoot = await findProjectRoot(cwd);
96
+ if (!projectRoot) {
97
+ stderr.write('[error] usage no project root found (no rcf/manifest.json in this directory or any ancestor).\n');
98
+ return 2;
99
+ }
100
+ const walkResult = await walkTree({ projectRoot });
101
+ const tree = walkResult.tree;
102
+ const ts = tree.byId.get(tsId);
103
+ if (!ts || tree.kindById.get(tsId) !== 'testSuite') {
104
+ stderr.write(`[error] usage test-suite: ${tsId} not found or not a Test Suite\n`);
105
+ return 2;
106
+ }
107
+
108
+ if (verb === 'provenance') return runProvenance({ tsId, ts, rest, projectRoot, tree, walkResult, stdout, stderr });
109
+ if (verb === 'approve') return runApprove({ tsId, ts, rest, projectRoot, tree, walkResult, stdout, stderr });
110
+ stderr.write(`[error] usage test-suite: unknown verb '${verb}' (expected provenance | approve)\n`);
111
+ return 2;
112
+ }
113
+
114
+ async function runProvenance({ tsId, ts, rest, projectRoot, tree, walkResult, stdout, stderr }) {
115
+ let parsed;
116
+ try {
117
+ parsed = parseArgs({ args: rest, options: PROVENANCE_OPTION_SPEC, allowPositionals: false, strict: true });
118
+ } catch (err) {
119
+ stderr.write(`[error] usage ${err.message}\n`);
120
+ return 2;
121
+ }
122
+ const flags = parsed.values;
123
+ if (flags.help) { stdout.write(HELP); return 0; }
124
+ if (!flags.profile) {
125
+ stderr.write('[error] usage test-suite provenance: --profile required\n');
126
+ return 2;
127
+ }
128
+ if (!VALID_PROFILES.has(flags.profile)) {
129
+ stderr.write(`[error] usage test-suite provenance: unknown --profile '${flags.profile}' (expected ${[...VALID_PROFILES].join(' | ')})\n`);
130
+ return 2;
131
+ }
132
+ const envVars = flags['env-var'] ?? [];
133
+ const hosts = flags.host ?? [];
134
+
135
+ const block = {
136
+ profile: flags.profile,
137
+ envVarsRequired: envVars,
138
+ externalHostsReached: hosts,
139
+ };
140
+ if (typeof flags.notes === 'string' && flags.notes.length > 0) {
141
+ // Redaction discipline: notes must never contain a token; a naive
142
+ // pattern grep here (looking for `=` followed by long alphanum
143
+ // stretches) is a belt-and-braces check that fires on the obvious
144
+ // paste-a-token mistake without pretending to be a real secret
145
+ // scanner.
146
+ if (/[A-Za-z0-9]{20,}/.test(flags.notes) && /=|token|secret|key/i.test(flags.notes)) {
147
+ stderr.write('[error] usage test-suite provenance: --notes looks like it contains a token or secret; the notes field is prose-only, never quotes credential material.\n');
148
+ return 2;
149
+ }
150
+ block.notes = flags.notes;
151
+ }
152
+
153
+ const tcs = Array.isArray(ts.testCases) ? ts.testCases : [];
154
+ const targetTcIds = flags.tc ? [flags.tc] : tcs.map((tc) => tc.id);
155
+ if (targetTcIds.length === 0) {
156
+ stderr.write(`[error] usage test-suite provenance: ${tsId} has no test cases\n`);
157
+ return 2;
158
+ }
159
+ if (flags.tc && !tcs.find((tc) => tc.id === flags.tc)) {
160
+ stderr.write(`[error] usage test-suite provenance: TC ${flags.tc} not found on ${tsId}\n`);
161
+ return 2;
162
+ }
163
+
164
+ const nextTcs = [];
165
+ for (const tc of tcs) {
166
+ if (!targetTcIds.includes(tc.id)) { nextTcs.push(tc); continue; }
167
+ if (tc.runtimeProvenance && !flags.force) {
168
+ stderr.write(`[error] refused test-suite provenance: TC ${tc.id} already carries runtimeProvenance; re-run with --force to overwrite.\n`);
169
+ return 4;
170
+ }
171
+ nextTcs.push({ ...tc, runtimeProvenance: block });
172
+ }
173
+
174
+ const sets = [{ path: 'testCases', value: nextTcs }];
175
+ const result = await updateDocument({
176
+ projectRoot, tree, id: tsId, patch: null, sets, options: {}, walkErrors: walkResult.errors,
177
+ });
178
+ if (isRcfError(result)) return handleWriterError(result, stderr);
179
+ stdout.write(`test-suite ${tsId} provenance updated on ${targetTcIds.length} test case(s).\n`);
180
+ return 0;
181
+ }
182
+
183
+ async function runApprove({ tsId, ts, rest, projectRoot, tree, walkResult, stdout, stderr }) {
184
+ let parsed;
185
+ try {
186
+ parsed = parseArgs({ args: rest, options: APPROVE_OPTION_SPEC, allowPositionals: false, strict: true });
187
+ } catch (err) {
188
+ stderr.write(`[error] usage ${err.message}\n`);
189
+ return 2;
190
+ }
191
+ const flags = parsed.values;
192
+ if (flags.help) { stdout.write(HELP); return 0; }
193
+ const current = ts.status;
194
+ if (current === 'approved') {
195
+ if (!flags.quiet) stdout.write(`test-suite ${tsId} is already approved (no-op).\n`);
196
+ return 0;
197
+ }
198
+ if (current === 'superseded' && !flags.force) {
199
+ stderr.write(`[error] refused test-suite approve: ${tsId} is superseded; re-run with --force to re-approve.\n`);
200
+ return 4;
201
+ }
202
+ if (current === 'needsRevision' && !flags.force) {
203
+ stderr.write(`[error] refused test-suite approve: ${tsId} is needsRevision; re-run with --force after re-work.\n`);
204
+ return 4;
205
+ }
206
+ const sets = [{ path: 'status', value: 'approved' }];
207
+ const result = await updateDocument({
208
+ projectRoot, tree, id: tsId, patch: null, sets, options: {}, walkErrors: walkResult.errors,
209
+ });
210
+ if (isRcfError(result)) return handleWriterError(result, stderr);
211
+ if (!flags.quiet) stdout.write(`test-suite ${tsId} approved.\n`);
212
+ return 0;
213
+ }
214
+
215
+ function handleWriterError(err, stderr) {
216
+ if (err.kind === 'ioFailure') { writeUnexpectedFailure(err, stderr); return 1; }
217
+ stderr.write(`[error] ${err.kind} ${err.message}\n`);
218
+ if (err.kind === 'usage') return 2;
219
+ if (err.kind === 'validation' || err.kind === 'brokenReference') return 3;
220
+ return 1;
221
+ }
@@ -0,0 +1,172 @@
1
+ // `rcf trace <id>` subcommand handler. Walks the graph from a pivot
2
+ // id in one direction or both. Phase 5 §D8 / §D9.
3
+ //
4
+ // --forward walks parent-child children + cross-link children (D7).
5
+ // --back walks parent-child ancestors only (D8: cross-links are NOT
6
+ // traversed by --back). --both emits {pivot, ancestors, descendants}.
7
+
8
+ import { parseArgs } from 'node:util';
9
+
10
+ import { formatErrors } from '#core/errors';
11
+ import { walkTree } from '#core/store';
12
+ import { findProjectRoot } from '../view/index.js';
13
+ import {
14
+ computeTrace,
15
+ formatJson,
16
+ formatMermaid,
17
+ formatTable,
18
+ kindOf,
19
+ } from '../query/index.js';
20
+
21
+ const OPTION_SPEC = {
22
+ forward: { type: 'boolean' },
23
+ back: { type: 'boolean' },
24
+ both: { type: 'boolean' },
25
+ format: { type: 'string' },
26
+ help: { type: 'boolean' },
27
+ // Phase 10 (X2 CodeNode bridge): extend a forward/both trace into the
28
+ // code layer. A backward trace from a <path> reaches code automatically.
29
+ 'to-code': { type: 'boolean' },
30
+ };
31
+
32
+ export const HELP = `Usage: rcf trace <id|path> [options]
33
+
34
+ Walk the graph from <id> forward (descendants), backward (ancestors),
35
+ or both. Default is --forward. When <id> is not a known document id, it
36
+ is tried as a source path (optionally #symbol-suffixed) and traced
37
+ backward from the matching Code Node(s) up to the root PRD / TAD / BS.
38
+
39
+ Options:
40
+ --forward Walk descendants (default)
41
+ --back Walk ancestors up to the root PRD / TAD / BS
42
+ --both Emit ancestors + descendants around <id>
43
+ --format <format> table (default) | json | mermaid
44
+ --to-code Extend the forward fan-out into Code Nodes
45
+ --help Print this help
46
+
47
+ Notes:
48
+ --forward, --back and --both are mutually exclusive.
49
+ Cross-links are NOT traversed by --back (fan-out is what 'impact' is for).
50
+ `;
51
+
52
+ const VALID_FORMATS = new Set(['table', 'json', 'mermaid']);
53
+
54
+ /**
55
+ * @param {string[]} argv - argv slice after `trace`
56
+ * @param {object} [deps]
57
+ * @returns {Promise<number>}
58
+ */
59
+ export async function main(argv, deps = {}) {
60
+ const stdout = deps.stdout ?? process.stdout;
61
+ const stderr = deps.stderr ?? process.stderr;
62
+ const cwd = deps.cwd ?? process.cwd();
63
+
64
+ let parsed;
65
+ try {
66
+ parsed = parseArgs({ args: argv, options: OPTION_SPEC, allowPositionals: true, strict: true });
67
+ } catch (err) {
68
+ stderr.write(`[error] usage ${err.message}\n`);
69
+ stderr.write(HELP);
70
+ return 2;
71
+ }
72
+ const flags = parsed.values;
73
+ const positionals = parsed.positionals;
74
+ if (flags.help) { stdout.write(HELP); return 0; }
75
+
76
+ // Direction: default forward; --forward | --back | --both mutually
77
+ // exclusive (spec §D9 test surface).
78
+ const set = ['forward', 'back', 'both'].filter((k) => flags[k]);
79
+ if (set.length > 1) {
80
+ stderr.write('[error] usage trace: --forward, --back and --both are mutually exclusive\n');
81
+ return 2;
82
+ }
83
+ let direction = 'forward';
84
+ if (flags.back) direction = 'back';
85
+ if (flags.both) direction = 'both';
86
+
87
+ const format = flags.format ?? 'table';
88
+ if (!VALID_FORMATS.has(format)) {
89
+ stderr.write(`[error] usage trace: unknown --format ${format} (expected table | json | mermaid)\n`);
90
+ return 2;
91
+ }
92
+
93
+ if (positionals.length === 0) {
94
+ stderr.write('[error] usage trace: expected exactly one <id>\n');
95
+ stderr.write(HELP);
96
+ return 2;
97
+ }
98
+ if (positionals.length > 1) {
99
+ stderr.write('[error] usage trace: multiple positional ids are not supported\n');
100
+ return 2;
101
+ }
102
+ const id = positionals[0];
103
+ if (id.includes('*') || id.includes('?')) {
104
+ stderr.write('[error] usage trace: wildcard / glob positional not supported\n');
105
+ return 2;
106
+ }
107
+
108
+ const projectRoot = await findProjectRoot(cwd);
109
+ if (!projectRoot) {
110
+ 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');
111
+ return 2;
112
+ }
113
+ const { tree, errors } = await walkTree({ projectRoot });
114
+ if (errors.length > 0) {
115
+ stderr.write(`${formatErrors(errors, { verbose: false, strict: false })}\n`);
116
+ return 3;
117
+ }
118
+
119
+ const includeCode = Boolean(flags['to-code']);
120
+
121
+ // Phase 10 (X2 CodeNode bridge): path mode. If the positional is not a
122
+ // known document id, try resolving it as a source path to one or more
123
+ // Code Nodes, then trace each backward (path -> CN -> AC -> US -> REQ ->
124
+ // PRD). This is the `rcf trace <path>` blast-radius-from-code query.
125
+ if (!kindOf(tree, id)) {
126
+ const cnIds = resolveCodeNodesForPath(tree, id);
127
+ if (cnIds.length === 0) {
128
+ stderr.write(`[error] usage trace: id ${id} not found (no document or code node matches)\n`);
129
+ return 2;
130
+ }
131
+ const chunks = [];
132
+ for (const cnId of cnIds) {
133
+ const cn = tree.byId.get(cnId);
134
+ const res = computeTrace(tree, { id: cnId, direction: 'back' });
135
+ let out;
136
+ if (format === 'json') out = formatJson(res, 'trace');
137
+ else if (format === 'mermaid') out = formatMermaid(res, 'trace');
138
+ else out = `# ${cnId} (${cn?.path ?? '?'})\n${formatTable(res, 'trace')}`;
139
+ chunks.push(out);
140
+ }
141
+ stdout.write(chunks.join('\n'));
142
+ return 0;
143
+ }
144
+
145
+ const result = computeTrace(tree, { id, direction, includeCode });
146
+
147
+ let output;
148
+ if (format === 'json') output = formatJson(result, 'trace');
149
+ else if (format === 'mermaid') output = formatMermaid(result, 'trace');
150
+ else output = formatTable(result, 'trace');
151
+ stdout.write(output);
152
+ return 0;
153
+ }
154
+
155
+ /**
156
+ * Phase 10: resolve a source-path query to Code Node ids. Matches a CN
157
+ * when its `path` equals the query (file-level or file#symbol form) or
158
+ * when the query names the file that a symbol-level CN lives in.
159
+ *
160
+ * @param {object} tree - walker TreeModel
161
+ * @param {string} query - a repo-relative path, optionally #symbol-suffixed
162
+ * @returns {string[]} matching CN ids, sorted
163
+ */
164
+ function resolveCodeNodesForPath(tree, query) {
165
+ const out = [];
166
+ for (const cn of tree.codeNodes ?? []) {
167
+ const cnPath = cn.path ?? '';
168
+ const cnFile = cnPath.split('#')[0];
169
+ if (cnPath === query || cnFile === query) out.push(cn.cnId);
170
+ }
171
+ return out.sort();
172
+ }
@@ -0,0 +1,292 @@
1
+ // `rcf ui-baseline <verb>` handler (ui-design-gate-0.7.0-spec §5.5).
2
+ //
3
+ // Three sub-verbs:
4
+ // init [--prd <prdId>] [--input <file>] [--dry-run] [--reset]
5
+ // Present the ruled defaults, take operator ack + any opt-outs,
6
+ // write the uiBaseline record (with --reset appending the prior
7
+ // record to uiBaselineHistory[]).
8
+ // show [--json]
9
+ // Print the current baseline record.
10
+ // opt-out --field <path> --reason "..."
11
+ // Append an entry to operatorOptOuts[] on the existing baseline.
12
+ //
13
+ // The init verb also applies the preflight seam pickup (spec §3.2):
14
+ // preflight-recorded design-shape answers whose uiBaselineWritePath
15
+ // targets defaults.* land as defaults on the fresh record. The Track A
16
+ // finalised `skippedUiBaseline` seam is the interlock; the opt-out
17
+ // ledger entries stay in place either way.
18
+
19
+ import { readFile } from 'node:fs/promises';
20
+ import { parseArgs } from 'node:util';
21
+ import { createInterface } from 'node:readline/promises';
22
+ import process from 'node:process';
23
+
24
+ import { walkTree } from '#core/store';
25
+ import { writeUnexpectedFailure, rcfError } from '#core/errors';
26
+
27
+ import { findProjectRoot } from '../view/index.js';
28
+ import {
29
+ UI_BASELINE_DEFAULTS_V1,
30
+ composeUiBaselineRecord,
31
+ isKnownBaselinePath,
32
+ normaliseNonInteractiveInput,
33
+ preflightSeamOverrides,
34
+ runInteractiveSession,
35
+ writeUiBaselineOptOut,
36
+ writeUiBaselineRecord,
37
+ } from '../ui-baseline/index.js';
38
+
39
+ const OPTION_SPEC = {
40
+ prd: { type: 'string' },
41
+ input: { type: 'string' },
42
+ 'dry-run': { type: 'boolean' },
43
+ reset: { type: 'boolean' },
44
+ json: { type: 'boolean' },
45
+ quiet: { type: 'boolean' },
46
+ field: { type: 'string' },
47
+ reason: { type: 'string' },
48
+ help: { type: 'boolean' },
49
+ };
50
+
51
+ export const HELP = `Usage: rcf ui-baseline <verb> [options]
52
+
53
+ Manage the project's ruled UI defaults (theme, layout, contrast,
54
+ components, auth flow). Written once per project as a uiBaseline record
55
+ on the manifest; every UI-bearing FBS inherits from it. Every opt-out
56
+ is explicit; silence is never an opt-out.
57
+
58
+ Sub-verbs:
59
+ init Present the ruled defaults and write the
60
+ uiBaseline record. Interactive by default;
61
+ --input reads a pre-filled JSON file for CI.
62
+ show Print the current baseline record.
63
+ opt-out Append an entry to operatorOptOuts[].
64
+
65
+ Options:
66
+ --prd <prd-id> PRD id to attach to the baseline (default:
67
+ the project's PRD).
68
+ --input <path> Non-interactive init: read a JSON file with
69
+ { optOuts: [], overrides: {} } shape.
70
+ --dry-run init only: print the composed record; do not
71
+ write.
72
+ --reset init only: append the previous baseline to
73
+ uiBaselineHistory[] before writing.
74
+ --json show only: emit JSON.
75
+ --field <path> opt-out only: dot-path into defaults (for
76
+ example 'themeMode' or 'componentVocabulary.declaredComponents').
77
+ --reason "..." opt-out only: plain-text reason (>= 20 chars).
78
+ --quiet Suppress non-error confirmations.
79
+ --help Print this help.
80
+
81
+ Exit codes:
82
+ 0 success
83
+ 1 IO / unexpected runtime failure
84
+ 2 usage error
85
+ 3 schema or tree validation failure
86
+ 4 session cancelled by operator (init only)
87
+ `;
88
+
89
+ /**
90
+ * @param {string[]} argv
91
+ * @param {object} [deps]
92
+ * @returns {Promise<number>}
93
+ */
94
+ export async function main(argv, deps = {}) {
95
+ const stdout = deps.stdout ?? process.stdout;
96
+ const stderr = deps.stderr ?? process.stderr;
97
+ const stdin = deps.stdin ?? process.stdin;
98
+ const cwd = deps.cwd ?? process.cwd();
99
+
100
+ let parsed;
101
+ try {
102
+ parsed = parseArgs({ args: argv, options: OPTION_SPEC, allowPositionals: true, strict: true });
103
+ } catch (err) {
104
+ stderr.write(`[error] usage ${err.message}\n`);
105
+ stderr.write(HELP);
106
+ return 2;
107
+ }
108
+ const flags = parsed.values;
109
+ if (flags.help) { stdout.write(HELP); return 0; }
110
+ if (parsed.positionals.length !== 1) {
111
+ stderr.write('[error] usage ui-baseline: expected one sub-verb (init | show | opt-out)\n');
112
+ return 2;
113
+ }
114
+ const sub = parsed.positionals[0];
115
+ if (!['init', 'show', 'opt-out'].includes(sub)) {
116
+ stderr.write(`[error] usage ui-baseline: unknown sub-verb '${sub}' (expected init | show | opt-out)\n`);
117
+ return 2;
118
+ }
119
+
120
+ const projectRoot = await findProjectRoot(cwd);
121
+ if (!projectRoot) {
122
+ stderr.write('[error] usage no project root found (no rcf/manifest.json in this directory or any ancestor). Run `rcf init` first.\n');
123
+ return 2;
124
+ }
125
+ const walkResult = await walkTree({ projectRoot });
126
+ if (walkResult.errors.length > 0) {
127
+ stderr.write(`[warn] tree has ${walkResult.errors.length} pre-existing issue(s); proceeding\n`);
128
+ }
129
+ const { tree } = walkResult;
130
+
131
+ if (sub === 'show') return runShow({ tree, stdout, flags });
132
+ if (sub === 'opt-out') return await runOptOut({ tree, projectRoot, stdout, stderr, flags, now: deps.now });
133
+ return await runInit({ tree, projectRoot, stdin, stdout, stderr, flags, now: deps.now });
134
+ }
135
+
136
+ function runShow({ tree, stdout, flags }) {
137
+ const record = tree.manifest?.uiBaseline;
138
+ if (!record) {
139
+ stdout.write('ui-baseline: no baseline recorded. Run \'rcf ui-baseline init\'.\n');
140
+ return 0;
141
+ }
142
+ if (flags.json) {
143
+ stdout.write(`${JSON.stringify(record, null, 2)}\n`);
144
+ return 0;
145
+ }
146
+ stdout.write(`ui-baseline ${record.id} (prd=${record.prdId}, ackedAt=${record.operatorAckAt})\n`);
147
+ const defaults = record.defaults ?? {};
148
+ const optOuts = new Set((record.operatorOptOuts ?? []).map((o) => o.field));
149
+ for (const spec of UI_BASELINE_DEFAULTS_V1) {
150
+ const value = readDot(defaults, spec.path);
151
+ const marker = optOuts.has(spec.path) ? '*' : ' ';
152
+ stdout.write(` ${marker} ${spec.path}: ${formatValue(value)}\n`);
153
+ }
154
+ if ((record.operatorOptOuts ?? []).length > 0) {
155
+ stdout.write('opt-outs:\n');
156
+ for (const o of record.operatorOptOuts) {
157
+ stdout.write(` * ${o.field} - ${o.reason} (ack ${o.operatorAckAt})\n`);
158
+ }
159
+ }
160
+ return 0;
161
+ }
162
+
163
+ async function runOptOut({ tree, projectRoot, stdout, stderr, flags, now }) {
164
+ if (typeof flags.field !== 'string' || flags.field.length === 0) {
165
+ stderr.write('[error] usage ui-baseline opt-out: --field is required\n');
166
+ return 2;
167
+ }
168
+ if (typeof flags.reason !== 'string' || flags.reason.length < 20) {
169
+ stderr.write('[error] usage ui-baseline opt-out: --reason is required and must be at least 20 characters\n');
170
+ return 2;
171
+ }
172
+ const normalisedField = flags.field.startsWith('defaults.') ? flags.field.slice('defaults.'.length) : flags.field;
173
+ if (!isKnownBaselinePath(normalisedField)) {
174
+ stderr.write(`[error] usage ui-baseline opt-out: unknown baseline field '${normalisedField}' (see 'rcf ui-baseline show' for the field list)\n`);
175
+ return 2;
176
+ }
177
+ const result = await writeUiBaselineOptOut({
178
+ projectRoot,
179
+ tree,
180
+ field: normalisedField,
181
+ reason: flags.reason,
182
+ isKnownField: isKnownBaselinePath,
183
+ now: now ? new Date(now()) : new Date(),
184
+ });
185
+ if (result && 'kind' in result && 'message' in result) {
186
+ if (result.kind === 'ioFailure') { writeUnexpectedFailure(result, stderr); return 1; }
187
+ stderr.write(`[error] ${result.kind} ${result.message}\n`);
188
+ if (result.kind === 'usage') return 2;
189
+ if (result.kind === 'validation') return 3;
190
+ return 1;
191
+ }
192
+ if (!flags.quiet) stdout.write(`ui-baseline: recorded opt-out for ${normalisedField}.\n`);
193
+ return 0;
194
+ }
195
+
196
+ async function runInit({ tree, projectRoot, stdin, stdout, stderr, flags, now }) {
197
+ const prdId = flags.prd ?? tree.prd?.prdId;
198
+ if (!prdId) {
199
+ stderr.write('[error] usage ui-baseline init: no --prd given and no PRD found on the tree\n');
200
+ return 2;
201
+ }
202
+ const priorBaseline = tree.manifest?.uiBaseline ?? null;
203
+ if (priorBaseline && !flags.reset && !flags['dry-run']) {
204
+ stderr.write(`[error] refused ui-baseline init: a baseline (${priorBaseline.id}) already exists; pass --reset to author a fresh one (the prior lands under uiBaselineHistory[]).\n`);
205
+ return 2;
206
+ }
207
+
208
+ const preflightOverrides = preflightSeamOverrides(tree.manifest);
209
+ const isTty = Boolean(stdout.isTTY && stdin.isTTY);
210
+
211
+ let session;
212
+ try {
213
+ if (flags.input) {
214
+ const raw = await readFile(flags.input, 'utf8');
215
+ const parsed = JSON.parse(raw);
216
+ session = normaliseNonInteractiveInput(parsed);
217
+ // Merge preflight overrides UNDER the input overrides: input wins.
218
+ session.overrides = { ...preflightOverrides, ...session.overrides };
219
+ } else if (isTty) {
220
+ const rl = createInterface({ input: stdin, output: stdout });
221
+ const prompt = (q) => rl.question(q);
222
+ const write = (line) => stdout.write(`${line}\n`);
223
+ try {
224
+ session = await runInteractiveSession({ prompt, write, preflightOverrides });
225
+ } finally {
226
+ rl.close();
227
+ }
228
+ } else {
229
+ stderr.write('[error] usage ui-baseline init: not on a TTY and no --input file given\n');
230
+ return 2;
231
+ }
232
+ } catch (err) {
233
+ if (err && String(err.message).includes('session cancelled by operator')) {
234
+ stderr.write('[error] ui-baseline init: session cancelled; no record written.\n');
235
+ return 4;
236
+ }
237
+ writeUnexpectedFailure(
238
+ rcfError({ kind: 'ioFailure', message: `ui-baseline init: ${err.message}`, stack: err.stack }),
239
+ stderr,
240
+ );
241
+ return 1;
242
+ }
243
+
244
+ const record = composeUiBaselineRecord({
245
+ manifest: tree.manifest,
246
+ prdId,
247
+ optOuts: session.optOuts,
248
+ overrides: session.overrides,
249
+ now: now ? new Date(now()) : new Date(),
250
+ });
251
+
252
+ if (flags['dry-run']) {
253
+ stdout.write(`${JSON.stringify(record, null, 2)}\n`);
254
+ if (!flags.quiet) stdout.write(`[dry-run] ui-baseline init: would write ${record.id} (${session.optOuts.length} opt-outs).\n`);
255
+ return 0;
256
+ }
257
+
258
+ const result = await writeUiBaselineRecord({
259
+ projectRoot, tree, record, options: { reset: Boolean(flags.reset) },
260
+ });
261
+ if (result && 'kind' in result && 'message' in result) {
262
+ if (result.kind === 'ioFailure') { writeUnexpectedFailure(result, stderr); return 1; }
263
+ stderr.write(`[error] ${result.kind} ${result.message}\n`);
264
+ if (result.kind === 'usage') return 2;
265
+ if (result.kind === 'validation') return 3;
266
+ return 1;
267
+ }
268
+ if (!flags.quiet) {
269
+ stdout.write(`ui-baseline init: wrote ${record.id} to rcf/manifest.json (${session.optOuts.length} opt-out(s)).\n`);
270
+ if (Object.keys(preflightOverrides).length > 0) {
271
+ stdout.write(` Applied ${Object.keys(preflightOverrides).length} preflight seam pickup(s): ${Object.keys(preflightOverrides).join(', ')}\n`);
272
+ }
273
+ }
274
+ return 0;
275
+ }
276
+
277
+ function readDot(obj, path) {
278
+ const parts = String(path).split('.');
279
+ let cursor = obj;
280
+ for (const key of parts) {
281
+ if (cursor === null || cursor === undefined) return undefined;
282
+ cursor = cursor[key];
283
+ }
284
+ return cursor;
285
+ }
286
+
287
+ function formatValue(v) {
288
+ if (v === undefined) return '(unset)';
289
+ if (Array.isArray(v)) return `[${v.map((x) => JSON.stringify(x)).join(', ')}]`;
290
+ if (typeof v === 'string') return v;
291
+ return JSON.stringify(v);
292
+ }