@isaacriehm/cairn-core 0.6.0 → 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 (431) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/align-undo/undo.js +2 -2
  3. package/dist/align-undo/undo.js.map +1 -1
  4. package/dist/attention/bulk-accept.js +104 -55
  5. package/dist/attention/bulk-accept.js.map +1 -1
  6. package/dist/attention/dedup.js +23 -10
  7. package/dist/attention/dedup.js.map +1 -1
  8. package/dist/attention/restore.js +2 -2
  9. package/dist/attention/restore.js.map +1 -1
  10. package/dist/attention/scoring.js +1 -1
  11. package/dist/attention/scoring.js.map +1 -1
  12. package/dist/attention/serve/api.d.ts +1 -0
  13. package/dist/attention/serve/api.js +130 -77
  14. package/dist/attention/serve/api.js.map +1 -1
  15. package/dist/attention/serve/index.d.ts +1 -0
  16. package/dist/attention/serve/index.js +21 -7
  17. package/dist/attention/serve/index.js.map +1 -1
  18. package/dist/attention/source-strip.js +41 -46
  19. package/dist/attention/source-strip.js.map +1 -1
  20. package/dist/claude/cache.d.ts +10 -19
  21. package/dist/claude/cache.js +0 -0
  22. package/dist/claude/cache.js.map +1 -1
  23. package/dist/claude/runner.d.ts +4 -6
  24. package/dist/claude/runner.js +242 -203
  25. package/dist/claude/runner.js.map +1 -1
  26. package/dist/claude/types.d.ts +5 -5
  27. package/dist/context/handoff-builder.d.ts +28 -4
  28. package/dist/context/handoff-builder.js +75 -157
  29. package/dist/context/handoff-builder.js.map +1 -1
  30. package/dist/context/spec-delta.js +61 -9
  31. package/dist/context/spec-delta.js.map +1 -1
  32. package/dist/context/task-summary.js +5 -1
  33. package/dist/context/task-summary.js.map +1 -1
  34. package/dist/decision-capture/id.js +1 -1
  35. package/dist/decision-capture/id.js.map +1 -1
  36. package/dist/doctor/index.d.ts +9 -11
  37. package/dist/doctor/index.js +118 -175
  38. package/dist/doctor/index.js.map +1 -1
  39. package/dist/drain/drain.d.ts +1 -1
  40. package/dist/drain/drain.js +16 -16
  41. package/dist/drain/drain.js.map +1 -1
  42. package/dist/drain/index.d.ts +1 -1
  43. package/dist/drain/index.js +1 -1
  44. package/dist/events/reader.d.ts +9 -10
  45. package/dist/events/reader.js +46 -57
  46. package/dist/events/reader.js.map +1 -1
  47. package/dist/events/writer.js +1 -1
  48. package/dist/events/writer.js.map +1 -1
  49. package/dist/fix-align/index.d.ts +1 -1
  50. package/dist/fix-align/index.js +1 -1
  51. package/dist/fix-align/run.d.ts +1 -1
  52. package/dist/fix-align/run.js +4 -3
  53. package/dist/fix-align/run.js.map +1 -1
  54. package/dist/fix-align/sentinel.d.ts +1 -2
  55. package/dist/fix-align/sentinel.js +3 -4
  56. package/dist/fix-align/sentinel.js.map +1 -1
  57. package/dist/gc/apply.js +1 -1
  58. package/dist/gc/apply.js.map +1 -1
  59. package/dist/gc/attested-commits.d.ts +19 -0
  60. package/dist/gc/attested-commits.js +48 -0
  61. package/dist/gc/attested-commits.js.map +1 -0
  62. package/dist/gc/canary.d.ts +11 -21
  63. package/dist/gc/canary.js +18 -24
  64. package/dist/gc/canary.js.map +1 -1
  65. package/dist/gc/citation-integrity.d.ts +11 -14
  66. package/dist/gc/citation-integrity.js +124 -125
  67. package/dist/gc/citation-integrity.js.map +1 -1
  68. package/dist/gc/classify.d.ts +1 -1
  69. package/dist/gc/classify.js +2 -2
  70. package/dist/gc/classify.js.map +1 -1
  71. package/dist/gc/completion-integrity.d.ts +6 -7
  72. package/dist/gc/completion-integrity.js +45 -53
  73. package/dist/gc/completion-integrity.js.map +1 -1
  74. package/dist/gc/doc-gardening.js +1 -9
  75. package/dist/gc/doc-gardening.js.map +1 -1
  76. package/dist/gc/frontmatter.js +2 -2
  77. package/dist/gc/frontmatter.js.map +1 -1
  78. package/dist/gc/index.d.ts +7 -6
  79. package/dist/gc/index.js +3 -2
  80. package/dist/gc/index.js.map +1 -1
  81. package/dist/gc/quality-update.d.ts +6 -10
  82. package/dist/gc/quality-update.js +12 -15
  83. package/dist/gc/quality-update.js.map +1 -1
  84. package/dist/gc/scope-coverage.js +1 -1
  85. package/dist/gc/scope-coverage.js.map +1 -1
  86. package/dist/gc/stub-hits.d.ts +7 -25
  87. package/dist/gc/stub-hits.js +14 -40
  88. package/dist/gc/stub-hits.js.map +1 -1
  89. package/dist/gc/sweep.js +15 -6
  90. package/dist/gc/sweep.js.map +1 -1
  91. package/dist/gc/types.d.ts +5 -5
  92. package/dist/gc/types.js +2 -2
  93. package/dist/gc/walk-source.js +8 -28
  94. package/dist/gc/walk-source.js.map +1 -1
  95. package/dist/hooks/defer.d.ts +9 -20
  96. package/dist/hooks/defer.js +10 -27
  97. package/dist/hooks/defer.js.map +1 -1
  98. package/dist/hooks/post-tool-use/allowlist-reader.d.ts +2 -1
  99. package/dist/hooks/post-tool-use/allowlist-reader.js +49 -34
  100. package/dist/hooks/post-tool-use/allowlist-reader.js.map +1 -1
  101. package/dist/hooks/post-tool-use/index.d.ts +3 -4
  102. package/dist/hooks/post-tool-use/index.js +3 -3
  103. package/dist/hooks/post-tool-use/index.js.map +1 -1
  104. package/dist/hooks/post-tool-use/legend-builder.d.ts +11 -2
  105. package/dist/hooks/post-tool-use/legend-builder.js +27 -2
  106. package/dist/hooks/post-tool-use/legend-builder.js.map +1 -1
  107. package/dist/hooks/post-tool-use/post-write.d.ts +8 -0
  108. package/dist/hooks/post-tool-use/post-write.js +112 -0
  109. package/dist/hooks/post-tool-use/post-write.js.map +1 -0
  110. package/dist/hooks/post-tool-use/read-enricher.d.ts +10 -5
  111. package/dist/hooks/post-tool-use/read-enricher.js +114 -149
  112. package/dist/hooks/post-tool-use/read-enricher.js.map +1 -1
  113. package/dist/hooks/post-tool-use/sot-align.d.ts +12 -0
  114. package/dist/hooks/post-tool-use/sot-align.js +73 -49
  115. package/dist/hooks/post-tool-use/sot-align.js.map +1 -1
  116. package/dist/hooks/post-tool-use/write-guardian.d.ts +40 -8
  117. package/dist/hooks/post-tool-use/write-guardian.js +202 -254
  118. package/dist/hooks/post-tool-use/write-guardian.js.map +1 -1
  119. package/dist/hooks/pre-commit/sot-align-precommit.js +2 -2
  120. package/dist/hooks/pre-commit/sot-align-precommit.js.map +1 -1
  121. package/dist/hooks/runners/index.d.ts +2 -2
  122. package/dist/hooks/runners/index.js +1 -1
  123. package/dist/hooks/runners/index.js.map +1 -1
  124. package/dist/hooks/runners/payload.d.ts +34 -19
  125. package/dist/hooks/runners/payload.js +47 -44
  126. package/dist/hooks/runners/payload.js.map +1 -1
  127. package/dist/hooks/runners/session-end.js +12 -11
  128. package/dist/hooks/runners/session-end.js.map +1 -1
  129. package/dist/hooks/runners/session-start.d.ts +4 -2
  130. package/dist/hooks/runners/session-start.js +88 -261
  131. package/dist/hooks/runners/session-start.js.map +1 -1
  132. package/dist/hooks/runners/stop.js +27 -8
  133. package/dist/hooks/runners/stop.js.map +1 -1
  134. package/dist/hooks/runners/user-prompt-submit.js +7 -1
  135. package/dist/hooks/runners/user-prompt-submit.js.map +1 -1
  136. package/dist/hooks/sot-align-common.d.ts +1 -2
  137. package/dist/hooks/sot-align-common.js +2 -2
  138. package/dist/hooks/sot-align-common.js.map +1 -1
  139. package/dist/index.d.ts +1 -1
  140. package/dist/index.js +12 -3
  141. package/dist/index.js.map +1 -1
  142. package/dist/init/baseline-audit.d.ts +14 -58
  143. package/dist/init/baseline-audit.js +92 -320
  144. package/dist/init/baseline-audit.js.map +1 -1
  145. package/dist/init/brand-derive.d.ts +11 -15
  146. package/dist/init/brand-derive.js +21 -46
  147. package/dist/init/brand-derive.js.map +1 -1
  148. package/dist/init/brand-setup.d.ts +1 -1
  149. package/dist/init/brand-setup.js +1 -1
  150. package/dist/init/detect.d.ts +25 -6
  151. package/dist/init/detect.js +107 -136
  152. package/dist/init/detect.js.map +1 -1
  153. package/dist/init/index.d.ts +4 -4
  154. package/dist/init/index.js +2 -2
  155. package/dist/init/index.js.map +1 -1
  156. package/dist/init/ingest-docs.d.ts +83 -23
  157. package/dist/init/ingest-docs.js +648 -136
  158. package/dist/init/ingest-docs.js.map +1 -1
  159. package/dist/init/init.d.ts +29 -20
  160. package/dist/init/init.js +132 -290
  161. package/dist/init/init.js.map +1 -1
  162. package/dist/init/mapper-merge.js +1 -1
  163. package/dist/init/mapper-merge.js.map +1 -1
  164. package/dist/init/mapper-parallel.d.ts +1 -1
  165. package/dist/init/mapper-parallel.js +10 -1
  166. package/dist/init/mapper-parallel.js.map +1 -1
  167. package/dist/init/mapper.js +1 -1
  168. package/dist/init/mapper.js.map +1 -1
  169. package/dist/init/multi-dev/install.d.ts +5 -5
  170. package/dist/init/multi-dev/install.js +4 -4
  171. package/dist/init/overlay.js +1 -0
  172. package/dist/init/overlay.js.map +1 -1
  173. package/dist/init/phases/1-detect.js +1 -1
  174. package/dist/init/phases/1-detect.js.map +1 -1
  175. package/dist/init/phases/{7c-rules-merge.d.ts → 10-rules-merge.d.ts} +2 -2
  176. package/dist/init/phases/{7c-rules-merge.js → 10-rules-merge.js} +6 -12
  177. package/dist/init/phases/10-rules-merge.js.map +1 -0
  178. package/dist/init/phases/11-baseline.d.ts +7 -0
  179. package/dist/init/phases/{8-baseline.js → 11-baseline.js} +5 -9
  180. package/dist/init/phases/11-baseline.js.map +1 -0
  181. package/dist/init/phases/12-strip.d.ts +10 -0
  182. package/dist/init/phases/{10-strip.js → 12-strip.js} +10 -14
  183. package/dist/init/phases/12-strip.js.map +1 -0
  184. package/dist/init/phases/{12-multidev.d.ts → 13-multidev.d.ts} +2 -2
  185. package/dist/init/phases/{12-multidev.js → 13-multidev.js} +4 -4
  186. package/dist/init/phases/{12-multidev.js.map → 13-multidev.js.map} +1 -1
  187. package/dist/init/phases/3-mapper.d.ts +1 -1
  188. package/dist/init/phases/3-mapper.js +2 -2
  189. package/dist/init/phases/3-mapper.js.map +1 -1
  190. package/dist/init/phases/{3b-seed.d.ts → 4-seed.d.ts} +2 -2
  191. package/dist/init/phases/{3b-seed.js → 4-seed.js} +7 -7
  192. package/dist/init/phases/4-seed.js.map +1 -0
  193. package/dist/init/phases/{4-pilot.d.ts → 5-pilot.d.ts} +2 -2
  194. package/dist/init/phases/{4-pilot.js → 5-pilot.js} +9 -9
  195. package/dist/init/phases/{4-pilot.js.map → 5-pilot.js.map} +1 -1
  196. package/dist/init/phases/{5-brand.d.ts → 6-brand.d.ts} +2 -7
  197. package/dist/init/phases/{5-brand.js → 6-brand.js} +6 -14
  198. package/dist/init/phases/6-brand.js.map +1 -0
  199. package/dist/init/phases/{5b-topic-index.d.ts → 7-topic-index.d.ts} +2 -11
  200. package/dist/init/phases/{5b-topic-index.js → 7-topic-index.js} +9 -18
  201. package/dist/init/phases/7-topic-index.js.map +1 -0
  202. package/dist/init/phases/8-docs-ingest.d.ts +10 -0
  203. package/dist/init/phases/{6-docs-ingest.js → 8-docs-ingest.js} +12 -14
  204. package/dist/init/phases/8-docs-ingest.js.map +1 -0
  205. package/dist/init/phases/9-source-comments.d.ts +6 -0
  206. package/dist/init/phases/{7b-source-comments.js → 9-source-comments.js} +7 -26
  207. package/dist/init/phases/9-source-comments.js.map +1 -0
  208. package/dist/init/phases/index.d.ts +12 -12
  209. package/dist/init/phases/index.js +11 -11
  210. package/dist/init/phases/index.js.map +1 -1
  211. package/dist/init/phases/{parallel-678.d.ts → parallel-8910.d.ts} +7 -8
  212. package/dist/init/phases/{parallel-678.js → parallel-8910.js} +34 -46
  213. package/dist/init/phases/parallel-8910.js.map +1 -0
  214. package/dist/init/phases/source-comments-output-io.d.ts +3 -3
  215. package/dist/init/phases/source-comments-output-io.js +3 -3
  216. package/dist/init/phases/types.d.ts +1 -1
  217. package/dist/init/phases/types.js +10 -10
  218. package/dist/init/phases/types.js.map +1 -1
  219. package/dist/init/rules-merge/discover.d.ts +2 -2
  220. package/dist/init/rules-merge/discover.js +28 -43
  221. package/dist/init/rules-merge/discover.js.map +1 -1
  222. package/dist/init/rules-merge/ingest.d.ts +6 -6
  223. package/dist/init/rules-merge/ingest.js +9 -10
  224. package/dist/init/rules-merge/ingest.js.map +1 -1
  225. package/dist/init/rules-merge/keep-markers.d.ts +1 -1
  226. package/dist/init/rules-merge/keep-markers.js +1 -1
  227. package/dist/init/rules-merge/regenerate.d.ts +3 -3
  228. package/dist/init/rules-merge/regenerate.js +3 -3
  229. package/dist/init/seed.js +17 -24
  230. package/dist/init/seed.js.map +1 -1
  231. package/dist/init/sot-emit.d.ts +48 -8
  232. package/dist/init/sot-emit.js +88 -10
  233. package/dist/init/sot-emit.js.map +1 -1
  234. package/dist/init/source-comments/classify.d.ts +2 -2
  235. package/dist/init/source-comments/classify.js +9 -1
  236. package/dist/init/source-comments/classify.js.map +1 -1
  237. package/dist/init/source-comments/index.d.ts +1 -1
  238. package/dist/init/source-comments/index.js.map +1 -1
  239. package/dist/init/source-comments/ingest.d.ts +2 -2
  240. package/dist/init/source-comments/ingest.js +110 -12
  241. package/dist/init/source-comments/ingest.js.map +1 -1
  242. package/dist/init/source-comments/strip-replace.js +3 -2
  243. package/dist/init/source-comments/strip-replace.js.map +1 -1
  244. package/dist/init/source-comments/walker.d.ts +3 -26
  245. package/dist/init/source-comments/walker.js +18 -30
  246. package/dist/init/source-comments/walker.js.map +1 -1
  247. package/dist/init/topic-index/index.d.ts +15 -1
  248. package/dist/init/topic-index/index.js +84 -5
  249. package/dist/init/topic-index/index.js.map +1 -1
  250. package/dist/init/topic-index/judge.d.ts +5 -9
  251. package/dist/init/topic-index/judge.js +25 -17
  252. package/dist/init/topic-index/judge.js.map +1 -1
  253. package/dist/init/topic-index/resolve.d.ts +23 -3
  254. package/dist/init/topic-index/resolve.js +102 -16
  255. package/dist/init/topic-index/resolve.js.map +1 -1
  256. package/dist/init/topic-index/walk.d.ts +36 -4
  257. package/dist/init/topic-index/walk.js +78 -12
  258. package/dist/init/topic-index/walk.js.map +1 -1
  259. package/dist/lock.js +4 -1
  260. package/dist/lock.js.map +1 -1
  261. package/dist/logger.js +3 -0
  262. package/dist/logger.js.map +1 -1
  263. package/dist/mcp/history/summarizer.js +6 -1
  264. package/dist/mcp/history/summarizer.js.map +1 -1
  265. package/dist/mcp/history/walker.js +1 -1
  266. package/dist/mcp/history/walker.js.map +1 -1
  267. package/dist/mcp/path-allowlist.js +1 -1
  268. package/dist/mcp/path-allowlist.js.map +1 -1
  269. package/dist/mcp/schemas.d.ts +47 -21
  270. package/dist/mcp/schemas.js +38 -9
  271. package/dist/mcp/schemas.js.map +1 -1
  272. package/dist/mcp/server.js +10 -0
  273. package/dist/mcp/server.js.map +1 -1
  274. package/dist/mcp/tools/archive.js +1 -1
  275. package/dist/mcp/tools/archive.js.map +1 -1
  276. package/dist/mcp/tools/attention-wait.js +12 -2
  277. package/dist/mcp/tools/attention-wait.js.map +1 -1
  278. package/dist/mcp/tools/bulk-accept-attention.js +29 -30
  279. package/dist/mcp/tools/bulk-accept-attention.js.map +1 -1
  280. package/dist/mcp/tools/canonical-for-topic.js +37 -15
  281. package/dist/mcp/tools/canonical-for-topic.js.map +1 -1
  282. package/dist/mcp/tools/decision-get.js +2 -2
  283. package/dist/mcp/tools/decision-get.js.map +1 -1
  284. package/dist/mcp/tools/decisions-for-symbol.js +2 -2
  285. package/dist/mcp/tools/decisions-for-symbol.js.map +1 -1
  286. package/dist/mcp/tools/get-full.js +1 -1
  287. package/dist/mcp/tools/get-full.js.map +1 -1
  288. package/dist/mcp/tools/ground-get.js +1 -1
  289. package/dist/mcp/tools/ground-get.js.map +1 -1
  290. package/dist/mcp/tools/in-scope.d.ts +8 -0
  291. package/dist/mcp/tools/in-scope.js +125 -0
  292. package/dist/mcp/tools/in-scope.js.map +1 -0
  293. package/dist/mcp/tools/index.js +11 -6
  294. package/dist/mcp/tools/index.js.map +1 -1
  295. package/dist/mcp/tools/init-phases.d.ts +3 -2
  296. package/dist/mcp/tools/init-phases.js +125 -124
  297. package/dist/mcp/tools/init-phases.js.map +1 -1
  298. package/dist/mcp/tools/invariant-get.js +2 -2
  299. package/dist/mcp/tools/invariant-get.js.map +1 -1
  300. package/dist/mcp/tools/record-decision.d.ts +4 -3
  301. package/dist/mcp/tools/record-decision.js +111 -36
  302. package/dist/mcp/tools/record-decision.js.map +1 -1
  303. package/dist/mcp/tools/reject-candidate.d.ts +24 -0
  304. package/dist/mcp/tools/reject-candidate.js +71 -0
  305. package/dist/mcp/tools/reject-candidate.js.map +1 -0
  306. package/dist/mcp/tools/resolve-attention.d.ts +5 -18
  307. package/dist/mcp/tools/resolve-attention.js +58 -158
  308. package/dist/mcp/tools/resolve-attention.js.map +1 -1
  309. package/dist/mcp/tools/search-candidates.d.ts +20 -0
  310. package/dist/mcp/tools/search-candidates.js +93 -0
  311. package/dist/mcp/tools/search-candidates.js.map +1 -0
  312. package/dist/mcp/tools/search.js +5 -4
  313. package/dist/mcp/tools/search.js.map +1 -1
  314. package/dist/mcp/tools/supersedes-chain.js +2 -2
  315. package/dist/mcp/tools/supersedes-chain.js.map +1 -1
  316. package/dist/mcp/tools/task-create.d.ts +1 -0
  317. package/dist/mcp/tools/task-create.js +2 -0
  318. package/dist/mcp/tools/task-create.js.map +1 -1
  319. package/dist/mcp/tools/timeline.js +1 -1
  320. package/dist/mcp/tools/timeline.js.map +1 -1
  321. package/dist/mcp/tools/types.d.ts +1 -0
  322. package/dist/mcp/tools/types.js +9 -1
  323. package/dist/mcp/tools/types.js.map +1 -1
  324. package/dist/sensors/attestation.d.ts +1 -1
  325. package/dist/sensors/attestation.js +2 -2
  326. package/dist/sensors/attestation.js.map +1 -1
  327. package/dist/sensors/catalog.d.ts +26 -28
  328. package/dist/sensors/catalog.js +79 -81
  329. package/dist/sensors/catalog.js.map +1 -1
  330. package/dist/sensors/decisions.d.ts +1 -1
  331. package/dist/sensors/decisions.js +15 -14
  332. package/dist/sensors/decisions.js.map +1 -1
  333. package/dist/sensors/diff.js +46 -22
  334. package/dist/sensors/diff.js.map +1 -1
  335. package/dist/sensors/runner.d.ts +2 -2
  336. package/dist/sensors/runner.js +3 -3
  337. package/dist/sensors/shell.d.ts +4 -0
  338. package/dist/sensors/shell.js +16 -0
  339. package/dist/sensors/shell.js.map +1 -0
  340. package/dist/sensors/structural.d.ts +3 -2
  341. package/dist/sensors/structural.js +3 -11
  342. package/dist/sensors/structural.js.map +1 -1
  343. package/dist/sensors/stub-catalog.d.ts +2 -1
  344. package/dist/sensors/stub-catalog.js +2 -9
  345. package/dist/sensors/stub-catalog.js.map +1 -1
  346. package/dist/sensors/types.d.ts +3 -4
  347. package/dist/sensors/types.js +1 -2
  348. package/dist/sensors/types.js.map +1 -1
  349. package/dist/session-start/build.js +1 -1
  350. package/dist/session-start/build.js.map +1 -1
  351. package/dist/status-line/event-queue.js +1 -1
  352. package/dist/status-line/index.d.ts +1 -1
  353. package/package.json +3 -2
  354. package/templates/.cairn/config/sensors.yaml +8 -8
  355. package/templates/attention-ui/app.js +101 -39
  356. package/dist/fs.d.ts +0 -5
  357. package/dist/fs.js +0 -11
  358. package/dist/fs.js.map +0 -1
  359. package/dist/ground/alignment-pending.d.ts +0 -28
  360. package/dist/ground/alignment-pending.js +0 -83
  361. package/dist/ground/alignment-pending.js.map +0 -1
  362. package/dist/ground/anchor-map.d.ts +0 -14
  363. package/dist/ground/anchor-map.js +0 -56
  364. package/dist/ground/anchor-map.js.map +0 -1
  365. package/dist/ground/drift.d.ts +0 -8
  366. package/dist/ground/drift.js +0 -23
  367. package/dist/ground/drift.js.map +0 -1
  368. package/dist/ground/frontmatter.d.ts +0 -32
  369. package/dist/ground/frontmatter.js +0 -77
  370. package/dist/ground/frontmatter.js.map +0 -1
  371. package/dist/ground/glob.d.ts +0 -10
  372. package/dist/ground/glob.js +0 -46
  373. package/dist/ground/glob.js.map +0 -1
  374. package/dist/ground/index.d.ts +0 -23
  375. package/dist/ground/index.js +0 -17
  376. package/dist/ground/index.js.map +0 -1
  377. package/dist/ground/ledgers.d.ts +0 -14
  378. package/dist/ground/ledgers.js +0 -105
  379. package/dist/ground/ledgers.js.map +0 -1
  380. package/dist/ground/manifest.d.ts +0 -10
  381. package/dist/ground/manifest.js +0 -84
  382. package/dist/ground/manifest.js.map +0 -1
  383. package/dist/ground/paths.d.ts +0 -41
  384. package/dist/ground/paths.js +0 -103
  385. package/dist/ground/paths.js.map +0 -1
  386. package/dist/ground/quality-grades.d.ts +0 -11
  387. package/dist/ground/quality-grades.js +0 -100
  388. package/dist/ground/quality-grades.js.map +0 -1
  389. package/dist/ground/schemas.d.ts +0 -502
  390. package/dist/ground/schemas.js +0 -318
  391. package/dist/ground/schemas.js.map +0 -1
  392. package/dist/ground/scope-index.d.ts +0 -96
  393. package/dist/ground/scope-index.js +0 -290
  394. package/dist/ground/scope-index.js.map +0 -1
  395. package/dist/ground/slug.d.ts +0 -60
  396. package/dist/ground/slug.js +0 -103
  397. package/dist/ground/slug.js.map +0 -1
  398. package/dist/ground/sot-bindings.d.ts +0 -14
  399. package/dist/ground/sot-bindings.js +0 -79
  400. package/dist/ground/sot-bindings.js.map +0 -1
  401. package/dist/ground/sot-cache.d.ts +0 -18
  402. package/dist/ground/sot-cache.js +0 -62
  403. package/dist/ground/sot-cache.js.map +0 -1
  404. package/dist/ground/topic-index.d.ts +0 -27
  405. package/dist/ground/topic-index.js +0 -82
  406. package/dist/ground/topic-index.js.map +0 -1
  407. package/dist/ground/walk.d.ts +0 -7
  408. package/dist/ground/walk.js +0 -53
  409. package/dist/ground/walk.js.map +0 -1
  410. package/dist/hooks/post-tool-use/ledger-cache.d.ts +0 -40
  411. package/dist/hooks/post-tool-use/ledger-cache.js +0 -290
  412. package/dist/hooks/post-tool-use/ledger-cache.js.map +0 -1
  413. package/dist/init/phases/10-strip.d.ts +0 -11
  414. package/dist/init/phases/10-strip.js.map +0 -1
  415. package/dist/init/phases/3b-seed.js.map +0 -1
  416. package/dist/init/phases/5-brand.js.map +0 -1
  417. package/dist/init/phases/5b-topic-index.js.map +0 -1
  418. package/dist/init/phases/6-docs-ingest.d.ts +0 -10
  419. package/dist/init/phases/6-docs-ingest.js.map +0 -1
  420. package/dist/init/phases/7b-source-comments.d.ts +0 -15
  421. package/dist/init/phases/7b-source-comments.js.map +0 -1
  422. package/dist/init/phases/7c-rules-merge.js.map +0 -1
  423. package/dist/init/phases/8-baseline.d.ts +0 -10
  424. package/dist/init/phases/8-baseline.js.map +0 -1
  425. package/dist/init/phases/parallel-678.js.map +0 -1
  426. package/dist/mcp/tools/decisions-in-scope.d.ts +0 -7
  427. package/dist/mcp/tools/decisions-in-scope.js +0 -66
  428. package/dist/mcp/tools/decisions-in-scope.js.map +0 -1
  429. package/dist/mcp/tools/invariants-in-scope.d.ts +0 -7
  430. package/dist/mcp/tools/invariants-in-scope.js +0 -81
  431. package/dist/mcp/tools/invariants-in-scope.js.map +0 -1
