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,123 @@
1
+ // Strand 2: seed writer for `rcf/knowledge/` (0.6.0 spec §3). Init
2
+ // writes four files unconditionally on a fresh scaffold; on a re-run,
3
+ // any managed file that already exists is left byte-identical. The
4
+ // seed is convention-only in v1 (no CLI verb, no retrieval machinery);
5
+ // what ships is the README that teaches the convention and a stub
6
+ // INDEX.md for human bullets.
7
+ //
8
+ // Idempotency contract (§3.3): the four files above are the ones that
9
+ // count as "managed by init". Any file the operator drops under
10
+ // `rcf/knowledge/` stays as-is. If the whole tree is missing on a
11
+ // re-run, init re-seeds it (a repo that pre-dates 0.6.0 gets the
12
+ // convention retroactively without needing a hand-edit).
13
+
14
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
15
+ import { dirname, join } from 'node:path';
16
+
17
+ /** Canonical README (spec §3.4). Verbatim. */
18
+ export const KNOWLEDGE_README = `# Knowledge
19
+
20
+ This directory is the project's memory. Everything an agent working on
21
+ this repo should not have to relearn from scratch belongs here.
22
+
23
+ ## The convention
24
+
25
+ - **\`notes/\`**: internal facts. Decisions, gotchas, runtime facts,
26
+ the small things that always cost time to rediscover ("the CI matrix
27
+ uses Node 22 not 24", "the pnpm store path on this machine is
28
+ non-default", "the local Postgres is on port 5433 not 5432"). Written
29
+ for future agents and future you, not for public readers.
30
+ - **\`docs/\`**: user-facing prose the project might surface elsewhere.
31
+ Design notes the operator wants tidy, sections that might land in a
32
+ README or a spec, prose intended for a wider audience.
33
+
34
+ ## The rules
35
+
36
+ 1. **One topic per file.** The filename is the topic
37
+ (\`notes/ci-node-version.md\`, not \`notes/general.md\`). One paragraph
38
+ is enough. An empty file that only carries a pointer to somewhere
39
+ else is fine. A file called \`misc.md\` or \`general.md\` is not.
40
+ 2. **Write on learn.** If this session established a fact the next
41
+ session should not have to rediscover, write it here before the
42
+ session ends.
43
+ 3. **Grep before asking.** Before asking the stakeholder a question,
44
+ \`rg -n '<topic>' rcf/knowledge/\` to see whether the answer is
45
+ already here.
46
+
47
+ ## \`INDEX.md\`
48
+
49
+ \`INDEX.md\` is a human table of contents. Add a bullet when you add a
50
+ file. Keep it grouped by area if it grows. Nobody scans it
51
+ programmatically in v1; it is for the person landing on the repo cold.
52
+
53
+ ## What this is not
54
+
55
+ This is not a knowledge graph. There is no CLI verb, no indexer, no
56
+ vector search. If a project needs one, that is a v2 decision, not a v1
57
+ default. The convention above is deliberately cheap. The value is in
58
+ the discipline of using it.
59
+ `;
60
+
61
+ /** Canonical INDEX.md stub (spec §3.4). Verbatim. */
62
+ export const KNOWLEDGE_INDEX = `# Knowledge index
63
+
64
+ The human table of contents for \`rcf/knowledge/\`. Add a bullet when you
65
+ add a file. Keep it grouped by area if it grows.
66
+
67
+ ## Notes
68
+
69
+ _(none yet)_
70
+
71
+ ## Docs
72
+
73
+ _(none yet)_
74
+ `;
75
+
76
+ /**
77
+ * Absolute path helpers for the seed files.
78
+ *
79
+ * @param {string} projectRoot
80
+ * @returns {{ dir: string, readme: string, index: string, notesKeep: string, docsKeep: string }}
81
+ */
82
+ export function knowledgePaths(projectRoot) {
83
+ const dir = join(projectRoot, 'rcf', 'knowledge');
84
+ return {
85
+ dir,
86
+ readme: join(dir, 'README.md'),
87
+ index: join(dir, 'INDEX.md'),
88
+ notesKeep: join(dir, 'notes', '.gitkeep'),
89
+ docsKeep: join(dir, 'docs', '.gitkeep'),
90
+ };
91
+ }
92
+
93
+ async function writeIfMissing(path, contents) {
94
+ try {
95
+ await readFile(path, 'utf8');
96
+ return { path, action: /** @type {'kept'} */ ('kept') };
97
+ } catch (err) {
98
+ if (/** @type {NodeJS.ErrnoException} */ (err).code !== 'ENOENT') throw err;
99
+ }
100
+ await mkdir(dirname(path), { recursive: true });
101
+ await writeFile(path, contents, 'utf8');
102
+ return { path, action: /** @type {'created'} */ ('created') };
103
+ }
104
+
105
+ /**
106
+ * Seed the four managed files under `rcf/knowledge/`. Any file already
107
+ * present is left byte-identical (AC-2.2). If the whole tree is missing
108
+ * on a re-run, all four are recreated (AC-2.3).
109
+ *
110
+ * @param {object} args
111
+ * @param {string} args.projectRoot
112
+ * @returns {Promise<{ writes: Array<{ file: string, action: 'created' | 'kept' }> }>}
113
+ */
114
+ export async function writeKnowledgeSeed({ projectRoot }) {
115
+ const paths = knowledgePaths(projectRoot);
116
+ const results = [
117
+ { file: 'rcf/knowledge/README.md', ...(await writeIfMissing(paths.readme, KNOWLEDGE_README)) },
118
+ { file: 'rcf/knowledge/INDEX.md', ...(await writeIfMissing(paths.index, KNOWLEDGE_INDEX)) },
119
+ { file: 'rcf/knowledge/notes/.gitkeep', ...(await writeIfMissing(paths.notesKeep, '')) },
120
+ { file: 'rcf/knowledge/docs/.gitkeep', ...(await writeIfMissing(paths.docsKeep, '')) },
121
+ ];
122
+ return { writes: results.map((r) => ({ file: r.file, action: r.action })) };
123
+ }
@@ -0,0 +1,193 @@
1
+ // Generic "managed block in file X" primitive (0.6.0 spec §11). Both
2
+ // the agent-instructions check (strand 1) and the gitignore check
3
+ // (strand 4) call into this module with their respective (marker-pair,
4
+ // canonical-text) tuples. Doctor's per-check code stays small; every
5
+ // managed-block check shares one implementation of the detect / extract
6
+ // / splice / hash primitives.
7
+ //
8
+ // Design rules from the spec:
9
+ // - Detection semantics enumerated at §2.6 / §4.4 (missing, stale-hash,
10
+ // legacy-markers, orphan-marker, duplicate-block).
11
+ // - --fix is wholesale replacement inside the markers; zero bytes
12
+ // touched outside; newline normalisation is off (§2.7).
13
+ // - Structurally corrupt files (orphan / duplicate) are refused by
14
+ // --fix; hand-repair message returned.
15
+ // - Idempotent: running --fix on already-clean state is a no-op that
16
+ // writes zero files (§2.7 last bullet).
17
+
18
+ import { createHash } from 'node:crypto';
19
+
20
+ /**
21
+ * @typedef {'clean' | 'missing-block' | 'stale-hash' | 'legacy-markers' | 'orphan-marker' | 'duplicate-block'} BlockState
22
+ */
23
+
24
+ /**
25
+ * @typedef {object} BlockOptions
26
+ * @property {string} markerBegin - current-generation begin marker.
27
+ * @property {string} markerEnd - current-generation end marker.
28
+ * @property {string | null} [legacyMarkerBegin] - pre-0.6.0 begin marker; omit to disable legacy detection.
29
+ * @property {string | null} [legacyMarkerEnd] - pre-0.6.0 end marker; omit to disable legacy detection.
30
+ */
31
+
32
+ /**
33
+ * SHA-256 of the trimmed text, matching gen-managed-artefacts.mjs's
34
+ * hashOf. Whitespace around marker lines does not trip staleness.
35
+ *
36
+ * @param {string} text
37
+ * @returns {string}
38
+ */
39
+ export function hashInnerContent(text) {
40
+ return createHash('sha256').update(text.trim(), 'utf8').digest('hex');
41
+ }
42
+
43
+ /**
44
+ * Locate the marker pair's byte offsets within `text`. Returns
45
+ * `beginIndex` (offset of MARKER_BEGIN's first char), `endIndex`
46
+ * (offset of the character AFTER MARKER_END, INCLUSIVE of a trailing
47
+ * newline if one exists), and `innerText` (bytes strictly between the
48
+ * markers, no surrounding newlines).
49
+ *
50
+ * @param {string} text
51
+ * @param {string} markerBegin
52
+ * @param {string} markerEnd
53
+ * @returns {{ beginIndex: number, endIndex: number, innerText: string } | null}
54
+ */
55
+ export function locateMarkers(text, markerBegin, markerEnd) {
56
+ const beginIndex = text.indexOf(markerBegin);
57
+ if (beginIndex < 0) return null;
58
+ const innerStart = beginIndex + markerBegin.length;
59
+ const endStart = text.indexOf(markerEnd, innerStart);
60
+ if (endStart < 0) return null;
61
+ let endIndex = endStart + markerEnd.length;
62
+ if (text[endIndex] === '\n') endIndex += 1;
63
+ const innerText = text.slice(innerStart, endStart);
64
+ return { beginIndex, endIndex, innerText };
65
+ }
66
+
67
+ /**
68
+ * Count occurrences of `needle` in `haystack`.
69
+ *
70
+ * @param {string} haystack
71
+ * @param {string} needle
72
+ * @returns {number}
73
+ */
74
+ function countOccurrences(haystack, needle) {
75
+ if (needle.length === 0) return 0;
76
+ let count = 0;
77
+ let i = 0;
78
+ while (true) {
79
+ const at = haystack.indexOf(needle, i);
80
+ if (at < 0) return count;
81
+ count += 1;
82
+ i = at + needle.length;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Classify the file's managed-block state given the current-generation
88
+ * markers, the pre-0.6.0 legacy markers (optional), and the canonical
89
+ * hash the caller expects the block's inner content to match.
90
+ *
91
+ * @param {string} fileText - the file's raw bytes as UTF-8 string.
92
+ * @param {string} canonicalHash - SHA-256 of the current canonical text (trimmed).
93
+ * @param {BlockOptions} opts
94
+ * @returns {BlockState}
95
+ */
96
+ export function classifyBlock(fileText, canonicalHash, opts) {
97
+ const { markerBegin, markerEnd, legacyMarkerBegin, legacyMarkerEnd } = opts;
98
+ const beginCount = countOccurrences(fileText, markerBegin);
99
+ const endCount = countOccurrences(fileText, markerEnd);
100
+ // Duplicate block detection first: more than one pair is unambiguous.
101
+ if (beginCount >= 2 && endCount >= 2) return 'duplicate-block';
102
+ // Orphan (unpaired current-marker) after duplicate; either half missing.
103
+ if (beginCount !== endCount) return 'orphan-marker';
104
+ // If neither current marker present, fall through to legacy / missing.
105
+ if (beginCount === 0) {
106
+ if (legacyMarkerBegin && legacyMarkerEnd) {
107
+ const legBegin = countOccurrences(fileText, legacyMarkerBegin);
108
+ const legEnd = countOccurrences(fileText, legacyMarkerEnd);
109
+ if (legBegin >= 1 && legEnd >= 1) return 'legacy-markers';
110
+ }
111
+ return 'missing-block';
112
+ }
113
+ // Exactly one pair present; hash the inner content.
114
+ const located = locateMarkers(fileText, markerBegin, markerEnd);
115
+ if (!located) return 'orphan-marker';
116
+ const innerHash = hashInnerContent(located.innerText);
117
+ return innerHash === canonicalHash ? 'clean' : 'stale-hash';
118
+ }
119
+
120
+ /**
121
+ * Produce the composed block string (markers + canonical text). Callers
122
+ * splice this into the file at the location `locateMarkers` reported.
123
+ *
124
+ * @param {string} canonicalText
125
+ * @param {string} markerBegin
126
+ * @param {string} markerEnd
127
+ * @returns {string}
128
+ */
129
+ export function composeBlock(canonicalText, markerBegin, markerEnd) {
130
+ const trimmed = canonicalText.trim();
131
+ return `${markerBegin}\n${trimmed}\n${markerEnd}\n`;
132
+ }
133
+
134
+ /**
135
+ * Rewrite `fileText` in place with the composed block:
136
+ * - `clean`: return the input unchanged (idempotent no-op).
137
+ * - `stale-hash`: splice the composed block over the current pair,
138
+ * preserving every byte outside the markers.
139
+ * - `legacy-markers`: splice the composed block over the legacy pair,
140
+ * preserving every byte outside the legacy markers.
141
+ * - `missing-block`: append the composed block at end of file, adding a
142
+ * leading blank-line separator if the file did not end in a newline.
143
+ * - `orphan-marker` / `duplicate-block`: refused; returns null so the
144
+ * caller emits the hand-repair message.
145
+ *
146
+ * @param {string} fileText
147
+ * @param {string} canonicalText
148
+ * @param {BlockOptions} opts
149
+ * @param {string} canonicalHash
150
+ * @returns {{ nextText: string, action: 'noop' | 'replaced' | 'migrated' | 'appended' } | null}
151
+ */
152
+ export function applyFix(fileText, canonicalText, opts, canonicalHash) {
153
+ const state = classifyBlock(fileText, canonicalHash, opts);
154
+ const composed = composeBlock(canonicalText, opts.markerBegin, opts.markerEnd);
155
+ if (state === 'clean') {
156
+ return { nextText: fileText, action: 'noop' };
157
+ }
158
+ if (state === 'orphan-marker' || state === 'duplicate-block') {
159
+ return null;
160
+ }
161
+ if (state === 'stale-hash') {
162
+ const loc = locateMarkers(fileText, opts.markerBegin, opts.markerEnd);
163
+ if (!loc) return null;
164
+ const nextText = fileText.slice(0, loc.beginIndex) + composed + fileText.slice(loc.endIndex);
165
+ return { nextText, action: 'replaced' };
166
+ }
167
+ if (state === 'legacy-markers' && opts.legacyMarkerBegin && opts.legacyMarkerEnd) {
168
+ const loc = locateMarkers(fileText, opts.legacyMarkerBegin, opts.legacyMarkerEnd);
169
+ if (!loc) return null;
170
+ const nextText = fileText.slice(0, loc.beginIndex) + composed + fileText.slice(loc.endIndex);
171
+ return { nextText, action: 'migrated' };
172
+ }
173
+ // missing-block
174
+ const separator = fileText.length === 0
175
+ ? ''
176
+ : (fileText.endsWith('\n') ? '\n' : '\n\n');
177
+ return { nextText: `${fileText}${separator}${composed}`, action: 'appended' };
178
+ }
179
+
180
+ /**
181
+ * Extract only the inner content of the current-marker block, or null
182
+ * if the file has no clean pair. For diagnostics: doctor's `stale-hash`
183
+ * report can quote or diff the block if it wants to (v1 does not).
184
+ *
185
+ * @param {string} fileText
186
+ * @param {string} markerBegin
187
+ * @param {string} markerEnd
188
+ * @returns {string | null}
189
+ */
190
+ export function extractInnerContent(fileText, markerBegin, markerEnd) {
191
+ const located = locateMarkers(fileText, markerBegin, markerEnd);
192
+ return located ? located.innerText : null;
193
+ }
@@ -0,0 +1,166 @@
1
+ // Aggregator seam for the managed `.gitignore` block (0.6.0 spec §4,
2
+ // D-4 normative aggregator function). The registry of "files RCF wants
3
+ // ignored by default" is composed by `managedGitignoreEntries()`. This
4
+ // module is the single shared file that 0.7.0+ extends: import a
5
+ // per-feature entry constant from its owning module and insert into the
6
+ // array, one file, two lines added, no doctor code change (§4.1's
7
+ // worked 0.7.0 diff).
8
+ //
9
+ // The `.gitignore` marker convention shares the visual shape of the
10
+ // agent-instructions markers but lives in a disjoint namespace
11
+ // (gitignore uses `#` for comments). Doctor's per-check logic still
12
+ // calls into the generic managed-block primitive in `managed-block.js`
13
+ // with these markers as its (markerBegin, markerEnd) pair.
14
+ //
15
+ // Composition primitives split by concern:
16
+ // - Production callers use the no-arg accessors `composeGitignoreBlock()`,
17
+ // `composeGitignoreInner()`, `computeGitignoreBlockHash()` — these
18
+ // compose from the module-owned aggregator (`managedGitignoreEntries()`).
19
+ // - Tests that need to prove the extension pipeline works with a
20
+ // synthetic second entry call the `*FromEntries(entries)` helpers
21
+ // directly with an explicit entries array. No production code path
22
+ // accepts an entries override; the aggregator's registered set is the
23
+ // only source of truth at runtime.
24
+
25
+ import { hashInnerContent } from './managed-block.js';
26
+ import { identityEntry } from './identity-seed.js';
27
+ import { preflightEntry } from '../preflight/secrets.js';
28
+ // Track C+D §9.3: gitignore the view-server pid file and supervisor log.
29
+ import { viewServerGitignoreEntry, viewServerLogGitignoreEntry } from '../view-supervisor/manifest-writer.js';
30
+
31
+ /**
32
+ * @typedef {object} GitignoreEntry
33
+ * @property {string} path - gitignore-syntax line (glob or literal path relative to project root).
34
+ * @property {string} owner - human-readable feature name shown in the block's owner comment.
35
+ * @property {string} since - SemVer minor the entry landed on.
36
+ */
37
+
38
+ /**
39
+ * Registered gitignore entries the managed block covers. Each entry
40
+ * exports from its owning feature module (per §4.1's contract: feature
41
+ * modules OWN their entry constant and do NOT import the aggregator,
42
+ * avoiding import-time mutation surprises); the aggregator imports and
43
+ * composes deterministically. Adding an entry (e.g. the 0.7.0
44
+ * credentials side-file) is a one-file edit here: import the constant
45
+ * and insert into the returned array. Do NOT mutate the returned array
46
+ * at runtime; callers should treat it as read-only.
47
+ *
48
+ * @returns {GitignoreEntry[]}
49
+ */
50
+ export function managedGitignoreEntries() {
51
+ return [
52
+ identityEntry,
53
+ preflightEntry,
54
+ viewServerGitignoreEntry,
55
+ viewServerLogGitignoreEntry,
56
+ ];
57
+ }
58
+
59
+ /** Owner-comment header on the begin marker (§4.3 canonical text). */
60
+ export const GITIGNORE_MARKER_BEGIN = '# rcf:managed:begin (managed by `rcf doctor`; do not edit inside)';
61
+ export const GITIGNORE_MARKER_END = '# rcf:managed:end';
62
+
63
+ /**
64
+ * Compose the full managed block (marker + inner + marker + terminating
65
+ * newline) from an explicit entries array. Pure helper — used by the
66
+ * production accessor `composeGitignoreBlock()` (which sources entries
67
+ * from the aggregator) AND by tests that need to exercise the
68
+ * composition pipeline with a synthetic entries array without any
69
+ * production test hook.
70
+ *
71
+ * @param {GitignoreEntry[]} entries
72
+ * @returns {string}
73
+ */
74
+ export function composeGitignoreBlockFromEntries(entries) {
75
+ const lines = [GITIGNORE_MARKER_BEGIN];
76
+ for (const e of entries) {
77
+ lines.push(`# ${e.owner} (since ${e.since})`);
78
+ lines.push(e.path);
79
+ }
80
+ lines.push(GITIGNORE_MARKER_END);
81
+ return `${lines.join('\n')}\n`;
82
+ }
83
+
84
+ /**
85
+ * Compose the inner content the block wraps (everything strictly
86
+ * between the marker lines) from an explicit entries array. Doctor's
87
+ * stale-hash primitive hashes this (trimmed) so operator whitespace
88
+ * around markers is ignored.
89
+ *
90
+ * @param {GitignoreEntry[]} entries
91
+ * @returns {string}
92
+ */
93
+ export function composeGitignoreInnerFromEntries(entries) {
94
+ const lines = [];
95
+ for (const e of entries) {
96
+ lines.push(`# ${e.owner} (since ${e.since})`);
97
+ lines.push(e.path);
98
+ }
99
+ return `${lines.join('\n')}`;
100
+ }
101
+
102
+ /**
103
+ * SHA-256 of the composed inner content (trimmed) for an explicit
104
+ * entries array. Doctor compares this to the hash of the inner content
105
+ * extracted from the file; mismatch means stale.
106
+ *
107
+ * @param {GitignoreEntry[]} entries
108
+ * @returns {string}
109
+ */
110
+ export function computeGitignoreBlockHashFromEntries(entries) {
111
+ return hashInnerContent(composeGitignoreInnerFromEntries(entries));
112
+ }
113
+
114
+ /**
115
+ * Compose the full managed block from the aggregator. Deterministic
116
+ * order (aggregator's registered order); each entry preceded by its
117
+ * one-line owner comment `# {owner} (since {since})`; trailing newline
118
+ * outside the end marker so the block sits cleanly inside a file.
119
+ * Callers splice the return value in place; the return is the FULL
120
+ * block (marker + inner + marker + terminating newline).
121
+ *
122
+ * @returns {string}
123
+ */
124
+ export function composeGitignoreBlock() {
125
+ return composeGitignoreBlockFromEntries(managedGitignoreEntries());
126
+ }
127
+
128
+ /**
129
+ * The inner-content string the composed block wraps, sourced from the
130
+ * aggregator.
131
+ *
132
+ * @returns {string}
133
+ */
134
+ export function composeGitignoreInner() {
135
+ return composeGitignoreInnerFromEntries(managedGitignoreEntries());
136
+ }
137
+
138
+ /**
139
+ * SHA-256 of the composed inner content (trimmed), sourced from the
140
+ * aggregator. Doctor's `stale-hash` production callsite.
141
+ *
142
+ * @returns {string}
143
+ */
144
+ export function computeGitignoreBlockHash() {
145
+ return computeGitignoreBlockHashFromEntries(managedGitignoreEntries());
146
+ }
147
+
148
+ /**
149
+ * Locate the managed gitignore block within `text`. Same primitive as
150
+ * managed-block.js's `locateMarkers` but exported here for the doctor
151
+ * flow's convenience.
152
+ *
153
+ * @param {string} text
154
+ * @returns {{ beginIndex: number, endIndex: number, innerText: string } | null}
155
+ */
156
+ export function extractGitignoreBlock(text) {
157
+ const beginIndex = text.indexOf(GITIGNORE_MARKER_BEGIN);
158
+ if (beginIndex < 0) return null;
159
+ const innerStart = beginIndex + GITIGNORE_MARKER_BEGIN.length;
160
+ const endStart = text.indexOf(GITIGNORE_MARKER_END, innerStart);
161
+ if (endStart < 0) return null;
162
+ let endIndex = endStart + GITIGNORE_MARKER_END.length;
163
+ if (text[endIndex] === '\n') endIndex += 1;
164
+ const innerText = text.slice(innerStart, endStart);
165
+ return { beginIndex, endIndex, innerText };
166
+ }
@@ -0,0 +1,49 @@
1
+ // Marker constants for the agent-instructions managed block (0.6.0
2
+ // spec §2.5, D-7). Extracted to a single module so a marker rename in
3
+ // six months lands in one place, not scattered across agent-setup,
4
+ // doctor, MCP funnel and tests. Drift between two definitions is the
5
+ // exact bug the strand-1 legacy-markers migration exists to prevent.
6
+ //
7
+ // The legacy pair (`<!-- rcf:begin -->` / `<!-- rcf:end -->`) is also
8
+ // exported so callers that need to recognise the pre-0.6.0 marker
9
+ // generation (the MCP setup funnel, doctor's `legacy-markers` check)
10
+ // have a single source of truth for its shape too.
11
+ //
12
+ // The managed-gitignore module owns its own marker constants in
13
+ // `managed-gitignore.js` (disjoint namespace: gitignore uses `#`
14
+ // comments rather than HTML comments).
15
+
16
+ /** The 0.6.0+ managed-block markers `rcf init` writes into agent-instructions files. */
17
+ export const MARKER_BEGIN = '<!-- rcf:managed:begin -->';
18
+ export const MARKER_END = '<!-- rcf:managed:end -->';
19
+
20
+ /** Pre-0.6.0 marker generation; kept for the transitional `hasAgentMarker` recogniser and doctor's `legacy-markers` check. */
21
+ export const LEGACY_MARKER_BEGIN = '<!-- rcf:begin -->';
22
+ export const LEGACY_MARKER_END = '<!-- rcf:end -->';
23
+
24
+ /**
25
+ * Compose the replace-in-place regex for the current-marker managed
26
+ * block. Non-greedy match between MARKER_BEGIN and MARKER_END so a file
27
+ * with structural corruption (unpaired markers, duplicate blocks) does
28
+ * not silently swallow arbitrary regions - doctor detects those states
29
+ * explicitly and refuses to --fix them.
30
+ *
31
+ * @returns {RegExp}
32
+ */
33
+ export function markerRegex() {
34
+ return new RegExp(`${escapeRegex(MARKER_BEGIN)}[\\s\\S]*?${escapeRegex(MARKER_END)}`);
35
+ }
36
+
37
+ /**
38
+ * Same shape as `markerRegex` for the legacy pair. Used by doctor's
39
+ * `legacy-markers` --fix path and by the transitional migration.
40
+ *
41
+ * @returns {RegExp}
42
+ */
43
+ export function legacyMarkerRegex() {
44
+ return new RegExp(`${escapeRegex(LEGACY_MARKER_BEGIN)}[\\s\\S]*?${escapeRegex(LEGACY_MARKER_END)}`);
45
+ }
46
+
47
+ function escapeRegex(s) {
48
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
49
+ }
@@ -0,0 +1,119 @@
1
+ // Ruled UI-baseline defaults (ui-design-gate-0.7.0-spec §6.1).
2
+ //
3
+ // Every value is Baz-ruled (Entry 5 of the 2026-07-29 cold-run operator
4
+ // feedback log) or a §4.3 mandate from the review-phase analysis. The
5
+ // `rcf ui-baseline init` verb presents these values as the defaults;
6
+ // the operator accepts (silence is NOT an opt-out) or overrides with a
7
+ // reason via `operatorOptOuts[]` (spec §6.2).
8
+ //
9
+ // Editing this catalogue is a spec-level change: baseline evolution
10
+ // happens via `rcf ui-baseline opt-out` per project, or a new spec
11
+ // revision.
12
+
13
+ /**
14
+ * @typedef {object} UiBaselineFieldSpec
15
+ * @property {string} path dot-path into `uiBaseline.defaults` (for opt-out records + refusal messages)
16
+ * @property {*} value the ruled value (the "present as default" for interactive init)
17
+ * @property {string} label operator-facing short label (used in the summary screen)
18
+ * @property {string} rulingSource brief citation for provenance
19
+ */
20
+
21
+ /**
22
+ * The v1 defaults catalogue. Ordered for stable presentation on the
23
+ * interactive summary screen: theme + layout first (most visible),
24
+ * then contrast + a11y, then typography + interaction, then auth flow.
25
+ * @type {UiBaselineFieldSpec[]}
26
+ */
27
+ export const UI_BASELINE_DEFAULTS_V1 = Object.freeze([
28
+ { path: 'themeMode', value: 'light-default-with-toggle', label: 'Theme mode', rulingSource: 'Baz Entry 5' },
29
+ { path: 'sharedLayoutModule', value: 'src/ui/layout.ts', label: 'Shared layout module', rulingSource: 'Baz Entry 5 + review-phase mandate 3' },
30
+ { path: 'designTokensModule', value: 'src/ui/tokens.ts', label: 'Design tokens module', rulingSource: 'review-phase mandate 1' },
31
+ { path: 'noHexInViewFiles', value: true, label: 'No hex literals in view files', rulingSource: 'review-phase mandate 1' },
32
+ { path: 'contrastTarget', value: 'WCAG AA', label: 'Contrast target', rulingSource: 'Baz Entry 5 + review-phase mandate 6' },
33
+ { path: 'contrastTestBeforePalette', value: true, label: 'Contrast test authored before palette', rulingSource: 'review-phase mandate 10' },
34
+ { path: 'focusRingsRequired', value: true, label: 'Focus rings required', rulingSource: 'review-phase mandate 6' },
35
+ { path: 'hoverStatesRequired', value: true, label: 'Hover states required', rulingSource: 'review-phase mandate 8' },
36
+ { path: 'componentVocabulary.declaredComponents', value: ['Button', 'Input', 'Card', 'Badge', 'Table', 'Notice'], label: 'Component vocabulary', rulingSource: 'review-phase mandate 5' },
37
+ { path: 'componentVocabulary.singleBadgeShape', value: true, label: 'Single badge shape', rulingSource: 'review-phase mandate 5' },
38
+ { path: 'typography.baseFontStack', value: 'system-ui', label: 'Base font stack', rulingSource: 'review-phase mandate 7' },
39
+ { path: 'typography.bodyLineHeight', value: 1.5, label: 'Body line height', rulingSource: 'review-phase mandate 7' },
40
+ { path: 'typography.headingLineHeight', value: 1.25, label: 'Heading line height', rulingSource: 'review-phase mandate 7' },
41
+ { path: 'typography.proseMaxWidth', value: '72ch', label: 'Prose max width', rulingSource: 'review-phase mandate 7' },
42
+ { path: 'interactionDefaults.loadingIndicatorOnFetch', value: true, label: 'Loading indicator on fetch', rulingSource: 'review-phase mandate 8' },
43
+ { path: 'interactionDefaults.disabledStateVisuallyDistinct', value: true, label: 'Disabled state visually distinct', rulingSource: 'review-phase mandate 8' },
44
+ { path: 'authFlow.htmlLoginPageRequired', value: true, label: 'HTML login page required', rulingSource: 'review-phase mandate 9 + Baz "real login flow"' },
45
+ { path: 'authFlow.smokeChecksRequired', value: true, label: 'Auth-REQ smoke checks required', rulingSource: 'review-phase recommendation 9' },
46
+ ]);
47
+
48
+ /**
49
+ * Deep-set a value at a dot-path on a plain object. Intermediate
50
+ * objects are created as needed. Arrays are copied (not shared with
51
+ * the caller). Returns the mutated top-level object for chaining.
52
+ *
53
+ * @param {object} obj
54
+ * @param {string} path dot-path (e.g. `componentVocabulary.declaredComponents`)
55
+ * @param {*} value
56
+ * @returns {object}
57
+ */
58
+ export function deepSet(obj, path, value) {
59
+ const parts = String(path).split('.');
60
+ let cursor = obj;
61
+ for (let i = 0; i < parts.length - 1; i += 1) {
62
+ const key = parts[i];
63
+ if (typeof cursor[key] !== 'object' || cursor[key] === null || Array.isArray(cursor[key])) {
64
+ cursor[key] = {};
65
+ }
66
+ cursor = cursor[key];
67
+ }
68
+ const last = parts[parts.length - 1];
69
+ cursor[last] = Array.isArray(value) ? [...value] : value;
70
+ return obj;
71
+ }
72
+
73
+ /**
74
+ * Deep-get a value at a dot-path. Returns `undefined` when any
75
+ * intermediate segment is absent.
76
+ *
77
+ * @param {object} obj
78
+ * @param {string} path
79
+ * @returns {*}
80
+ */
81
+ export function deepGet(obj, path) {
82
+ const parts = String(path).split('.');
83
+ let cursor = obj;
84
+ for (const key of parts) {
85
+ if (cursor === null || cursor === undefined) return undefined;
86
+ cursor = cursor[key];
87
+ }
88
+ return cursor;
89
+ }
90
+
91
+ /**
92
+ * Compose the ruled defaults object from the v1 catalogue. Every field
93
+ * carries its Baz-ruled value; caller overrides land under
94
+ * `operatorOptOuts[]` on the surrounding record.
95
+ *
96
+ * @param {object} [overrides] dot-path -> value overrides (used by preflight seam pickup)
97
+ * @returns {object}
98
+ */
99
+ export function composeDefaults(overrides = {}) {
100
+ const out = {};
101
+ for (const spec of UI_BASELINE_DEFAULTS_V1) {
102
+ deepSet(out, spec.path, spec.value);
103
+ }
104
+ for (const [path, value] of Object.entries(overrides ?? {})) {
105
+ deepSet(out, path, value);
106
+ }
107
+ return out;
108
+ }
109
+
110
+ /**
111
+ * True when the given dot-path is a known baseline field. Used by
112
+ * `rcf ui-baseline opt-out` to refuse writes to non-existent fields.
113
+ *
114
+ * @param {string} path
115
+ * @returns {boolean}
116
+ */
117
+ export function isKnownBaselinePath(path) {
118
+ return UI_BASELINE_DEFAULTS_V1.some((s) => s.path === path);
119
+ }
@@ -0,0 +1,25 @@
1
+ // Public surface for the UI-baseline module (ui-design-gate-0.7.0
2
+ // spec §5.4, §6). The CLI handler lives at `src/cli/ui-baseline.js`;
3
+ // consumers reach the module through this barrel.
4
+
5
+ export {
6
+ UI_BASELINE_DEFAULTS_V1,
7
+ composeDefaults,
8
+ deepGet,
9
+ deepSet,
10
+ isKnownBaselinePath,
11
+ } from './defaults.js';
12
+
13
+ export {
14
+ baselineDesignDisagreement,
15
+ composeUiBaselineRecord,
16
+ nextUiBaselineId,
17
+ preflightSeamOverrides,
18
+ writeUiBaselineOptOut,
19
+ writeUiBaselineRecord,
20
+ } from './manifest-writer.js';
21
+
22
+ export {
23
+ normaliseNonInteractiveInput,
24
+ runInteractiveSession,
25
+ } from './session.js';