rcf-lite 0.0.1 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (342) hide show
  1. package/CHANGELOG.md +288 -0
  2. package/LICENSE +202 -0
  3. package/README.md +92 -4
  4. package/bin/rcf-verify.js +122 -0
  5. package/bin/rcf.js +174 -0
  6. package/bin/view-supervisor-child.mjs +14 -0
  7. package/fixtures/canary-manifest.json +103 -0
  8. package/guidance/README.md +25 -0
  9. package/guidance/build-cycle-playbook.md +492 -0
  10. package/guidance/build-cycle.md +40 -0
  11. package/guidance/document-model.md +45 -0
  12. package/guidance/elicitation-playbook.md +442 -0
  13. package/guidance/harness-template.md +246 -0
  14. package/guidance/managed/README.md +63 -0
  15. package/guidance/managed/agent-instructions-block.hash +1 -0
  16. package/guidance/managed/agent-instructions-block.md +220 -0
  17. package/guidance/managed/legacy-fragment-hashes.json +22 -0
  18. package/guidance/manifest.json +21 -0
  19. package/guidance/overview.md +48 -0
  20. package/guidance/persona-programme.md +44 -0
  21. package/package.json +69 -8
  22. package/rcf/adrs/adr-001.json +25 -0
  23. package/rcf/adrs/adr-002.json +25 -0
  24. package/rcf/adrs/adr-003.json +25 -0
  25. package/rcf/adrs/adr-004.json +20 -0
  26. package/rcf/adrs/adr-005.json +20 -0
  27. package/rcf/adrs/adr-006.json +25 -0
  28. package/rcf/adrs/adr-007.json +25 -0
  29. package/rcf/adrs/adr-008.json +25 -0
  30. package/rcf/adrs/adr-009.json +25 -0
  31. package/rcf/build-sequence.json +11 -0
  32. package/rcf/code-nodes/cn-001.json +14 -0
  33. package/rcf/code-nodes/cn-002.json +14 -0
  34. package/rcf/code-nodes/cn-003.json +15 -0
  35. package/rcf/code-nodes/cn-004.json +17 -0
  36. package/rcf/code-nodes/cn-005.json +19 -0
  37. package/rcf/code-nodes/cn-006.json +17 -0
  38. package/rcf/code-nodes/cn-007.json +16 -0
  39. package/rcf/code-nodes/cn-008.json +19 -0
  40. package/rcf/code-nodes/cn-009.json +16 -0
  41. package/rcf/code-nodes/cn-010.json +18 -0
  42. package/rcf/code-nodes/cn-011.json +18 -0
  43. package/rcf/code-nodes/cn-012.json +18 -0
  44. package/rcf/code-nodes/cn-013.json +19 -0
  45. package/rcf/code-nodes/cn-014.json +16 -0
  46. package/rcf/code-nodes/cn-015.json +16 -0
  47. package/rcf/code-nodes/cn-016.json +16 -0
  48. package/rcf/code-nodes/cn-017.json +16 -0
  49. package/rcf/code-nodes/cn-018.json +15 -0
  50. package/rcf/code-nodes/cn-019.json +16 -0
  51. package/rcf/code-nodes/cn-020.json +16 -0
  52. package/rcf/code-nodes/cn-021.json +16 -0
  53. package/rcf/code-nodes/cn-022.json +16 -0
  54. package/rcf/code-nodes/cn-023.json +16 -0
  55. package/rcf/code-nodes/cn-024.json +16 -0
  56. package/rcf/code-nodes/cn-025.json +16 -0
  57. package/rcf/code-nodes/cn-026.json +16 -0
  58. package/rcf/code-nodes/cn-027.json +16 -0
  59. package/rcf/code-nodes/cn-028.json +16 -0
  60. package/rcf/code-nodes/cn-029.json +19 -0
  61. package/rcf/code-nodes/cn-030.json +14 -0
  62. package/rcf/code-nodes/cn-031.json +14 -0
  63. package/rcf/code-nodes/cn-032.json +14 -0
  64. package/rcf/code-nodes/cn-033.json +14 -0
  65. package/rcf/code-nodes/cn-034.json +14 -0
  66. package/rcf/code-nodes/cn-035.json +14 -0
  67. package/rcf/code-nodes/cn-036.json +14 -0
  68. package/rcf/code-nodes/cn-037.json +14 -0
  69. package/rcf/code-nodes/cn-038.json +14 -0
  70. package/rcf/code-nodes/cn-039.json +14 -0
  71. package/rcf/code-nodes/cn-040.json +14 -0
  72. package/rcf/code-nodes/cn-041.json +14 -0
  73. package/rcf/code-nodes/cn-042.json +14 -0
  74. package/rcf/code-nodes/cn-043.json +14 -0
  75. package/rcf/code-nodes/cn-044.json +14 -0
  76. package/rcf/code-nodes/cn-045.json +14 -0
  77. package/rcf/code-nodes/cn-046.json +14 -0
  78. package/rcf/code-nodes/cn-047.json +14 -0
  79. package/rcf/code-nodes/cn-048.json +14 -0
  80. package/rcf/code-nodes/cn-049.json +14 -0
  81. package/rcf/code-nodes/cn-050.json +14 -0
  82. package/rcf/code-nodes/cn-051.json +14 -0
  83. package/rcf/code-nodes/cn-052.json +14 -0
  84. package/rcf/code-nodes/cn-053.json +14 -0
  85. package/rcf/code-nodes/cn-054.json +14 -0
  86. package/rcf/code-nodes/cn-055.json +14 -0
  87. package/rcf/code-nodes/cn-056.json +14 -0
  88. package/rcf/code-nodes/cn-057.json +14 -0
  89. package/rcf/fbs/fbs-001.json +49 -0
  90. package/rcf/fbs/fbs-002.json +42 -0
  91. package/rcf/fbs/fbs-003.json +37 -0
  92. package/rcf/fbs/fbs-004.json +39 -0
  93. package/rcf/fbs/fbs-005.json +38 -0
  94. package/rcf/fbs/fbs-006.json +48 -0
  95. package/rcf/fbs/fbs-007.json +39 -0
  96. package/rcf/fbs/fbs-008.json +40 -0
  97. package/rcf/fbs/fbs-009.json +36 -0
  98. package/rcf/fbs/fbs-010.json +41 -0
  99. package/rcf/fbs/fbs-011.json +36 -0
  100. package/rcf/fbs/fbs-012.json +46 -0
  101. package/rcf/fbs/fbs-013.json +42 -0
  102. package/rcf/fbs/fbs-014.json +49 -0
  103. package/rcf/fbs/fbs-015.json +32 -0
  104. package/rcf/manifest.json +17 -0
  105. package/rcf/prd.json +47 -0
  106. package/rcf/requirements/req-001.json +19 -0
  107. package/rcf/requirements/req-002.json +19 -0
  108. package/rcf/requirements/req-003.json +19 -0
  109. package/rcf/requirements/req-004.json +19 -0
  110. package/rcf/requirements/req-005.json +19 -0
  111. package/rcf/requirements/req-006.json +19 -0
  112. package/rcf/requirements/req-007.json +19 -0
  113. package/rcf/requirements/req-008.json +19 -0
  114. package/rcf/requirements/req-009.json +19 -0
  115. package/rcf/tacs/tac-001.json +45 -0
  116. package/rcf/tacs/tac-002.json +109 -0
  117. package/rcf/tacs/tac-003.json +40 -0
  118. package/rcf/tacs/tac-004.json +51 -0
  119. package/rcf/tacs/tac-005.json +52 -0
  120. package/rcf/tacs/tac-006.json +104 -0
  121. package/rcf/tacs/tac-007.json +38 -0
  122. package/rcf/tacs/tac-008.json +51 -0
  123. package/rcf/tad.json +51 -0
  124. package/rcf/test-suites/PENDING.md +23 -0
  125. package/rcf/test-suites/ts-001.json +38 -0
  126. package/rcf/test-suites/ts-002.json +38 -0
  127. package/rcf/test-suites/ts-003.json +43 -0
  128. package/rcf/test-suites/ts-004.json +44 -0
  129. package/rcf/test-suites/ts-005.json +30 -0
  130. package/rcf/test-suites/ts-006.json +36 -0
  131. package/rcf/test-suites/ts-007.json +43 -0
  132. package/rcf/test-suites/ts-008.json +37 -0
  133. package/rcf/test-suites/ts-009.json +38 -0
  134. package/rcf/test-suites/ts-010.json +38 -0
  135. package/rcf/test-suites/ts-011.json +44 -0
  136. package/rcf/test-suites/ts-012.json +36 -0
  137. package/rcf/test-suites/ts-013.json +38 -0
  138. package/rcf/test-suites/ts-014.json +38 -0
  139. package/rcf/test-suites/ts-015.json +38 -0
  140. package/rcf/test-suites/ts-016.json +37 -0
  141. package/rcf/test-suites/ts-017.json +38 -0
  142. package/rcf/test-suites/ts-018.json +38 -0
  143. package/rcf/test-suites/ts-019.json +37 -0
  144. package/rcf/test-suites/ts-020.json +46 -0
  145. package/rcf/test-suites/ts-021.json +46 -0
  146. package/rcf/test-suites/ts-022.json +46 -0
  147. package/rcf/test-suites/ts-023.json +46 -0
  148. package/rcf/test-suites/ts-024.json +46 -0
  149. package/rcf/test-suites/ts-025.json +52 -0
  150. package/rcf/user-stories/us-101.json +40 -0
  151. package/rcf/user-stories/us-102.json +40 -0
  152. package/rcf/user-stories/us-201.json +40 -0
  153. package/rcf/user-stories/us-202.json +40 -0
  154. package/rcf/user-stories/us-203.json +32 -0
  155. package/rcf/user-stories/us-301.json +40 -0
  156. package/rcf/user-stories/us-302.json +40 -0
  157. package/rcf/user-stories/us-303.json +40 -0
  158. package/rcf/user-stories/us-304.json +40 -0
  159. package/rcf/user-stories/us-401.json +40 -0
  160. package/rcf/user-stories/us-402.json +40 -0
  161. package/rcf/user-stories/us-403.json +40 -0
  162. package/rcf/user-stories/us-501.json +40 -0
  163. package/rcf/user-stories/us-502.json +40 -0
  164. package/rcf/user-stories/us-503.json +40 -0
  165. package/rcf/user-stories/us-601.json +40 -0
  166. package/rcf/user-stories/us-602.json +40 -0
  167. package/rcf/user-stories/us-701.json +40 -0
  168. package/rcf/user-stories/us-702.json +40 -0
  169. package/rcf/user-stories/us-801.json +49 -0
  170. package/rcf/user-stories/us-802.json +49 -0
  171. package/rcf/user-stories/us-803.json +49 -0
  172. package/rcf/user-stories/us-804.json +49 -0
  173. package/rcf/user-stories/us-805.json +49 -0
  174. package/rcf/user-stories/us-901.json +40 -0
  175. package/src/.gitkeep +0 -0
  176. package/src/browser-verify/auth-smoke.js +109 -0
  177. package/src/browser-verify/index.js +29 -0
  178. package/src/browser-verify/invariants.js +336 -0
  179. package/src/browser-verify/manifest-writer.js +189 -0
  180. package/src/browser-verify/runner.js +171 -0
  181. package/src/build/bundle.js +198 -0
  182. package/src/build/formatters/json.js +18 -0
  183. package/src/build/formatters/markdown.js +406 -0
  184. package/src/build/index.js +14 -0
  185. package/src/build/mark.js +177 -0
  186. package/src/build/queue.js +285 -0
  187. package/src/cli/browser-verify.js +231 -0
  188. package/src/cli/build.js +584 -0
  189. package/src/cli/coverage.js +219 -0
  190. package/src/cli/create.js +355 -0
  191. package/src/cli/delete.js +127 -0
  192. package/src/cli/design.js +353 -0
  193. package/src/cli/doctor.js +548 -0
  194. package/src/cli/fbs.js +171 -0
  195. package/src/cli/finalise.js +377 -0
  196. package/src/cli/guidance.js +206 -0
  197. package/src/cli/help.js +156 -0
  198. package/src/cli/impact.js +119 -0
  199. package/src/cli/init.js +282 -0
  200. package/src/cli/intake.js +153 -0
  201. package/src/cli/link.js +128 -0
  202. package/src/cli/mcp.js +160 -0
  203. package/src/cli/preflight.js +220 -0
  204. package/src/cli/read.js +162 -0
  205. package/src/cli/req-baseline.js +269 -0
  206. package/src/cli/req-classify.js +135 -0
  207. package/src/cli/review.js +295 -0
  208. package/src/cli/test-suite.js +216 -0
  209. package/src/cli/trace.js +172 -0
  210. package/src/cli/ui-baseline.js +292 -0
  211. package/src/cli/ui-classify.js +108 -0
  212. package/src/cli/update.js +197 -0
  213. package/src/cli/validate.js +168 -0
  214. package/src/cli/view.js +452 -0
  215. package/src/core/baseline-catalog/data/auth.json +42 -0
  216. package/src/core/baseline-catalog/data/http-api.json +42 -0
  217. package/src/core/baseline-catalog/data/notifications.json +33 -0
  218. package/src/core/baseline-catalog/data/persistence.json +33 -0
  219. package/src/core/baseline-catalog/data/web-ui.json +60 -0
  220. package/src/core/baseline-catalog/index.js +121 -0
  221. package/src/core/errors/index.js +167 -0
  222. package/src/core/fixtures/register-canary/canary-prompt-01.json +18 -0
  223. package/src/core/fixtures/register-canary/canary-prompt-02.json +21 -0
  224. package/src/core/fixtures/register-canary/canary-prompt-03.json +17 -0
  225. package/src/core/isolation/index.js +60 -0
  226. package/src/core/mcp/framing.js +103 -0
  227. package/src/core/mcp/index.js +8 -0
  228. package/src/core/mcp/server.js +228 -0
  229. package/src/core/patterns/register-canary.js +209 -0
  230. package/src/core/patterns/req-shapes.js +158 -0
  231. package/src/core/patterns/services.js +358 -0
  232. package/src/core/patterns/ui-shapes.js +166 -0
  233. package/src/core/store/cn-resolve.js +134 -0
  234. package/src/core/store/derive-deps.js +93 -0
  235. package/src/core/store/ids.js +78 -0
  236. package/src/core/store/index.js +20 -0
  237. package/src/core/store/init.js +255 -0
  238. package/src/core/store/loader.js +211 -0
  239. package/src/core/store/tp-resolve.js +176 -0
  240. package/src/core/store/validator.js +191 -0
  241. package/src/core/store/walker.js +898 -0
  242. package/src/core/store/writer.js +1849 -0
  243. package/src/design/index.js +11 -0
  244. package/src/design/writer.js +271 -0
  245. package/src/finalise/detect.js +129 -0
  246. package/src/finalise/index.js +17 -0
  247. package/src/finalise/ingest.js +119 -0
  248. package/src/finalise/install.js +119 -0
  249. package/src/finalise/ship-without-verified.js +131 -0
  250. package/src/finalise/spawn.js +84 -0
  251. package/src/intake/fidelity.js +105 -0
  252. package/src/intake/index.js +6 -0
  253. package/src/intake/manifest-writer.js +100 -0
  254. package/src/intake/orchestrator.js +138 -0
  255. package/src/intake/validate.js +80 -0
  256. package/src/mcp/map-errors.js +131 -0
  257. package/src/mcp/prompts.js +49 -0
  258. package/src/mcp/resources.js +244 -0
  259. package/src/mcp/tools.js +1204 -0
  260. package/src/preflight/design-shapes.js +185 -0
  261. package/src/preflight/index.js +90 -0
  262. package/src/preflight/manifest-writer.js +264 -0
  263. package/src/preflight/scanner.js +206 -0
  264. package/src/preflight/secrets.js +134 -0
  265. package/src/preflight/session.js +246 -0
  266. package/src/query/attestation.js +285 -0
  267. package/src/query/coverage.js +308 -0
  268. package/src/query/formatters/json.js +21 -0
  269. package/src/query/formatters/mermaid.js +209 -0
  270. package/src/query/formatters/table.js +203 -0
  271. package/src/query/impact.js +173 -0
  272. package/src/query/index.js +9 -0
  273. package/src/query/trace.js +345 -0
  274. package/src/register-canary/fixture-loader.js +87 -0
  275. package/src/register-canary/index.js +10 -0
  276. package/src/register-canary/record-writer.js +132 -0
  277. package/src/register-canary/runner.js +156 -0
  278. package/src/req-baseline/gate.js +86 -0
  279. package/src/req-baseline/index.js +27 -0
  280. package/src/req-baseline/open-candidates.js +143 -0
  281. package/src/req-baseline/opt-out.js +195 -0
  282. package/src/req-baseline/sweep.js +230 -0
  283. package/src/req-detection/classifier.js +181 -0
  284. package/src/req-detection/index.js +9 -0
  285. package/src/req-detection/persist.js +55 -0
  286. package/src/review/index.js +325 -0
  287. package/src/review/mutation.js +117 -0
  288. package/src/review/ui-baseline-drift.js +138 -0
  289. package/src/server/index.js +178 -0
  290. package/src/server/routes.js +110 -0
  291. package/src/server/sse.js +118 -0
  292. package/src/setup/agent-setup.js +362 -0
  293. package/src/setup/identity-seed.js +104 -0
  294. package/src/setup/knowledge-seed.js +123 -0
  295. package/src/setup/managed-block.js +193 -0
  296. package/src/setup/managed-gitignore.js +166 -0
  297. package/src/setup/managed-markers.js +49 -0
  298. package/src/ui-baseline/defaults.js +119 -0
  299. package/src/ui-baseline/index.js +25 -0
  300. package/src/ui-baseline/manifest-writer.js +282 -0
  301. package/src/ui-baseline/session.js +178 -0
  302. package/src/ui-detection/classifier.js +192 -0
  303. package/src/verify/chain/index.js +190 -0
  304. package/src/verify/cli/cleanup.js +61 -0
  305. package/src/verify/cli/help.js +56 -0
  306. package/src/verify/cli/mcp.js +98 -0
  307. package/src/verify/cli/provision.js +71 -0
  308. package/src/verify/cli/report.js +71 -0
  309. package/src/verify/cli/run.js +155 -0
  310. package/src/verify/engine/brief.js +87 -0
  311. package/src/verify/engine/index.js +177 -0
  312. package/src/verify/engine/launcher.js +307 -0
  313. package/src/verify/mcp/tools.js +107 -0
  314. package/src/verify/profile/index.js +146 -0
  315. package/src/verify/provision/index.js +256 -0
  316. package/src/verify/report/index.js +139 -0
  317. package/src/verify/report/renderer.js +118 -0
  318. package/src/verify/verdict/index.js +246 -0
  319. package/src/view/doc-renderers/adr.js +44 -0
  320. package/src/view/doc-renderers/build-sequence.js +40 -0
  321. package/src/view/doc-renderers/fbs.js +128 -0
  322. package/src/view/doc-renderers/helpers.js +159 -0
  323. package/src/view/doc-renderers/index.js +12 -0
  324. package/src/view/doc-renderers/prd.js +45 -0
  325. package/src/view/doc-renderers/req.js +43 -0
  326. package/src/view/doc-renderers/tac.js +38 -0
  327. package/src/view/doc-renderers/tad.js +74 -0
  328. package/src/view/doc-renderers/test-suite.js +45 -0
  329. package/src/view/doc-renderers/user-story.js +63 -0
  330. package/src/view/html-page.js +462 -0
  331. package/src/view/index.js +63 -0
  332. package/src/view/live-client.js +338 -0
  333. package/src/view/mermaid-diagram.js +178 -0
  334. package/src/view/style.css +735 -0
  335. package/src/view/tree-model.js +152 -0
  336. package/src/view/vendored/mermaid.min.js +2607 -0
  337. package/src/view-supervisor/index.js +26 -0
  338. package/src/view-supervisor/logs.js +32 -0
  339. package/src/view-supervisor/manifest-writer.js +178 -0
  340. package/src/view-supervisor/persist-until.js +85 -0
  341. package/src/view-supervisor/supervisor.js +276 -0
  342. package/src/watch/index.js +152 -0
@@ -0,0 +1,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';