@@ -1,28 +1,78 @@
1
1
  /**
2
- * Phase 6 — docs ingestion (v0.5.0 SoT model).
2
+ * Phase 8staged docs ingestion.
3
3
  *
4
- * Reads the topic-index built by phase 5b, filters to entries whose SoT
5
- * source lives under `docs/*`, and emits verbatim DEC files under
6
- * `.cairn/ground/decisions/`. Auto-promoted to `status: accepted`. No
7
- * draft inbox, no LLM paraphrase — the doc paragraph itself IS the
8
- * canonical body, recorded with `sot_kind: path` so the lens renders
9
- * the live source on every read.
4
+ * Replaces the v0.6 bulk-classifier path. Cuts wall from ~15 min
5
+ * ~75 s on gcb-platform-scale repos AND collapses the noisy ledger
6
+ * (7000 DECs) to a curated draft inbox (30-80 drafts).
10
7
  *
11
- * Per-entry Haiku call decides `kind` only (decision / domain-rule /
12
- * voice-guidelines / api-docs / other). The first two emit a DEC; the
13
- * rest are skipped at this layer (voice + canonical-topic flows are
14
- * handled by other tooling now — they were file-level concerns under
15
- * the v0.4.x model and have no clean paragraph-level analogue).
8
+ * Pipeline:
9
+ *
10
+ * Stage 3 (deterministic, 0 Haiku) marker scan
11
+ * Topic-index entries with `marker_kind` in {"decision","rule"} go
12
+ * straight to emit. The walker stamped them at parse time when it
13
+ * saw frontmatter `cairn.kind` or `<!-- cairn:decision -->` /
14
+ * `<!-- cairn:rule -->` within 3 lines of the heading.
15
+ *
16
+ * Stage 1 — file-purpose binary filter (batch=30, concurrency=5)
17
+ * Per file: filepath + frontmatter + first 800 chars + every
18
+ * H1/H2/H3 line (capped at 100). Locked rigid prompt: a file is
19
+ * authoritative ONLY if it's a canonical rulebook, formal ADR,
20
+ * or list of binding domain invariants. Plans / scratchpads /
21
+ * UAT logs / API docs are NOT authoritative even if they
22
+ * contain proposed or historical decisions.
23
+ *
24
+ * Stage 2 — section-level batch classifier (batch=30, concurrency=5)
25
+ * Same shape as the v0.6 classifier, but scoped to sections
26
+ * belonging to Stage-1-authoritative files AND not already
27
+ * handled by a marker. This is where Haiku still adds signal —
28
+ * the file passed the rigid filter; now decide WHICH sections
29
+ * of it are decisions vs context.
30
+ *
31
+ * Stage 4 — emit
32
+ * Stage 2 + Stage 3 outputs → `.cairn/ground/decisions/_inbox/<id>.draft.md`.
33
+ * `status: draft`, `capture_source: init-docs-ingest`,
34
+ * `decided_by: cairn-init`. Body is verbatim via
35
+ * `readSotBody` — no Haiku paraphrasing. Operator triages via
36
+ * the existing `cairn-attention` skill.
37
+ *
38
+ * Skipped entries (everything else) stay in the topic-index as
39
+ * unpromoted candidates. The PR 2 `cairn_search_candidates` /
40
+ * `cairn_propose_decision` MCP tools surface them to AI agents as
41
+ * the project lives.
16
42
  */
