@latysh-development/monolith 0.0.0-stage → 0.1.0-alpha.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 (345) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +179 -2
  3. package/dist/brief/cli.d.ts +30 -0
  4. package/dist/brief/cli.d.ts.map +1 -0
  5. package/dist/brief/cli.js +85 -0
  6. package/dist/brief/collect.d.ts +17 -0
  7. package/dist/brief/collect.d.ts.map +1 -0
  8. package/dist/brief/collect.js +342 -0
  9. package/dist/brief/config.d.ts +40 -0
  10. package/dist/brief/config.d.ts.map +1 -0
  11. package/dist/brief/config.js +85 -0
  12. package/dist/brief/constants.d.ts +12 -0
  13. package/dist/brief/constants.d.ts.map +1 -0
  14. package/dist/brief/constants.js +11 -0
  15. package/dist/brief/docs.d.ts +53 -0
  16. package/dist/brief/docs.d.ts.map +1 -0
  17. package/dist/brief/docs.js +229 -0
  18. package/dist/brief/freshness.d.ts +104 -0
  19. package/dist/brief/freshness.d.ts.map +1 -0
  20. package/dist/brief/freshness.js +263 -0
  21. package/dist/brief/git-signals.d.ts +17 -0
  22. package/dist/brief/git-signals.d.ts.map +1 -0
  23. package/dist/brief/git-signals.js +101 -0
  24. package/dist/brief/index.d.ts +11 -0
  25. package/dist/brief/index.d.ts.map +1 -0
  26. package/dist/brief/index.js +5 -0
  27. package/dist/brief/reconcile.d.ts +75 -0
  28. package/dist/brief/reconcile.d.ts.map +1 -0
  29. package/dist/brief/reconcile.js +259 -0
  30. package/dist/brief/render.d.ts +67 -0
  31. package/dist/brief/render.d.ts.map +1 -0
  32. package/dist/brief/render.js +458 -0
  33. package/dist/brief/sanitize.d.ts +38 -0
  34. package/dist/brief/sanitize.d.ts.map +1 -0
  35. package/dist/brief/sanitize.js +48 -0
  36. package/dist/brief/selectors.d.ts +36 -0
  37. package/dist/brief/selectors.d.ts.map +1 -0
  38. package/dist/brief/selectors.js +75 -0
  39. package/dist/brief/story.d.ts +18 -0
  40. package/dist/brief/story.d.ts.map +1 -0
  41. package/dist/brief/story.js +59 -0
  42. package/dist/brief/types.d.ts +126 -0
  43. package/dist/brief/types.d.ts.map +1 -0
  44. package/dist/brief/types.js +11 -0
  45. package/dist/cli/index.d.ts +3 -0
  46. package/dist/cli/index.d.ts.map +1 -0
  47. package/dist/cli/index.js +199 -0
  48. package/dist/cli/init-args.d.ts +16 -0
  49. package/dist/cli/init-args.d.ts.map +1 -0
  50. package/dist/cli/init-args.js +24 -0
  51. package/dist/cli/ui-args.d.ts +23 -0
  52. package/dist/cli/ui-args.d.ts.map +1 -0
  53. package/dist/cli/ui-args.js +66 -0
  54. package/dist/cli/ui.d.ts +40 -0
  55. package/dist/cli/ui.d.ts.map +1 -0
  56. package/dist/cli/ui.js +180 -0
  57. package/dist/coordinatorium/checkpoint.d.ts +44 -0
  58. package/dist/coordinatorium/checkpoint.d.ts.map +1 -0
  59. package/dist/coordinatorium/checkpoint.js +124 -0
  60. package/dist/coordinatorium/cli.d.ts +39 -0
  61. package/dist/coordinatorium/cli.d.ts.map +1 -0
  62. package/dist/coordinatorium/cli.js +203 -0
  63. package/dist/coordinatorium/errors.d.ts +112 -0
  64. package/dist/coordinatorium/errors.d.ts.map +1 -0
  65. package/dist/coordinatorium/errors.js +145 -0
  66. package/dist/coordinatorium/index.d.ts +11 -0
  67. package/dist/coordinatorium/index.d.ts.map +1 -0
  68. package/dist/coordinatorium/index.js +5 -0
  69. package/dist/coordinatorium/lock.d.ts +187 -0
  70. package/dist/coordinatorium/lock.d.ts.map +1 -0
  71. package/dist/coordinatorium/lock.js +659 -0
  72. package/dist/coordinatorium/mission.d.ts +32 -0
  73. package/dist/coordinatorium/mission.d.ts.map +1 -0
  74. package/dist/coordinatorium/mission.js +87 -0
  75. package/dist/coordinatorium/state-keeper.d.ts +115 -0
  76. package/dist/coordinatorium/state-keeper.d.ts.map +1 -0
  77. package/dist/coordinatorium/state-keeper.js +396 -0
  78. package/dist/coordinatorium/types.d.ts +69 -0
  79. package/dist/coordinatorium/types.d.ts.map +1 -0
  80. package/dist/coordinatorium/types.js +27 -0
  81. package/dist/discovery/compose.d.ts +28 -0
  82. package/dist/discovery/compose.d.ts.map +1 -0
  83. package/dist/discovery/compose.js +159 -0
  84. package/dist/discovery/contracts.d.ts +211 -0
  85. package/dist/discovery/contracts.d.ts.map +1 -0
  86. package/dist/discovery/contracts.js +555 -0
  87. package/dist/discovery/docs.d.ts +278 -0
  88. package/dist/discovery/docs.d.ts.map +1 -0
  89. package/dist/discovery/docs.js +1182 -0
  90. package/dist/discovery/facts.d.ts +49 -0
  91. package/dist/discovery/facts.d.ts.map +1 -0
  92. package/dist/discovery/facts.js +541 -0
  93. package/dist/discovery/index.d.ts +8 -0
  94. package/dist/discovery/index.d.ts.map +1 -0
  95. package/dist/discovery/index.js +9 -0
  96. package/dist/discovery/orchestrate.d.ts +35 -0
  97. package/dist/discovery/orchestrate.d.ts.map +1 -0
  98. package/dist/discovery/orchestrate.js +32 -0
  99. package/dist/discovery/render.d.ts +36 -0
  100. package/dist/discovery/render.d.ts.map +1 -0
  101. package/dist/discovery/render.js +278 -0
  102. package/dist/discovery/traverse.d.ts +197 -0
  103. package/dist/discovery/traverse.d.ts.map +1 -0
  104. package/dist/discovery/traverse.js +510 -0
  105. package/dist/durable/assess.d.ts +105 -0
  106. package/dist/durable/assess.d.ts.map +1 -0
  107. package/dist/durable/assess.js +239 -0
  108. package/dist/durable/canonical.d.ts +25 -0
  109. package/dist/durable/canonical.d.ts.map +1 -0
  110. package/dist/durable/canonical.js +78 -0
  111. package/dist/durable/cli.d.ts +37 -0
  112. package/dist/durable/cli.d.ts.map +1 -0
  113. package/dist/durable/cli.js +141 -0
  114. package/dist/durable/errors.d.ts +192 -0
  115. package/dist/durable/errors.d.ts.map +1 -0
  116. package/dist/durable/errors.js +323 -0
  117. package/dist/durable/fs-store.d.ts +254 -0
  118. package/dist/durable/fs-store.d.ts.map +1 -0
  119. package/dist/durable/fs-store.js +1493 -0
  120. package/dist/durable/index.d.ts +20 -0
  121. package/dist/durable/index.d.ts.map +1 -0
  122. package/dist/durable/index.js +13 -0
  123. package/dist/durable/log.d.ts +33 -0
  124. package/dist/durable/log.d.ts.map +1 -0
  125. package/dist/durable/log.js +157 -0
  126. package/dist/durable/memory-store.d.ts +51 -0
  127. package/dist/durable/memory-store.d.ts.map +1 -0
  128. package/dist/durable/memory-store.js +302 -0
  129. package/dist/durable/projection.d.ts +35 -0
  130. package/dist/durable/projection.d.ts.map +1 -0
  131. package/dist/durable/projection.js +328 -0
  132. package/dist/durable/prune-check.d.ts +34 -0
  133. package/dist/durable/prune-check.d.ts.map +1 -0
  134. package/dist/durable/prune-check.js +72 -0
  135. package/dist/durable/register.d.ts +100 -0
  136. package/dist/durable/register.d.ts.map +1 -0
  137. package/dist/durable/register.js +532 -0
  138. package/dist/durable/retention.d.ts +49 -0
  139. package/dist/durable/retention.d.ts.map +1 -0
  140. package/dist/durable/retention.js +59 -0
  141. package/dist/durable/runtime.d.ts +499 -0
  142. package/dist/durable/runtime.d.ts.map +1 -0
  143. package/dist/durable/runtime.js +1288 -0
  144. package/dist/durable/seams.d.ts +44 -0
  145. package/dist/durable/seams.d.ts.map +1 -0
  146. package/dist/durable/seams.js +21 -0
  147. package/dist/durable/testing.d.ts +11 -0
  148. package/dist/durable/testing.d.ts.map +1 -0
  149. package/dist/durable/testing.js +25 -0
  150. package/dist/durable/types.d.ts +442 -0
  151. package/dist/durable/types.d.ts.map +1 -0
  152. package/dist/durable/types.js +75 -0
  153. package/dist/graph/builder.d.ts +11 -0
  154. package/dist/graph/builder.d.ts.map +1 -0
  155. package/dist/graph/builder.js +153 -0
  156. package/dist/graph/identity.d.ts +37 -0
  157. package/dist/graph/identity.d.ts.map +1 -0
  158. package/dist/graph/identity.js +46 -0
  159. package/dist/graph/index.d.ts +7 -0
  160. package/dist/graph/index.d.ts.map +1 -0
  161. package/dist/graph/index.js +3 -0
  162. package/dist/graph/resolver.d.ts +41 -0
  163. package/dist/graph/resolver.d.ts.map +1 -0
  164. package/dist/graph/resolver.js +36 -0
  165. package/dist/graph/types.d.ts +42 -0
  166. package/dist/graph/types.d.ts.map +1 -0
  167. package/dist/graph/types.js +1 -0
  168. package/dist/http/index.d.ts +5 -0
  169. package/dist/http/index.d.ts.map +1 -0
  170. package/dist/http/index.js +2 -0
  171. package/dist/http/server.d.ts +76 -0
  172. package/dist/http/server.d.ts.map +1 -0
  173. package/dist/http/server.js +195 -0
  174. package/dist/http/ui-assets.d.ts +22 -0
  175. package/dist/http/ui-assets.d.ts.map +1 -0
  176. package/dist/http/ui-assets.js +100 -0
  177. package/dist/ideas/cli.d.ts +33 -0
  178. package/dist/ideas/cli.d.ts.map +1 -0
  179. package/dist/ideas/cli.js +158 -0
  180. package/dist/ideas/format.d.ts +38 -0
  181. package/dist/ideas/format.d.ts.map +1 -0
  182. package/dist/ideas/format.js +194 -0
  183. package/dist/ideas/index.d.ts +6 -0
  184. package/dist/ideas/index.d.ts.map +1 -0
  185. package/dist/ideas/index.js +3 -0
  186. package/dist/ideas/store.d.ts +55 -0
  187. package/dist/ideas/store.d.ts.map +1 -0
  188. package/dist/ideas/store.js +262 -0
  189. package/dist/ideas/types.d.ts +54 -0
  190. package/dist/ideas/types.d.ts.map +1 -0
  191. package/dist/ideas/types.js +26 -0
  192. package/dist/index.d.ts +32 -0
  193. package/dist/index.d.ts.map +1 -0
  194. package/dist/index.js +28 -0
  195. package/dist/init/index.d.ts +12 -0
  196. package/dist/init/index.d.ts.map +1 -0
  197. package/dist/init/index.js +90 -0
  198. package/dist/init/prompts.d.ts +3 -0
  199. package/dist/init/prompts.d.ts.map +1 -0
  200. package/dist/init/prompts.js +47 -0
  201. package/dist/init/scaffold.d.ts +29 -0
  202. package/dist/init/scaffold.d.ts.map +1 -0
  203. package/dist/init/scaffold.js +208 -0
  204. package/dist/init/types.d.ts +14 -0
  205. package/dist/init/types.d.ts.map +1 -0
  206. package/dist/init/types.js +1 -0
  207. package/dist/managed/index.d.ts +73 -0
  208. package/dist/managed/index.d.ts.map +1 -0
  209. package/dist/managed/index.js +222 -0
  210. package/dist/readmodel/dashboard.d.ts +40 -0
  211. package/dist/readmodel/dashboard.d.ts.map +1 -0
  212. package/dist/readmodel/dashboard.js +236 -0
  213. package/dist/readmodel/index.d.ts +3 -0
  214. package/dist/readmodel/index.d.ts.map +1 -0
  215. package/dist/readmodel/index.js +1 -0
  216. package/dist/readmodel/types.d.ts +104 -0
  217. package/dist/readmodel/types.d.ts.map +1 -0
  218. package/dist/readmodel/types.js +15 -0
  219. package/dist/root/index.d.ts +186 -0
  220. package/dist/root/index.d.ts.map +1 -0
  221. package/dist/root/index.js +430 -0
  222. package/dist/root/safe-write.d.ts +268 -0
  223. package/dist/root/safe-write.d.ts.map +1 -0
  224. package/dist/root/safe-write.js +966 -0
  225. package/dist/root/write-artifacts.d.ts +20 -0
  226. package/dist/root/write-artifacts.d.ts.map +1 -0
  227. package/dist/root/write-artifacts.js +42 -0
  228. package/dist/scriptorium/compile.d.ts +4 -0
  229. package/dist/scriptorium/compile.d.ts.map +1 -0
  230. package/dist/scriptorium/compile.js +185 -0
  231. package/dist/scriptorium/frontmatter.d.ts +13 -0
  232. package/dist/scriptorium/frontmatter.d.ts.map +1 -0
  233. package/dist/scriptorium/frontmatter.js +90 -0
  234. package/dist/scriptorium/index.d.ts +3 -0
  235. package/dist/scriptorium/index.d.ts.map +1 -0
  236. package/dist/scriptorium/index.js +1 -0
  237. package/dist/scriptorium/markdown.d.ts +31 -0
  238. package/dist/scriptorium/markdown.d.ts.map +1 -0
  239. package/dist/scriptorium/markdown.js +88 -0
  240. package/dist/scriptorium/types.d.ts +78 -0
  241. package/dist/scriptorium/types.d.ts.map +1 -0
  242. package/dist/scriptorium/types.js +1 -0
  243. package/dist/types/enforcement.d.ts +11 -0
  244. package/dist/types/enforcement.d.ts.map +1 -0
  245. package/dist/types/enforcement.js +1 -0
  246. package/dist/types/integrity.d.ts +33 -0
  247. package/dist/types/integrity.d.ts.map +1 -0
  248. package/dist/types/integrity.js +1 -0
  249. package/dist/types/project-definition.d.ts +30 -0
  250. package/dist/types/project-definition.d.ts.map +1 -0
  251. package/dist/types/project-definition.js +1 -0
  252. package/dist/ui/assets/cosmic-glass-bg-Dwx47zPu.webp +0 -0
  253. package/dist/ui/assets/index-C4NhMaq7.js +28 -0
  254. package/dist/ui/assets/index-DMZ7ZvZY.css +1 -0
  255. package/dist/ui/index.html +13 -0
  256. package/dist/update/cli.d.ts +8 -0
  257. package/dist/update/cli.d.ts.map +1 -0
  258. package/dist/update/cli.js +115 -0
  259. package/dist/update/index.d.ts +78 -0
  260. package/dist/update/index.d.ts.map +1 -0
  261. package/dist/update/index.js +285 -0
  262. package/dist/validate/index.d.ts +6 -0
  263. package/dist/validate/index.d.ts.map +1 -0
  264. package/dist/validate/index.js +214 -0
  265. package/dist/version.d.ts +2 -0
  266. package/dist/version.d.ts.map +1 -0
  267. package/dist/version.js +1 -0
  268. package/package.json +67 -4
  269. package/templates/.gitkeep +0 -0
  270. package/templates/default-instance/instruction/.cursorrules +59 -0
  271. package/templates/default-instance/instruction/.windsurfrules +59 -0
  272. package/templates/default-instance/instruction/CLAUDE.md +57 -0
  273. package/templates/default-instance/managed/agent.md +20 -0
  274. package/templates/default-instance/managed/bootstrap.md +7 -0
  275. package/templates/default-instance/managed/index-idea.md +5 -0
  276. package/templates/default-instance/managed/routing-idea.md +14 -0
  277. package/templates/default-instance/monolith/AGENT.md +1 -0
  278. package/templates/default-instance/monolith/context/boot/HANDOFF.md +49 -0
  279. package/templates/default-instance/monolith/context/boot/WORK_SESSION.md +30 -0
  280. package/templates/default-instance/monolith/context/execution/current-state.md +30 -0
  281. package/templates/default-instance/monolith/context/maps/dependency-map.md +38 -0
  282. package/templates/default-instance/monolith/context/maps/project-map.md +85 -0
  283. package/templates/default-instance/monolith/context/maps/roadmap-index.md +41 -0
  284. package/templates/default-instance/monolith/context/maps/routing-map.md +151 -0
  285. package/templates/default-instance/monolith/context/maps/semantic-map.md +75 -0
  286. package/templates/default-instance/monolith/context/models/model-registry.md +37 -0
  287. package/templates/default-instance/monolith/context/models/privacy-classification.md +35 -0
  288. package/templates/default-instance/monolith/context/templates/plan-template.md +56 -0
  289. package/templates/default-instance/monolith/governance/README.md +29 -0
  290. package/templates/default-instance/monolith/governance/RULE_SCHEMA.md +58 -0
  291. package/templates/default-instance/monolith/learning/INDEX.md +48 -0
  292. package/templates/default-instance/monolith/learning/injection-map.md +49 -0
  293. package/templates/default-instance/monolith/learning/taxonomy.md +30 -0
  294. package/templates/default-instance/monolith/memory/errors/README.md +26 -0
  295. package/templates/default-instance/monolith/memory/errors/SCHEMA.md +47 -0
  296. package/templates/default-instance/monolith/memory/errors/TAXONOMY.md +27 -0
  297. package/templates/default-instance/monolith/memory/long-term/architecture-principles.md +31 -0
  298. package/templates/default-instance/monolith/memory/long-term/decisions.md +36 -0
  299. package/templates/default-instance/monolith/memory/long-term/execution-history.md +20 -0
  300. package/templates/default-instance/monolith/memory/long-term/tech-debt.md +25 -0
  301. package/templates/default-instance/monolith/memory/working/active.md +41 -0
  302. package/templates/default-instance/monolith/memory/working/governance-result.md +0 -0
  303. package/templates/default-instance/monolith/memory/working/integrity-result.md +0 -0
  304. package/templates/default-instance/monolith/memory/working/story-index.md +28 -0
  305. package/templates/default-instance/monolith/protocols/boot/boot-sequence.md +59 -0
  306. package/templates/default-instance/monolith/protocols/cognition/cognitive-modes.md +90 -0
  307. package/templates/default-instance/monolith/protocols/cognition/dry-run.md +56 -0
  308. package/templates/default-instance/monolith/protocols/cognition/execution-trace-format.md +52 -0
  309. package/templates/default-instance/monolith/protocols/cognition/semantic-planning.md +51 -0
  310. package/templates/default-instance/monolith/protocols/execution/hybrid-execution.md +37 -0
  311. package/templates/default-instance/monolith/protocols/execution/local-llm-governance.md +42 -0
  312. package/templates/default-instance/monolith/protocols/governance/command-policy.md +51 -0
  313. package/templates/default-instance/monolith/protocols/governance/core-invariants.md +40 -0
  314. package/templates/default-instance/monolith/protocols/governance/memory-governance.md +54 -0
  315. package/templates/default-instance/monolith/protocols/governance/metadata-schema.md +62 -0
  316. package/templates/default-instance/monolith/protocols/governance/skills.md +53 -0
  317. package/templates/default-instance/monolith/protocols/governance/work-session.md +46 -0
  318. package/templates/default-instance/monolith/skills/INDEX.md +32 -0
  319. package/templates/default-instance/monolith/skills/current-status/SKILL.md +47 -0
  320. package/templates/default-instance/monolith/skills/finalize-story/SKILL.md +86 -0
  321. package/templates/default-instance/monolith/skills/global-status/SKILL.md +56 -0
  322. package/templates/default-instance/monolith/skills/idea-capture/SKILL.md +157 -0
  323. package/templates/default-instance/monolith/skills/self-improvement/SKILL.md +77 -0
  324. package/templates/default-instance/monolith/state.json +11 -0
  325. package/templates/default-instance/monolith/tests/INDEX.md +57 -0
  326. package/templates/default-instance/monolith/tests/scenarios/architecture-review.md +32 -0
  327. package/templates/default-instance/monolith/tests/scenarios/debug-failure.md +32 -0
  328. package/templates/default-instance/monolith/tests/scenarios/dry-run-activation.md +33 -0
  329. package/templates/default-instance/monolith/tests/scenarios/execution-trace-verification.md +31 -0
  330. package/templates/default-instance/monolith/tests/scenarios/finalize-story.md +35 -0
  331. package/templates/default-instance/monolith/tests/scenarios/implement-feature.md +36 -0
  332. package/templates/default-instance/monolith/tests/scenarios/local-llm-delegation.md +31 -0
  333. package/templates/default-instance/monolith/tests/scenarios/planning-request.md +32 -0
  334. package/templates/default-instance/monolith/tests/scenarios/sensitive-context-task.md +30 -0
  335. package/templates/default-instance/monolith/tests/scenarios/validator-verification.md +33 -0
  336. package/templates/default-instance/monolith/tests/validation/expected-behavior.md +70 -0
  337. package/templates/default-instance/monolith/tests/validators/INDEX.md +17 -0
  338. package/templates/default-instance/monolith/tests/validators/delegation-semantics.md +22 -0
  339. package/templates/default-instance/monolith/tests/validators/mode-consistency.md +25 -0
  340. package/templates/default-instance/monolith/tests/validators/privacy-routing.md +23 -0
  341. package/templates/default-instance/monolith/tests/validators/protocol-references.md +27 -0
  342. package/templates/default-instance/monolith/tests/validators/routing-integrity.md +30 -0
  343. package/templates/default-instance/projectroot/AGENTS.md +111 -0
  344. package/templates/default-instance/projectroot/monolith.config.json +32 -0
  345. package/templates/default-instance/projectroot/project.definition.mjs.template +41 -0
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Generic, deterministic markdown-frontmatter discovery used for both
3
+ * Mandatory Protocols (section 7) and ADR Discovery (section 8). Reuses
4
+ * Scriptorium's existing frontmatter parser (per the brief contract) — does
5
+ * NOT use `compileMarkdownArtifact()` or the ERG graph builder, both of
6
+ * which are explicitly out of scope here.
7
+ *
8
+ * Resilience (review findings F-2/F-3): a single unreadable directory,
9
+ * unreadable file, dangling symlink, symlink cycle, or a configured
10
+ * directory that escapes the project root must never crash the whole brief
11
+ * or silently disappear. Each failure is caught, reported as a diagnostic
12
+ * (forcing Completeness: INCOMPLETE), and discovery continues with
13
+ * whatever else is readable.
14
+ *
15
+ * Symlinks (file or directory entries) are never followed while walking —
16
+ * this is what makes a symlink cycle (ELOOP) impossible by construction,
17
+ * and is also the first line of defense against using a nested symlink to
18
+ * tunnel outside a configured directory. The *top-level* directories named
19
+ * in `dirs` (i.e. `contextGateway.adrDirs` and the standard protocols
20
+ * dir) get an explicit containment check in addition: both a string-level
21
+ * check (rejects `..` segments in the configured path itself) and a
22
+ * real-path check after resolving symlinks (rejects a configured directory
23
+ * that is itself a symlink escaping the project root). Neither check alone
24
+ * is treated as sufficient — path.join()/path.relative() string comparison
25
+ * alone does not account for symlinks on disk.
26
+ *
27
+ * `monolithRoot` itself, and the fixed files below it (state.json,
28
+ * active.md), are vetted separately by the shared resolver in src/root
29
+ * (SEC-001A); this check covers document directories only.
30
+ */
31
+ import { readdirSync, existsSync, lstatSync, realpathSync } from 'fs';
32
+ import { join, relative, isAbsolute, resolve } from 'path';
33
+ import { readBoundedFileStable } from '../root/index.js';
34
+ import { parseFrontmatter } from '../scriptorium/frontmatter.js';
35
+ import { compareStrings } from './selectors.js';
36
+ /** SEC-001B.1: protocol / ADR documents (largest in this repo is ~80 KB). */
37
+ export const MAX_MARKDOWN_DOC_BYTES = 1024 * 1024;
38
+ const H1_RE = /^#\s+(.+?)\s*$/;
39
+ function errMsg(err) {
40
+ return err instanceof Error ? err.message : String(err);
41
+ }
42
+ /** True if `candidateAbs` is `baseAbs` itself or strictly inside it. */
43
+ function isContained(baseAbs, candidateAbs) {
44
+ const rel = relative(baseAbs, candidateAbs);
45
+ return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
46
+ }
47
+ /**
48
+ * Resolves `relDir` (relative to targetDir) to an absolute path, rejecting
49
+ * it — with a diagnostic, never silently — if it is not contained within
50
+ * targetDir. See the module doc comment for why both a string-level and a
51
+ * real-path (symlink-resolved) check are performed.
52
+ */
53
+ function resolveContainedDir(targetDir, relDir, diagnostics) {
54
+ const targetAbs = resolve(targetDir);
55
+ const requestedAbs = resolve(join(targetDir, relDir));
56
+ if (!isContained(targetAbs, requestedAbs)) {
57
+ diagnostics.push({
58
+ code: 'DOC_DIR_OUTSIDE_PROJECT',
59
+ severity: 'error',
60
+ message: `Configured directory "${relDir}" resolves outside the project root and was skipped.`,
61
+ });
62
+ return null;
63
+ }
64
+ // `lstat`, not `existsSync`: existsSync follows symlinks, so a DANGLING symlink would
65
+ // read as "absent" and be skipped silently (SEC-001B.1-R, M-1). Only ENOENT on the
66
+ // path itself means "not created yet"; a dangling link falls through to realpath below
67
+ // and is reported as DOC_DIR_UNREADABLE.
68
+ try {
69
+ lstatSync(requestedAbs);
70
+ }
71
+ catch (err) {
72
+ if (err.code === 'ENOENT') {
73
+ // Not an error: an ADR/protocol directory that hasn't been created yet is routine.
74
+ return requestedAbs;
75
+ }
76
+ diagnostics.push({
77
+ code: 'DOC_DIR_UNREADABLE',
78
+ severity: 'error',
79
+ message: `"${relDir}" could not be inspected: ${errMsg(err)}`,
80
+ });
81
+ return null;
82
+ }
83
+ let realRequested;
84
+ try {
85
+ realRequested = realpathSync(requestedAbs);
86
+ }
87
+ catch (err) {
88
+ diagnostics.push({
89
+ code: 'DOC_DIR_UNREADABLE',
90
+ severity: 'error',
91
+ message: `"${relDir}" could not be resolved: ${errMsg(err)}`,
92
+ });
93
+ return null;
94
+ }
95
+ let realTarget;
96
+ try {
97
+ realTarget = realpathSync(targetAbs);
98
+ }
99
+ catch {
100
+ realTarget = targetAbs;
101
+ }
102
+ if (!isContained(realTarget, realRequested)) {
103
+ diagnostics.push({
104
+ code: 'DOC_DIR_OUTSIDE_PROJECT',
105
+ severity: 'error',
106
+ message: `Configured directory "${relDir}" escapes the project root via a symlink and was skipped.`,
107
+ });
108
+ return null;
109
+ }
110
+ return requestedAbs;
111
+ }
112
+ /**
113
+ * All `.md` files under `dir`, sorted deterministically (not FS traversal
114
+ * order). Never follows symlinks — a symlinked file or directory entry is
115
+ * skipped outright, which also makes a symlink cycle harmless (it is never
116
+ * entered to begin with). Directory read failures (EACCES, ELOOP from a
117
+ * symlink that isn't itself recognized as one by readdir's dirent type,
118
+ * etc.) are reported and that subtree is skipped; sibling subtrees are
119
+ * still walked.
120
+ */
121
+ function walkMarkdownFilesSorted(dir, targetDir, diagnostics) {
122
+ const results = [];
123
+ if (!existsSync(dir))
124
+ return results;
125
+ function walk(d) {
126
+ let entries;
127
+ try {
128
+ entries = readdirSync(d, { withFileTypes: true });
129
+ }
130
+ catch (err) {
131
+ diagnostics.push({
132
+ code: 'DOC_DIR_UNREADABLE',
133
+ severity: 'error',
134
+ message: `"${relative(targetDir, d)}" could not be read: ${errMsg(err)}`,
135
+ });
136
+ return;
137
+ }
138
+ const sorted = entries.slice().sort((a, b) => compareStrings(a.name, b.name));
139
+ for (const entry of sorted) {
140
+ if (entry.isSymbolicLink())
141
+ continue; // never follow symlinks — files or directories
142
+ const full = join(d, entry.name);
143
+ if (entry.isDirectory()) {
144
+ walk(full);
145
+ }
146
+ else if (entry.isFile() && entry.name.endsWith('.md')) {
147
+ results.push(full);
148
+ }
149
+ }
150
+ }
151
+ walk(dir);
152
+ return results.sort(compareStrings);
153
+ }
154
+ function firstH1(bodyLines) {
155
+ for (const line of bodyLines) {
156
+ const match = H1_RE.exec(line.trim());
157
+ if (match)
158
+ return match[1];
159
+ }
160
+ return undefined;
161
+ }
162
+ function parseDoc(absPath, targetDir, diagnostics) {
163
+ const read = readBoundedFileStable(absPath, `"${relative(targetDir, absPath)}"`, { boundary: targetDir, maxBytes: MAX_MARKDOWN_DOC_BYTES, allowEmpty: true }, true);
164
+ if (!read.ok) {
165
+ diagnostics.push({
166
+ code: 'DOC_FILE_UNREADABLE',
167
+ severity: 'error',
168
+ message: `"${relative(targetDir, absPath)}" could not be read: ${read.code}: ${read.detail}`,
169
+ readCode: read.code,
170
+ });
171
+ return null;
172
+ }
173
+ const content = 'content' in read ? read.content : '';
174
+ const lines = content.split('\n');
175
+ const fm = parseFrontmatter(lines);
176
+ if (!fm)
177
+ return null;
178
+ const fields = new Map();
179
+ let frontmatterTitle;
180
+ let id;
181
+ for (const field of fm.fields) {
182
+ if (field.values[0] !== undefined)
183
+ fields.set(field.key, field.values[0]);
184
+ if (field.key === 'title' && field.values[0])
185
+ frontmatterTitle = field.values[0];
186
+ if (field.key === 'id' && field.values[0])
187
+ id = field.values[0];
188
+ }
189
+ const resolvedTitle = frontmatterTitle ?? firstH1(lines.slice(fm.bodyStartLine));
190
+ return {
191
+ relPath: relative(targetDir, absPath).split('\\').join('/'),
192
+ id,
193
+ resolvedTitle,
194
+ fields,
195
+ };
196
+ }
197
+ function resolveTitle(doc) {
198
+ return doc.resolvedTitle ?? doc.id ?? doc.relPath;
199
+ }
200
+ /**
201
+ * Discover documents under `dirs` (relative to targetDir, deduplicated,
202
+ * each walked recursively) whose frontmatter matches `predicate`. Only
203
+ * documents with a usable frontmatter `id:` are included — a markdown file
204
+ * without one isn't a candidate ADR/protocol record, it's silently excluded
205
+ * from this listing (not an error). `diagnostics` collects every resilience
206
+ * finding described in the module doc comment; callers that don't care can
207
+ * omit it.
208
+ */
209
+ export function discoverDocs(targetDir, dirs, predicate, diagnostics = []) {
210
+ const seenAbsDirs = new Set();
211
+ const refs = [];
212
+ for (const relDir of dirs) {
213
+ const absDir = resolveContainedDir(targetDir, relDir, diagnostics);
214
+ if (!absDir)
215
+ continue;
216
+ if (seenAbsDirs.has(absDir))
217
+ continue;
218
+ seenAbsDirs.add(absDir);
219
+ for (const absPath of walkMarkdownFilesSorted(absDir, targetDir, diagnostics)) {
220
+ const doc = parseDoc(absPath, targetDir, diagnostics);
221
+ if (!doc || !doc.id)
222
+ continue;
223
+ if (!predicate(doc.fields))
224
+ continue;
225
+ refs.push({ id: doc.id, title: resolveTitle(doc), path: doc.relPath });
226
+ }
227
+ }
228
+ return refs.sort((a, b) => compareStrings(a.id, b.id) || compareStrings(a.path, b.path));
229
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Project State Freshness (SELF-AWARENESS-001).
3
+ *
4
+ * `Completeness` says whether the brief lost data while being rendered.
5
+ * `Freshness` says whether the recorded project state can still be trusted.
6
+ * They are independent: `Completeness: COMPLETE` + `Freshness: STALE` is a
7
+ * valid combination, and nothing here ever changes `Completeness`.
8
+ *
9
+ * `evaluateFreshness()` is pure: it takes already-gathered evidence and
10
+ * returns a verdict. Git is a signal, never the only source of truth:
11
+ *
12
+ * STALE only on a concrete, named discrepancy — either the state
13
+ * documents contradict each other, or git history references
14
+ * work items that no state document mentions.
15
+ * FRESH only on positive evidence — git is readable, the last state
16
+ * update is located, and every commit after it either touches
17
+ * only the state documents or belongs to a recorded work item.
18
+ * UNKNOWN everything else. Git activity alone, an old date alone, or the
19
+ * absence of an active story alone never produce STALE, and the
20
+ * absence of an active story never means "project complete".
21
+ */
22
+ export type FreshnessStatus = 'FRESH' | 'STALE' | 'UNKNOWN';
23
+ export type FreshnessReasonCode = 'STATE_CONTRADICTION' | 'UNRECORDED_WORK' | 'GIT_UNAVAILABLE' | 'STATE_NOT_IN_GIT' | 'GIT_HISTORY_TRUNCATED' | 'UNATTRIBUTED_ACTIVITY' | 'NO_ACTIVITY_SINCE_STATE_UPDATE' | 'ACTIVITY_RECORDED';
24
+ export interface FreshnessReason {
25
+ code: FreshnessReasonCode;
26
+ message: string;
27
+ /** STATE_CONTRADICTION only: the state documents (by file name) that disagree. */
28
+ documents?: string[];
29
+ }
30
+ /** One commit after the last state update, as seen by git. */
31
+ export interface GitCommitSignal {
32
+ sha: string;
33
+ /** ISO-8601 committer date. Informational only — never compared with "now". */
34
+ date: string;
35
+ subject: string;
36
+ /** Paths changed by the commit, relative to the project directory. Empty for plain merges. */
37
+ files: string[];
38
+ }
39
+ export type GitUnavailableReason = 'GIT_NOT_INSTALLED' | 'NOT_A_GIT_REPOSITORY' | 'GIT_FAILED';
40
+ export type GitSignals = {
41
+ status: 'unavailable';
42
+ reason: GitUnavailableReason;
43
+ } | {
44
+ status: 'ok';
45
+ /** The newest commit that touched any state document; null if none ever did. */
46
+ lastStateUpdate: {
47
+ sha: string;
48
+ date: string;
49
+ } | null;
50
+ /** Commits reachable from HEAD but not from `lastStateUpdate`, newest first. */
51
+ commitsSince: GitCommitSignal[];
52
+ /** True when git history was cut at the read limit — the picture is partial. */
53
+ truncated: boolean;
54
+ };
55
+ export type StoryClaim = {
56
+ kind: 'none';
57
+ } | {
58
+ kind: 'ids';
59
+ ids: string[];
60
+ } | {
61
+ kind: 'unknown';
62
+ };
63
+ export interface FreshnessEvidence {
64
+ git: GitSignals;
65
+ /** What `active.md` says is current. `unknown` when the file/section is unreadable or says nothing parseable. */
66
+ activeStory: StoryClaim;
67
+ /** What `roadmap-index.md` says is the active story ("Active Story: ..."), or null if the line is absent. */
68
+ roadmapActiveStory: string | null;
69
+ /** Work-item status by id as declared by roadmap-index.md and story-index.md (uppercased status). */
70
+ roadmapStatuses: ReadonlyMap<string, string>;
71
+ storyIndexStatuses: ReadonlyMap<string, string>;
72
+ /** Concatenated text of the state documents that were readable; used to decide whether an id is "recorded". */
73
+ recordedText: string;
74
+ /** Repo-relative-to-project paths that are state documents (their own commits are not "activity"). */
75
+ stateFiles: ReadonlySet<string>;
76
+ }
77
+ export interface FreshnessResult {
78
+ status: FreshnessStatus;
79
+ reasons: FreshnessReason[];
80
+ /** Human-readable, single-line warnings (never fatal). */
81
+ warnings: string[];
82
+ recommendation: string;
83
+ activeStory: 'none-recorded' | 'recorded' | 'unknown';
84
+ lastStateUpdate: {
85
+ sha: string;
86
+ date: string;
87
+ } | null;
88
+ /** Commits after the last state update that touched something other than state documents. */
89
+ activityCount: number | null;
90
+ /** Work-item ids seen in git after the last state update and absent from every state document. */
91
+ unrecordedWork: string[];
92
+ }
93
+ export declare function extractWorkItemIds(text: string): string[];
94
+ export declare function mentionsId(text: string, id: string): boolean;
95
+ /** Interprets the `## Current Story` excerpt of active.md. */
96
+ export declare function parseActiveStoryClaim(excerpt: string | null): StoryClaim;
97
+ /** `Active Story: <text>` line of roadmap-index.md. */
98
+ export declare function parseRoadmapActiveStory(content: string): string | null;
99
+ /** Table rows `| id | title | STATUS | date |` of roadmap-index.md. */
100
+ export declare function parseRoadmapStatuses(content: string): Map<string, string>;
101
+ /** `### [STORY ]<id> — title` followed by `**Status:** X` in story-index.md. */
102
+ export declare function parseStoryIndexStatuses(content: string): Map<string, string>;
103
+ export declare function evaluateFreshness(ev: FreshnessEvidence): FreshnessResult;
104
+ //# sourceMappingURL=freshness.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"freshness.d.ts","sourceRoot":"","sources":["../../src/brief/freshness.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Project State Freshness (SELF-AWARENESS-001).
3
+ *
4
+ * `Completeness` says whether the brief lost data while being rendered.
5
+ * `Freshness` says whether the recorded project state can still be trusted.
6
+ * They are independent: `Completeness: COMPLETE` + `Freshness: STALE` is a
7
+ * valid combination, and nothing here ever changes `Completeness`.
8
+ *
9
+ * `evaluateFreshness()` is pure: it takes already-gathered evidence and
10
+ * returns a verdict. Git is a signal, never the only source of truth:
11
+ *
12
+ * STALE only on a concrete, named discrepancy — either the state
13
+ * documents contradict each other, or git history references
14
+ * work items that no state document mentions.
15
+ * FRESH only on positive evidence — git is readable, the last state
16
+ * update is located, and every commit after it either touches
17
+ * only the state documents or belongs to a recorded work item.
18
+ * UNKNOWN everything else. Git activity alone, an old date alone, or the
19
+ * absence of an active story alone never produce STALE, and the
20
+ * absence of an active story never means "project complete".
21
+ */
22
+ // ---------------------------------------------------------------------------
23
+ // Work-item ids
24
+ // ---------------------------------------------------------------------------
25
+ /**
26
+ * Work-item id: an upper-case, dash-separated prefix, a number, then optional
27
+ * sub-story qualifiers — UI-002D, SEC-001B.1-R, LOCK-RELEASE-001, CONTEXT-001A.
28
+ * Deliberately narrow: an id that is not matched is simply not evidence.
29
+ */
30
+ const WORK_ITEM_SOURCE = String.raw `(?:[A-Z]{2,}-)+\d{3}[A-Z]?(?:\.\d+)*(?:-[A-Z])?`;
31
+ /** Decision / lesson records are not work items. */
32
+ const NON_WORK_PREFIXES = ['ADR-', 'DEC-', 'LESSON-'];
33
+ function isWorkItem(id) {
34
+ return !NON_WORK_PREFIXES.some((p) => id.startsWith(p));
35
+ }
36
+ export function extractWorkItemIds(text) {
37
+ const re = new RegExp(`(?<![A-Za-z0-9-])${WORK_ITEM_SOURCE}(?![A-Za-z0-9]|\\.\\d|-[A-Z0-9])`, 'g');
38
+ const seen = new Set();
39
+ for (const m of text.matchAll(re)) {
40
+ if (isWorkItem(m[0]))
41
+ seen.add(m[0]);
42
+ }
43
+ return [...seen];
44
+ }
45
+ export function mentionsId(text, id) {
46
+ const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
47
+ return new RegExp(`(?<![A-Za-z0-9-])${escaped}(?![A-Za-z0-9]|\\.\\d|-[A-Z0-9])`).test(text);
48
+ }
49
+ // ---------------------------------------------------------------------------
50
+ // Parsers for the state documents (confirmed structure only)
51
+ // ---------------------------------------------------------------------------
52
+ const NONE_RE = /^\W*(?:no active story|none|unknown)\b|^\s*[—-]\s*$/i;
53
+ /** Interprets the `## Current Story` excerpt of active.md. */
54
+ export function parseActiveStoryClaim(excerpt) {
55
+ if (excerpt === null)
56
+ return { kind: 'unknown' };
57
+ const firstLine = excerpt.split('\n').find((l) => l.trim() !== '') ?? '';
58
+ if (/^\W*no active story\b/i.test(firstLine))
59
+ return { kind: 'none' };
60
+ const ids = extractWorkItemIds(excerpt);
61
+ return ids.length > 0 ? { kind: 'ids', ids } : { kind: 'unknown' };
62
+ }
63
+ /** `Active Story: <text>` line of roadmap-index.md. */
64
+ export function parseRoadmapActiveStory(content) {
65
+ const m = /^Active Story:\s*(.+?)\s*$/m.exec(content);
66
+ return m ? m[1] : null;
67
+ }
68
+ /** Table rows `| id | title | STATUS | date |` of roadmap-index.md. */
69
+ export function parseRoadmapStatuses(content) {
70
+ const out = new Map();
71
+ for (const line of content.split('\n')) {
72
+ const cells = line.split('|').map((c) => c.trim());
73
+ // leading and trailing '|' produce empty first/last cells
74
+ if (cells.length < 5 || cells[0] !== '' || cells[cells.length - 1] !== '')
75
+ continue;
76
+ const id = cells[1];
77
+ const status = cells[3];
78
+ if (!/^[A-Za-z0-9][A-Za-z0-9.\-]*$/.test(id) || !/^[A-Z][A-Z _]+$/.test(status))
79
+ continue;
80
+ if (!out.has(id))
81
+ out.set(id, status);
82
+ }
83
+ return out;
84
+ }
85
+ /** `### [STORY ]<id> — title` followed by `**Status:** X` in story-index.md. */
86
+ export function parseStoryIndexStatuses(content) {
87
+ const out = new Map();
88
+ const lines = content.split('\n');
89
+ for (let i = 0; i < lines.length; i++) {
90
+ const h = /^###\s+(?:STORY\s+)?([A-Za-z0-9][A-Za-z0-9.\-]*)\s+—/.exec(lines[i]);
91
+ if (!h)
92
+ continue;
93
+ for (let j = i + 1; j < Math.min(lines.length, i + 6); j++) {
94
+ if (/^#{1,3}\s/.test(lines[j]))
95
+ break;
96
+ const s = /^\*\*Status:\*\*\s*([A-Z][A-Z _]*)/.exec(lines[j]);
97
+ if (s) {
98
+ if (!out.has(h[1]))
99
+ out.set(h[1], s[1].trim());
100
+ break;
101
+ }
102
+ }
103
+ }
104
+ return out;
105
+ }
106
+ // ---------------------------------------------------------------------------
107
+ // Evaluation
108
+ // ---------------------------------------------------------------------------
109
+ const MAX_LISTED = 5;
110
+ function listIds(ids) {
111
+ if (ids.length <= MAX_LISTED)
112
+ return ids.join(', ');
113
+ return `${ids.slice(0, MAX_LISTED).join(', ')} and ${ids.length - MAX_LISTED} more`;
114
+ }
115
+ function isDone(status) {
116
+ return status === 'COMPLETED' || status === 'DONE';
117
+ }
118
+ function findContradictions(ev) {
119
+ const out = [];
120
+ if (ev.activeStory.kind === 'none' && ev.roadmapActiveStory !== null && !NONE_RE.test(ev.roadmapActiveStory)) {
121
+ out.push({
122
+ message: `active.md records no active story, but roadmap-index.md names "${ev.roadmapActiveStory}".`,
123
+ documents: ['active.md', 'roadmap-index.md'],
124
+ });
125
+ }
126
+ if (ev.activeStory.kind === 'ids') {
127
+ for (const id of ev.activeStory.ids) {
128
+ if (isDone(ev.roadmapStatuses.get(id))) {
129
+ out.push({
130
+ message: `active.md treats ${id} as current, but roadmap-index.md marks it ${ev.roadmapStatuses.get(id)}.`,
131
+ documents: ['active.md', 'roadmap-index.md'],
132
+ });
133
+ }
134
+ else if (isDone(ev.storyIndexStatuses.get(id))) {
135
+ out.push({
136
+ message: `active.md treats ${id} as current, but story-index.md marks it ${ev.storyIndexStatuses.get(id)}.`,
137
+ documents: ['active.md', 'story-index.md'],
138
+ });
139
+ }
140
+ }
141
+ }
142
+ for (const [id, roadmapStatus] of ev.roadmapStatuses) {
143
+ const indexStatus = ev.storyIndexStatuses.get(id);
144
+ if (indexStatus !== undefined && indexStatus !== roadmapStatus) {
145
+ out.push({
146
+ message: `${id} is ${roadmapStatus} in roadmap-index.md but ${indexStatus} in story-index.md.`,
147
+ documents: ['roadmap-index.md', 'story-index.md'],
148
+ });
149
+ }
150
+ }
151
+ return out;
152
+ }
153
+ const REVIEW_RECOMMENDATION = 'Review project state before relying on this brief.';
154
+ export function evaluateFreshness(ev) {
155
+ const reasons = [];
156
+ const warnings = [];
157
+ const base = {
158
+ activeStory: ev.activeStory.kind === 'none' ? 'none-recorded' : ev.activeStory.kind === 'ids' ? 'recorded' : 'unknown',
159
+ lastStateUpdate: ev.git.status === 'ok' ? ev.git.lastStateUpdate : null,
160
+ activityCount: null,
161
+ unrecordedWork: [],
162
+ };
163
+ // 1. Internal contradictions need no git at all.
164
+ const contradictions = findContradictions(ev);
165
+ for (const c of contradictions)
166
+ reasons.push({ code: 'STATE_CONTRADICTION', message: c.message, documents: c.documents });
167
+ // 2. Git-derived signals.
168
+ let gitVerdict = null;
169
+ if (ev.git.status === 'unavailable') {
170
+ const why = {
171
+ GIT_NOT_INSTALLED: 'git is not installed or not runnable',
172
+ NOT_A_GIT_REPOSITORY: 'the project is not inside a git repository',
173
+ GIT_FAILED: 'git could not be queried',
174
+ };
175
+ reasons.push({ code: 'GIT_UNAVAILABLE', message: `Cannot compare state with history: ${why[ev.git.reason]}.` });
176
+ gitVerdict = 'unknown';
177
+ }
178
+ else if (ev.git.lastStateUpdate === null) {
179
+ reasons.push({
180
+ code: 'STATE_NOT_IN_GIT',
181
+ message: 'No commit touches the state documents, so the last state update cannot be located.',
182
+ });
183
+ gitVerdict = 'unknown';
184
+ }
185
+ else {
186
+ const activity = ev.git.commitsSince.filter((c) => c.files.some((f) => !ev.stateFiles.has(f)));
187
+ base.activityCount = activity.length;
188
+ const referenced = new Set();
189
+ let unattributed = 0;
190
+ for (const c of activity) {
191
+ const ids = extractWorkItemIds(c.subject);
192
+ if (ids.length === 0)
193
+ unattributed++;
194
+ for (const id of ids)
195
+ referenced.add(id);
196
+ }
197
+ const unrecorded = [...referenced].filter((id) => !mentionsId(ev.recordedText, id)).sort();
198
+ base.unrecordedWork = unrecorded;
199
+ if (unrecorded.length > 0) {
200
+ reasons.push({
201
+ code: 'UNRECORDED_WORK',
202
+ message: `${activity.length} commit(s) after the last state update (${ev.git.lastStateUpdate.sha.slice(0, 7)}, ${ev.git.lastStateUpdate.date.slice(0, 10)}) ` +
203
+ `reference ${unrecorded.length} work item(s) that no state document mentions: ${listIds(unrecorded)}.`,
204
+ });
205
+ gitVerdict = 'stale';
206
+ }
207
+ else if (ev.git.truncated) {
208
+ reasons.push({
209
+ code: 'GIT_HISTORY_TRUNCATED',
210
+ message: 'Git history after the last state update was cut at the read limit; the check is partial.',
211
+ });
212
+ gitVerdict = 'unknown';
213
+ }
214
+ else if (unattributed > 0) {
215
+ reasons.push({
216
+ code: 'UNATTRIBUTED_ACTIVITY',
217
+ message: `${unattributed} commit(s) after the last state update touch project files without naming a work item; ` +
218
+ `whether the recorded state still describes them cannot be determined.`,
219
+ });
220
+ gitVerdict = 'unknown';
221
+ }
222
+ else if (activity.length === 0) {
223
+ reasons.push({
224
+ code: 'NO_ACTIVITY_SINCE_STATE_UPDATE',
225
+ message: 'No commit after the last state update changed anything beyond the state documents.',
226
+ });
227
+ gitVerdict = 'fresh';
228
+ }
229
+ else {
230
+ reasons.push({
231
+ code: 'ACTIVITY_RECORDED',
232
+ message: `All ${activity.length} commit(s) after the last state update reference work items that the state documents mention.`,
233
+ });
234
+ gitVerdict = 'fresh';
235
+ }
236
+ if (activity.length > 0 && ev.activeStory.kind === 'none') {
237
+ warnings.push('Git activity exists after the last recorded state update, but no active story is recorded.');
238
+ }
239
+ }
240
+ // 3. Combine. A contradiction or unrecorded work is concrete evidence of staleness;
241
+ // FRESH needs positive evidence and no contradiction.
242
+ let status;
243
+ if (contradictions.length > 0 || gitVerdict === 'stale')
244
+ status = 'STALE';
245
+ else if (gitVerdict === 'fresh')
246
+ status = 'FRESH';
247
+ else
248
+ status = 'UNKNOWN';
249
+ if (status !== 'FRESH') {
250
+ if (status === 'STALE')
251
+ warnings.unshift('Project state may be outdated.');
252
+ else
253
+ warnings.unshift('Freshness could not be established from the available evidence.');
254
+ if (ev.activeStory.kind === 'none') {
255
+ warnings.push('No active story is recorded; this does not mean the project is complete.');
256
+ }
257
+ else if (ev.activeStory.kind === 'unknown') {
258
+ warnings.push('The active story could not be determined from active.md.');
259
+ }
260
+ }
261
+ const recommendation = status === 'FRESH' ? 'No action needed.' : status === 'STALE' ? REVIEW_RECOMMENDATION : `${REVIEW_RECOMMENDATION} Freshness is unverified, not confirmed outdated.`;
262
+ return { status, reasons, warnings, recommendation, ...base };
263
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Read-only git signals for the freshness check (SELF-AWARENESS-001).
3
+ *
4
+ * Runs only `git log` and `git rev-parse`, never a command that writes, never
5
+ * through a shell, with config that could launch helper programs switched off.
6
+ * Any failure degrades to `{ status: 'unavailable' }` — git being absent,
7
+ * broken or refusing the directory must never fail the brief.
8
+ */
9
+ import type { GitSignals } from './freshness.js';
10
+ /** Upper bound on commits read after the last state update. Hitting it marks the result truncated. */
11
+ export declare const MAX_COMMITS_SINCE = 500;
12
+ /**
13
+ * @param targetDir project directory (git is run from here; paths are relative to it)
14
+ * @param stateFiles state documents, relative to `targetDir`. Only existing ones need be passed.
15
+ */
16
+ export declare function readGitSignals(targetDir: string, stateFiles: string[]): GitSignals;
17
+ //# sourceMappingURL=git-signals.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-signals.d.ts","sourceRoot":"","sources":["../../src/brief/git-signals.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAmB,MAAM,gBAAgB,CAAC;AAElE,sGAAsG;AACtG,eAAO,MAAM,iBAAiB,MAAM,CAAC;AAgCrC;;;GAGG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,UAAU,CAqDlF"}