17
- import { existsSync, readdirSync, statSync, } from "node:fs";
18
- import { join, relative } from "node:path";
43
+ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync, } from "node:fs";
44
+ import { join } from "node:path";
45
+ import { stringify as stringifyYaml } from "yaml";
19
46
  import { runClaude } from "../claude/index.js";
20
- import { readAnchorMap, readTopicIndex, writeSotBindings, writeSotCache, writeTopicIndex, } from "../ground/index.js";
47
+ import { bodyContentHash, decisionsDir, deriveDecId, readAnchorMap, readRejectedYaml, readTopicIndex, setTopic, walkFs, writeFileCandidatesMap, writeTopicIndex, } from "@isaacriehm/cairn-state";
21
48
  import { logger } from "../logger.js";
22
- import { emitFromTopicIndex } from "./sot-emit.js";
49
+ import { firstLineFallback, readSotBody } from "./sot-emit.js";
23
50
  const log = logger("init.ingest-docs");
24
- const PER_DOC_TIMEOUT_MS = 60_000;
25
- const DOC_BODY_CAP = 8_000;
51
+ /* -------------------------------------------------------------------------- */
52
+ /* Tunables locked tunables */
53
+ /* -------------------------------------------------------------------------- */
54
+ /** N files per Stage-1 Haiku call. */
55
+ const FILE_FILTER_BATCH_SIZE = 30;
56
+ /** Concurrent Stage-1 batches. */
57
+ const FILE_FILTER_CONCURRENCY = 5;
58
+ /** Stage 1 per-file context — first chars of body, frontmatter stripped. */
59
+ const FILE_FILTER_INTRO_CHARS = 800;
60
+ /** Stage 1 max ToC lines (H1/H2/H3 only). */
61
+ const FILE_FILTER_TOC_MAX_LINES = 100;
62
+ /** Stage 1 wall budget per Haiku call. */
63
+ const FILE_FILTER_TIMEOUT_MS = 60_000;
64
+ /** N sections per Stage-2 Haiku call. */
65
+ const SECTION_BATCH_SIZE = 30;
66
+ /** Concurrent Stage-2 batches. */
67
+ const SECTION_CONCURRENCY = 5;
68
+ /** Stage 2 per-section body cap (chars) before truncation marker. */
69
+ const SECTION_BODY_CAP = 2_000;
70
+ /** Stage 2 wall budget per Haiku call. */
71
+ const SECTION_TIMEOUT_MS = 120_000;
72
+ /** Capture source stamped on every Stage 2/3 emit. */
73
+ const CAPTURE_SOURCE = "init-docs-ingest";
74
+ /** Decided-by stamp on every Stage 2/3 emit. */
75
+ const DECIDED_BY = "cairn-init";
26
76
  /** Subdirs we never descend into when discovering candidate doc files. */
27
77
  const SKIP_DIRS = new Set([
28
78
  ".cairn",
@@ -45,178 +95,640 @@ export function discoverDocs(repoRoot) {
45
95
  if (!existsSync(docsDir))
46
96
  return [];
47
97
  const out = [];
48
- walkDocsDir(docsDir, repoRoot, out);
98
+ walkFs({
99
+ dir: docsDir,
100
+ repoRoot,
101
+ skipDirs: SKIP_DIRS,
102
+ onFile: (rel, abs, ent) => {
103
+ if (!ent.name.endsWith(".md"))
104
+ return;
105
+ let st;
106
+ try {
107
+ st = statSync(abs);
108
+ }
109
+ catch {
110
+ return;
111
+ }
112
+ out.push({
113
+ path: rel,
114
+ size: st.size,
115
+ group: dirGroup(rel),
116
+ });
117
+ },
118
+ });
49
119
  return out;
50
120
  }
51
- function walkDocsDir(dir, repoRoot, out) {
52
- let entries;
53
- try {
54
- entries = readdirSync(dir, { withFileTypes: true, encoding: "utf8" });
55
- }
56
- catch {
57
- return;
58
- }
59
- for (const ent of entries) {
60
- if (SKIP_DIRS.has(ent.name))
61
- continue;
62
- const abs = join(dir, ent.name);
63
- if (ent.isDirectory()) {
64
- walkDocsDir(abs, repoRoot, out);
65
- continue;
66
- }
67
- if (!ent.isFile() || !ent.name.endsWith(".md"))
121
+ function dirGroup(rel) {
122
+ const parts = rel.split("/");
123
+ if (parts.length <= 1)
124
+ return "(root)";
125
+ return `${parts[0]}/`;
126
+ }
127
+ /* -------------------------------------------------------------------------- */
128
+ /* Stage 1 — file-purpose binary filter */
129
+ /* */
130
+ /* Locked rigid prompt — DO NOT paraphrase. A file is authoritative ONLY */
131
+ /* if it's a canonical rulebook, a formal ADR, or a list of active binding */
132
+ /* domain invariants. Plans / scratchpads / UAT logs / API docs are NOT */
133
+ /* authoritative even if they contain proposed or historical decisions. */
134
+ /* -------------------------------------------------------------------------- */
135
+ const FILE_FILTER_SCHEMA = {
136
+ type: "object",
137
+ additionalProperties: false,
138
+ required: ["files"],
139
+ properties: {
140
+ files: {
141
+ type: "array",
142
+ items: {
143
+ type: "object",
144
+ additionalProperties: false,
145
+ required: ["path", "is_authoritative", "reason"],
146
+ properties: {
147
+ path: { type: "string" },
148
+ is_authoritative: { type: "boolean" },
149
+ reason: { type: "string" },
150
+ },
151
+ },
152
+ },
153
+ },
154
+ };
155
+ const FILE_FILTER_SYSTEM = `You are a rigid filter for an architecture ledger. A file is authoritative ONLY if it is a canonical rulebook, a formal Architecture Decision Record (ADR), or a list of active, binding domain invariants.
156
+
157
+ If a file is a project plan, research scratchpad, UAT log, status update, or API documentation, it is NOT authoritative, even if it contains proposed or historical decisions.
158
+
159
+ Evaluate the provided filepath, frontmatter, intro, and Table of Contents. Return JSON:
160
+ { "files": [ { "path": "<filepath>", "is_authoritative": <bool>, "reason": "10 words max" }, ... ] }
161
+
162
+ EXACTLY one entry per input filepath. Do NOT omit. Do NOT invent paths.`;
163
+ function buildFileFilterInputs(repoRoot, files) {
164
+ const out = [];
165
+ for (const rel of files) {
166
+ const abs = join(repoRoot, rel);
167
+ if (!existsSync(abs))
68
168
  continue;
69
- let st;
169
+ let raw;
70
170
  try {
71
- st = statSync(abs);
171
+ raw = readFileSync(abs, "utf8");
72
172
  }
73
173
  catch {
74
174
  continue;
75
175
  }
76
- out.push({ path: relative(repoRoot, abs), size: st.size, group: dirGroup(relative(repoRoot, abs)) });
176
+ const { frontmatter, body } = splitFrontmatter(raw);
177
+ const introChars = body.slice(0, FILE_FILTER_INTRO_CHARS);
178
+ const toc = extractToc(body);
179
+ out.push({ path: rel, frontmatter, introChars, toc });
77
180
  }
181
+ return out;
78
182
  }
79
- function dirGroup(rel) {
80
- const parts = rel.split("/");
81
- if (parts.length <= 1)
82
- return "(root)";
83
- return `${parts[0]}/`;
183
+ function splitFrontmatter(raw) {
184
+ const m = raw.match(/^---\n([\s\S]*?)\n---\n?/);
185
+ if (m === null)
186
+ return { frontmatter: null, body: raw };
187
+ const fm = m[1] ?? "";
188
+ return { frontmatter: fm, body: raw.slice(m[0].length) };
189
+ }
190
+ function extractToc(body) {
191
+ const lines = body.split("\n");
192
+ const toc = [];
193
+ for (const line of lines) {
194
+ if (/^#{1,3}\s+/.test(line)) {
195
+ toc.push(line.trim());
196
+ if (toc.length >= FILE_FILTER_TOC_MAX_LINES)
197
+ break;
198
+ }
199
+ }
200
+ return toc.join("\n");
201
+ }
202
+ async function classifyFileBatch(inputs) {
203
+ if (inputs.length === 0)
204
+ return new Map();
205
+ const blocks = inputs
206
+ .map((it) => {
207
+ const fmBlock = it.frontmatter !== null
208
+ ? `frontmatter:\n${it.frontmatter}\n`
209
+ : `frontmatter: (none)\n`;
210
+ const tocBlock = it.toc.length > 0 ? `toc:\n${it.toc}\n` : `toc: (none)\n`;
211
+ const intro = it.introChars.length > 0
212
+ ? `intro:\n${it.introChars}`
213
+ : `intro: (empty)`;
214
+ return `=== path: ${it.path}\n${fmBlock}${tocBlock}${intro}`;
215
+ })
216
+ .join("\n\n");
217
+ const prompt = `Classify each file. Return one entry per path.\n\n${blocks}`;
218
+ const result = await runClaude({
219
+ tier: "haiku",
220
+ system: FILE_FILTER_SYSTEM,
221
+ prompt,
222
+ jsonSchema: FILE_FILTER_SCHEMA,
223
+ timeoutMs: FILE_FILTER_TIMEOUT_MS,
224
+ isolateAmbientContext: true,
225
+ });
226
+ const parsed = result.parsed;
227
+ if (typeof parsed !== "object" || parsed === null) {
228
+ throw new Error("haiku file-filter returned non-object");
229
+ }
230
+ const arr = parsed["files"];
231
+ if (!Array.isArray(arr)) {
232
+ throw new Error("haiku file-filter missing `files` array");
233
+ }
234
+ const out = new Map();
235
+ for (const raw of arr) {
236
+ if (typeof raw !== "object" || raw === null)
237
+ continue;
238
+ const e = raw;
239
+ const path = e["path"];
240
+ const flag = e["is_authoritative"];
241
+ const reason = e["reason"];
242
+ if (typeof path !== "string")
243
+ continue;
244
+ if (typeof flag !== "boolean")
245
+ continue;
246
+ out.set(path, {
247
+ is_authoritative: flag,
248
+ reason: typeof reason === "string" ? reason : "",
249
+ });
250
+ }
251
+ return out;
84
252
  }
85
253
  /* -------------------------------------------------------------------------- */
86
- /* Haiku classifier — kind only, no rewriting */
254
+ /* Stage 2section batch classifier (kind + proposedTitle) */
87
255
  /* -------------------------------------------------------------------------- */
88
- const CLASSIFY_SCHEMA = {
256
+ const SECTION_SCHEMA = {
89
257
  type: "object",
90
258
  additionalProperties: false,
259
+ required: ["classifications"],
91
260
  properties: {
92
- kind: {
93
- type: "string",
94
- enum: ["decision", "domain-rule", "voice-guidelines", "api-docs", "other"],
261
+ classifications: {
262
+ type: "array",
263
+ items: {
264
+ type: "object",
265
+ additionalProperties: false,
266
+ required: ["slug", "kind", "proposedTitle"],
267
+ properties: {
268
+ slug: { type: "string" },
269
+ kind: {
270
+ type: "string",
271
+ enum: ["decision", "domain-rule", "voice-guidelines", "api-docs", "other"],
272
+ },
273
+ proposedTitle: { type: "string" },
274
+ },
275
+ },
95
276
  },
96
- proposedTitle: { type: "string" },
97
277
  },
98
- required: ["kind", "proposedTitle"],
99
278
  };
100
- const CLASSIFY_SYSTEM = `You classify project documentation paragraphs for Cairn's Single-Source-of-Truth ledger.
279
+ const SECTION_SYSTEM = `You classify N sections from authoritative project documentation for Cairn's Single-Source-of-Truth ledger.
280
+
281
+ These sections come from files already filtered as canonical rulebooks, ADRs, or binding invariant lists. Decide which sections are themselves binding decisions / rules vs supporting context.
101
282
 
102
- Return JSON matching the supplied schema.
283
+ Return JSON: { "classifications": [ { "slug": "...", "kind": "...", "proposedTitle": "..." }, ... ] }
284
+
285
+ EXACTLY one classification per input section, keyed by its slug. Do NOT omit. Do NOT invent slugs. If unsure, kind="other".
103
286
 
104
287
  \`kind\` choices:
105
- - "decision" paragraph describes a binding decision or architectural choice
106
- - "domain-rule" paragraph describes a domain rule or constraint developers must obey
107
- - "voice-guidelines" paragraph is brand voice / tone guidance
108
- - "api-docs" paragraph documents an API surface or schema (descriptive, not binding)
288
+ - "decision" binding decision or architectural choice
289
+ - "domain-rule" domain rule or constraint developers must obey
290
+ - "voice-guidelines" brand voice / tone guidance
291
+ - "api-docs" API surface / schema documentation (descriptive)
109
292
  - "other" nothing actionable for the cairn state layer
110
293
 
111
- \`proposedTitle\` 5-10 words, imperative voice, empty for "other".
294
+ \`proposedTitle\` 5-10 words, imperative voice. Empty string for "other".
112
295
 
113
- Be conservative — false-positive decisions pollute the ground state worse
114
- than missed capture. Default to "other" when uncertain.`;
115
- async function classifyEntry(entry, body) {
116
- const capped = body.length > DOC_BODY_CAP ? `${body.slice(0, DOC_BODY_CAP)}\n…[truncated]` : body;
117
- const prompt = `Source: ${entry.sot_source}\nSlug: ${entry.slug}\n\n---\n${capped}`;
296
+ Be conservative — false-positive decisions pollute the ground state worse than missed capture. Default to "other" when uncertain.`;
297
+ async function classifySectionBatch(items) {
298
+ if (items.length === 0)
299
+ return new Map();
300
+ const sections = items
301
+ .map((it, i) => {
302
+ const capped = it.body.length > SECTION_BODY_CAP
303
+ ? `${it.body.slice(0, SECTION_BODY_CAP)}\n…[truncated]`
304
+ : it.body;
305
+ return `[${i + 1}] slug=${it.slug} source=${it.sot_source}\n${capped}`;
306
+ })
307
+ .join("\n\n---\n\n");
308
+ const prompt = `Classify each section. Return one entry per slug.\n\n${sections}`;
118
309
  const result = await runClaude({
119
310
  tier: "haiku",
120
- system: CLASSIFY_SYSTEM,
311
+ system: SECTION_SYSTEM,
121
312
  prompt,
122
- jsonSchema: CLASSIFY_SCHEMA,
123
- timeoutMs: PER_DOC_TIMEOUT_MS,
313
+ jsonSchema: SECTION_SCHEMA,
314
+ timeoutMs: SECTION_TIMEOUT_MS,
124
315
  isolateAmbientContext: true,
125
316
  });
126
317
  const parsed = result.parsed;
127
318
  if (typeof parsed !== "object" || parsed === null) {
128
- throw new Error("haiku returned non-object classification");
319
+ throw new Error("haiku section batch returned non-object");
129
320
  }
130
- const r = parsed;
131
- const kind = r["kind"];
132
- if (kind !== "decision" &&
133
- kind !== "domain-rule" &&
134
- kind !== "voice-guidelines" &&
135
- kind !== "api-docs" &&
136
- kind !== "other") {
137
- throw new Error(`haiku returned unexpected kind: ${String(kind)}`);
321
+ const arr = parsed["classifications"];
322
+ if (!Array.isArray(arr)) {
323
+ throw new Error("haiku section batch missing `classifications`");
138
324
  }
139
- return {
140
- kind,
141
- proposedTitle: typeof r["proposedTitle"] === "string" ? r["proposedTitle"] : "",
142
- };
325
+ const out = new Map();
326
+ for (const raw of arr) {
327
+ if (typeof raw !== "object" || raw === null)
328
+ continue;
329
+ const e = raw;
330
+ const slug = e["slug"];
331
+ const kind = e["kind"];
332
+ if (typeof slug !== "string")
333
+ continue;
334
+ if (kind !== "decision" &&
335
+ kind !== "domain-rule" &&
336
+ kind !== "voice-guidelines" &&
337
+ kind !== "api-docs" &&
338
+ kind !== "other") {
339
+ continue;
340
+ }
341
+ out.set(slug, {
342
+ kind,
343
+ proposedTitle: typeof e["proposedTitle"] === "string" ? e["proposedTitle"] : "",
344
+ });
345
+ }
346
+ return out;
143
347
  }
144
- /* -------------------------------------------------------------------------- */
145
- /* Orchestrator */
146
- /* -------------------------------------------------------------------------- */
147
348
  export async function runDocsIngestion(args) {
148
349
  const topicIndex = readTopicIndex(args.repoRoot);
149
350
  const anchorMap = readAnchorMap(args.repoRoot);
150
- const candidateEntries = Object.values(topicIndex.topics).filter((entry) => isDocSoT(entry) && entry.dec_id === undefined);
151
- if (candidateEntries.length === 0) {
152
- log.info("phase 6 found no eligible docs entries in topic-index");
153
- return { decsWritten: [], skipped: [], scannedEntries: 0 };
154
- }
155
- let processed = 0;
156
- const result = await emitFromTopicIndex({
157
- repoRoot: args.repoRoot,
158
- topicIndex,
159
- anchorMap,
160
- filter: (entry) => isDocSoT(entry) && entry.dec_id === undefined,
161
- classifier: async ({ body, entry }) => {
351
+ const rejected = readRejectedYaml(args.repoRoot);
352
+ const allCandidates = Object.values(topicIndex.topics).filter((entry) => isDocSoT(entry) && entry.dec_id === undefined && !rejected.has(entry.slug));
353
+ if (allCandidates.length === 0) {
354
+ log.info("phase 8 found no eligible docs entries in topic-index");
355
+ writeFileCandidatesMap(args.repoRoot, topicIndex);
356
+ return zeroResult(allCandidates.length, topicIndex);
357
+ }
358
+ // Read each candidate body once. Stage 3 needs the body for title
359
+ // derivation; Stages 1/2 don't, but reading up front keeps the
360
+ // pipeline single-pass over entries. Bodies that fail to read are
361
+ // dropped anchor-map drift is the only realistic cause and the
362
+ // entry stays as a candidate for the next phase 7 refresh.
363
+ const ctxBySlug = new Map();
364
+ for (const entry of allCandidates) {
365
+ const body = readSotBody(args.repoRoot, entry, anchorMap);
366
+ if (body === null)
367
+ continue;
368
+ ctxBySlug.set(entry.slug, { entry, body });
369
+ }
370
+ // ── Stage 3 — marker scan (deterministic, 0 Haiku) ──
371
+ const markerCandidates = [];
372
+ const nonMarkerCandidates = [];
373
+ for (const ctx of ctxBySlug.values()) {
374
+ if (ctx.entry.marker_kind !== undefined)
375
+ markerCandidates.push(ctx);
376
+ else
377
+ nonMarkerCandidates.push(ctx);
378
+ }
379
+ // ── Mock path — bypass Stages 1+2; run mockClassify on every
380
+ // non-marker candidate. Smokes only.
381
+ let sectionEmits = [];
382
+ let authoritativeFileCount = 0;
383
+ let filesEvaluated = 0;
384
+ if (args.mockClassify !== undefined) {
385
+ for (const ctx of nonMarkerCandidates) {
162
386
  let cls;
163
387
  try {
164
- cls = args.mockClassify !== undefined
165
- ? args.mockClassify(entry, body)
166
- : await classifyEntry(entry, body);
388
+ cls = args.mockClassify(ctx.entry, ctx.body);
167
389
  }
168
390
  catch (err) {
169
- log.warn({ slug: entry.slug, err: err instanceof Error ? err.message : String(err) }, "classifier failed; skipping");
170
- return { kind: "skip", title: "" };
171
- }
172
- processed += 1;
173
- if (args.onEntryProgress !== undefined) {
174
- args.onEntryProgress({
175
- slug: entry.slug,
176
- emitted: cls.kind === "decision" || cls.kind === "domain-rule",
177
- total: candidateEntries.length,
178
- });
391
+ log.warn({ slug: ctx.entry.slug, err: err instanceof Error ? err.message : String(err) }, "mockClassify failed; skipping");
392
+ continue;
179
393
  }
180
394
  if (cls.kind === "decision" || cls.kind === "domain-rule") {
181
- return { kind: "decision", title: cls.proposedTitle };
395
+ sectionEmits.push({ ctx, cls });
182
396
  }
183
- return { kind: "skip", title: cls.proposedTitle };
184
- },
185
- sot_kind: "path",
186
- capture_source: "init-docs-ingest",
187
- });
188
- writeSotBindings(args.repoRoot, result.bindings);
189
- writeSotCache(args.repoRoot, result.cache);
190
- writeTopicIndex(args.repoRoot, result.topicIndex);
191
- const decsWritten = result.emitted.map((rec) => ({
192
- id: rec.id,
193
- path: relativeDecPath(rec.id),
194
- sourceFile: rec.source_file,
195
- slug: rec.slug,
196
- }));
397
+ }
398
+ if (args.onChunkProgress !== undefined) {
399
+ args.onChunkProgress({
400
+ chunksDone: 1,
401
+ totalChunks: 1,
402
+ entriesDone: nonMarkerCandidates.length,
403
+ totalEntries: nonMarkerCandidates.length,
404
+ stage: "section-classify",
405
+ });
406
+ }
407
+ }
408
+ else {
409
+ // ── Stage 1 — file-purpose binary filter ──
410
+ const distinctFiles = [
411
+ ...new Set(nonMarkerCandidates.map((c) => c.entry.sot_source)),
412
+ ].sort();
413
+ filesEvaluated = distinctFiles.length;
414
+ const stage1Args = {
415
+ repoRoot: args.repoRoot,
416
+ files: distinctFiles,
417
+ };
418
+ if (args.onChunkProgress !== undefined) {
419
+ stage1Args.onChunkProgress = args.onChunkProgress;
420
+ }
421
+ const fileVerdicts = await runStage1FileFilter(stage1Args);
422
+ const authoritativeFiles = new Set();
423
+ for (const [path, v] of fileVerdicts.entries()) {
424
+ if (v.is_authoritative)
425
+ authoritativeFiles.add(path);
426
+ }
427
+ authoritativeFileCount = authoritativeFiles.size;
428
+ // ── Stage 2 — section batch classifier (scoped) ──
429
+ const stage2Inputs = nonMarkerCandidates.filter((c) => authoritativeFiles.has(c.entry.sot_source));
430
+ const stage2Args = {
431
+ candidates: stage2Inputs,
432
+ };
433
+ if (args.onChunkProgress !== undefined) {
434
+ stage2Args.onChunkProgress = args.onChunkProgress;
435
+ }
436
+ sectionEmits = await runStage2SectionClassifier(stage2Args);
437
+ }
438
+ // ── Stage 4 — emit drafts to `_inbox/` ──
439
+ const existingDecIds = args.existingDecIds ?? scanExistingDecIds(args.repoRoot);
440
+ const finalEmits = [
441
+ ...markerCandidates.map((ctx) => {
442
+ const kind = ctx.entry.marker_kind === "rule" ? "domain-rule" : "decision";
443
+ return { ctx, cls: { kind, proposedTitle: deriveMarkerTitle(ctx) } };
444
+ }),
445
+ ...sectionEmits,
446
+ ];
447
+ let updatedTopicIndex = topicIndex;
448
+ const decsWritten = [];
449
+ const skipped = [];
450
+ for (const { ctx, cls } of finalEmits) {
451
+ const sot_path = entryToSotPath(ctx.entry);
452
+ const titleSeed = cls.proposedTitle.length > 0
453
+ ? cls.proposedTitle
454
+ : firstLineFallback(ctx.body);
455
+ const id = allocateUniqueDecId({ sot_path, title: titleSeed, capture_source: CAPTURE_SOURCE }, existingDecIds);
456
+ const draftPath = writeDraftToInbox({
457
+ repoRoot: args.repoRoot,
458
+ id,
459
+ title: titleSeed,
460
+ body: ctx.body,
461
+ sot_path,
462
+ source_file: ctx.entry.sot_source,
463
+ });
464
+ decsWritten.push({
465
+ id,
466
+ path: relativeInboxPath(id),
467
+ sourceFile: ctx.entry.sot_source,
468
+ slug: ctx.entry.slug,
469
+ });
470
+ updatedTopicIndex = setTopic(updatedTopicIndex, ctx.entry.slug, {
471
+ ...ctx.entry,
472
+ dec_id: id,
473
+ });
474
+ log.debug({ id, slug: ctx.entry.slug, draftPath }, "phase 8 emitted draft");
475
+ }
476
+ // Refresh topic-index + file-candidates-map so the read-enrich hook
477
+ // sees the post-emit candidate counts. Anchor-map / sot-bindings /
478
+ // sot-cache stay untouched — drafts in `_inbox/` aren't canonical
479
+ // until the operator (or `cairn attention`) accepts them.
480
+ writeTopicIndex(args.repoRoot, updatedTopicIndex);
481
+ writeFileCandidatesMap(args.repoRoot, updatedTopicIndex);
482
+ const unpromotedCandidates = countUnpromoted(updatedTopicIndex);
197
483
  log.info({
198
- scanned: candidateEntries.length,
484
+ scanned: allCandidates.length,
199
485
  emitted: decsWritten.length,
200
- skipped: result.skipped.length,
201
- processed,
202
- }, "phase 6 complete");
486
+ markerEmits: markerCandidates.length,
487
+ sectionEmits: sectionEmits.length,
488
+ authoritativeFiles: authoritativeFileCount,
489
+ filesEvaluated,
490
+ unpromotedCandidates,
491
+ }, "phase 8 complete");
203
492
  return {
204
493
  decsWritten,
205
- skipped: result.skipped,
206
- scannedEntries: candidateEntries.length,
494
+ skipped,
495
+ scannedEntries: allCandidates.length,
496
+ markerEmits: markerCandidates.length,
497
+ sectionEmits: sectionEmits.length,
498
+ authoritativeFiles: authoritativeFileCount,
499
+ filesEvaluated,
500
+ unpromotedCandidates,
207
501
  };
208
502
  }
209
- function relativeDecPath(id) {
210
- return `.cairn/ground/decisions/${id}.md`;
503
+ /* -------------------------------------------------------------------------- */
504
+ /* Stage runners */
505
+ /* -------------------------------------------------------------------------- */
506
+ export async function runStage1FileFilter(args) {
507
+ const verdicts = new Map();
508
+ if (args.files.length === 0)
509
+ return verdicts;
510
+ const inputs = buildFileFilterInputs(args.repoRoot, args.files);
511
+ const chunks = [];
512
+ for (let i = 0; i < inputs.length; i += FILE_FILTER_BATCH_SIZE) {
513
+ chunks.push(inputs.slice(i, i + FILE_FILTER_BATCH_SIZE));
514
+ }
515
+ let nextIdx = 0;
516
+ let chunksDone = 0;
517
+ let entriesDone = 0;
518
+ const worker = async () => {
519
+ for (;;) {
520
+ const idx = nextIdx;
521
+ nextIdx += 1;
522
+ if (idx >= chunks.length)
523
+ return;
524
+ const chunk = chunks[idx];
525
+ try {
526
+ const map = await classifyFileBatch(chunk);
527
+ for (const [path, v] of map.entries())
528
+ verdicts.set(path, v);
529
+ }
530
+ catch (err) {
531
+ log.warn({ chunkIdx: idx, size: chunk.length, err: err instanceof Error ? err.message : String(err) }, "phase 8 stage 1 file-filter failed; chunk treated as non-authoritative");
532
+ }
533
+ chunksDone += 1;
534
+ entriesDone += chunk.length;
535
+ if (args.onChunkProgress !== undefined) {
536
+ args.onChunkProgress({
537
+ chunksDone,
538
+ totalChunks: chunks.length,
539
+ entriesDone,
540
+ totalEntries: inputs.length,
541
+ stage: "file-filter",
542
+ });
543
+ }
544
+ }
545
+ };
546
+ await Promise.all(Array.from({ length: Math.min(FILE_FILTER_CONCURRENCY, Math.max(1, chunks.length)) }, () => worker()));
547
+ return verdicts;
211
548
  }
212
- /**
213
- * Phase 6 owns every topic-index entry whose SoT candidate was tagged
214
- * `kind="doc"` by the phase 5b walker. Path-prefix matching would lock
215
- * us to `docs/` and miss `documentation/`, `official_docs/`, etc.; the
216
- * walker's per-candidate kind is already the right discriminant.
217
- */
549
+ async function runStage2SectionClassifier(args) {
550
+ const out = [];
551
+ if (args.candidates.length === 0)
552
+ return out;
553
+ const items = args.candidates.map((c) => ({
554
+ slug: c.entry.slug,
555
+ body: c.body,
556
+ sot_source: c.entry.sot_source,
557
+ }));
558
+ const ctxBySlug = new Map(args.candidates.map((c) => [c.entry.slug, c]));
559
+ const chunks = [];
560
+ for (let i = 0; i < items.length; i += SECTION_BATCH_SIZE) {
561
+ chunks.push(items.slice(i, i + SECTION_BATCH_SIZE));
562
+ }
563
+ let nextIdx = 0;
564
+ let chunksDone = 0;
565
+ let entriesDone = 0;
566
+ const verdicts = new Map();
567
+ const worker = async () => {
568
+ for (;;) {
569
+ const idx = nextIdx;
570
+ nextIdx += 1;
571
+ if (idx >= chunks.length)
572
+ return;
573
+ const chunk = chunks[idx];
574
+ try {
575
+ const map = await classifySectionBatch(chunk);
576
+ for (const [slug, cls] of map.entries())
577
+ verdicts.set(slug, cls);
578
+ }
579
+ catch (err) {
580
+ log.warn({ chunkIdx: idx, size: chunk.length, err: err instanceof Error ? err.message : String(err) }, "phase 8 stage 2 batch failed; chunk skipped");
581
+ }
582
+ chunksDone += 1;
583
+ entriesDone += chunk.length;
584
+ if (args.onChunkProgress !== undefined) {
585
+ args.onChunkProgress({
586
+ chunksDone,
587
+ totalChunks: chunks.length,
588
+ entriesDone,
589
+ totalEntries: items.length,
590
+ stage: "section-classify",
591
+ });
592
+ }
593
+ }
594
+ };
595
+ await Promise.all(Array.from({ length: Math.min(SECTION_CONCURRENCY, Math.max(1, chunks.length)) }, () => worker()));
596
+ for (const [slug, cls] of verdicts.entries()) {
597
+ if (cls.kind !== "decision" && cls.kind !== "domain-rule")
598
+ continue;
599
+ const ctx = ctxBySlug.get(slug);
600
+ if (ctx === undefined)
601
+ continue;
602
+ out.push({ ctx, cls });
603
+ }
604
+ return out;
605
+ }
606
+ function writeDraftToInbox(args) {
607
+ const inboxDir = join(decisionsDir(args.repoRoot), "_inbox");
608
+ mkdirSync(inboxDir, { recursive: true });
609
+ const abs = join(inboxDir, `${args.id}.draft.md`);
610
+ const now = new Date().toISOString();
611
+ const fm = {
612
+ id: args.id,
613
+ title: args.title,
614
+ type: "adr",
615
+ status: "draft",
616
+ audience: "dual",
617
+ generated: now,
618
+ "verified-at": now,
619
+ decided_at: now,
620
+ decided_by: DECIDED_BY,
621
+ sot_kind: "path",
622
+ sot_path: args.sot_path,
623
+ sot_content_hash: bodyContentHash(args.body),
624
+ capture_source: CAPTURE_SOURCE,
625
+ source_file: args.source_file,
626
+ };
627
+ const out = [];
628
+ out.push("---");
629
+ out.push(stringifyYaml(fm).trimEnd());
630
+ out.push("---");
631
+ out.push("");
632
+ out.push(args.body.trimEnd());
633
+ out.push("");
634
+ writeFileSync(abs, out.join("\n"), "utf8");
635
+ return abs;
636
+ }
637
+ function relativeInboxPath(id) {
638
+ return `.cairn/ground/decisions/_inbox/${id}.draft.md`;
639
+ }
640
+ /* -------------------------------------------------------------------------- */
641
+ /* Helpers */
642
+ /* -------------------------------------------------------------------------- */
218
643
  function isDocSoT(entry) {
219
644
  const sot = entry.candidates.find((c) => c.file === entry.sot_source);
220
645
  return sot !== undefined && sot.kind === "doc";
221
646
  }
647
+ function entryToSotPath(entry) {
648
+ const sot = entry.candidates.find((c) => c.file === entry.sot_source);
649
+ if (sot === undefined)
650
+ return entry.sot_source;
651
+ if (sot.anchor !== undefined && sot.anchor.length > 0) {
652
+ return `${entry.sot_source}#${sot.anchor}`;
653
+ }
654
+ return entry.sot_source;
655
+ }
656
+ // firstLineFallback now lives in sot-emit.ts (single source of truth).
657
+ // Imported above as `firstLineFallback`.
658
+ function deriveMarkerTitle(ctx) {
659
+ // Prefer the topic-index entry's anchor text (post-walker normalization)
660
+ // when present; fall back to the SoT body's first non-blank line.
661
+ const sot = ctx.entry.candidates.find((c) => c.file === ctx.entry.sot_source);
662
+ if (sot?.anchor !== undefined && sot.anchor.length > 0) {
663
+ return sot.anchor.replace(/[-_]+/g, " ").trim().slice(0, 120) || firstLineFallback(ctx.body);
664
+ }
665
+ return firstLineFallback(ctx.body);
666
+ }
667
+ /**
668
+ * Allocate a DEC id that doesn't collide with `existingIds`. The
669
+ * derivation is content-stable, but two distinct topics with identical
670
+ * `(sot_path, title, capture_source)` tuples would clash — fall back
671
+ * to a counter suffix in that pathological case.
672
+ */
673
+ function allocateUniqueDecId(input, existingIds) {
674
+ let id = deriveDecId(input);
675
+ if (!existingIds.has(id)) {
676
+ existingIds.add(id);
677
+ return id;
678
+ }
679
+ for (let suffix = 2; suffix < 1_000; suffix += 1) {
680
+ const tagged = deriveDecId({ ...input, title: `${input.title} #${suffix}` });
681
+ if (!existingIds.has(tagged)) {
682
+ existingIds.add(tagged);
683
+ return tagged;
684
+ }
685
+ }
686
+ // Exceedingly unlikely. If we hit it, return the deterministic id and
687
+ // let the filesystem write fail loudly rather than fabricating a
688
+ // random suffix that would break subsequent re-runs.
689
+ existingIds.add(id);
690
+ return id;
691
+ }
692
+ function scanExistingDecIds(repoRoot) {
693
+ const out = new Set();
694
+ const dir = decisionsDir(repoRoot);
695
+ for (const sub of [dir, join(dir, "_inbox")]) {
696
+ let entries;
697
+ try {
698
+ entries = readdirSync(sub, { withFileTypes: true, encoding: "utf8" });
699
+ }
700
+ catch {
701
+ continue;
702
+ }
703
+ for (const e of entries) {
704
+ if (!e.isFile())
705
+ continue;
706
+ const m = e.name.match(/^(DEC-[0-9a-f]{7,})/);
707
+ if (m === null)
708
+ continue;
709
+ out.add(m[1]);
710
+ }
711
+ }
712
+ return out;
713
+ }
714
+ function countUnpromoted(topicIndex) {
715
+ let n = 0;
716
+ for (const e of Object.values(topicIndex.topics)) {
717
+ if (e.dec_id === undefined)
718
+ n += 1;
719
+ }
720
+ return n;
721
+ }
722
+ function zeroResult(scanned, topicIndex) {
723
+ return {
724
+ decsWritten: [],
725
+ skipped: [],
726
+ scannedEntries: scanned,
727
+ markerEmits: 0,
728
+ sectionEmits: 0,
729
+ authoritativeFiles: 0,
730
+ filesEvaluated: 0,
731
+ unpromotedCandidates: countUnpromoted(topicIndex),
732
+ };
733
+ }
222
734
  //# sourceMappingURL=ingest-docs.js.map