@smartmemory/compose 0.3.6-beta → 0.3.8

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 (269) hide show
  1. package/.claude/skills/compose/SKILL.md +42 -88
  2. package/.compose-deps.json +1 -13
  3. package/README.md +72 -5
  4. package/bin/compose.js +754 -347
  5. package/bin/git-hooks/pre-push.template +29 -0
  6. package/bin/judgment-import.js +7 -0
  7. package/bin/judgment-migrate.js +387 -0
  8. package/contracts/comp-obs-contract.schema.json +9 -3
  9. package/contracts/feature-json.schema.json +5 -0
  10. package/contracts/fluid-record.schema.json +209 -0
  11. package/contracts/judgment-record.schema.json +425 -4
  12. package/contracts/lifecycle-backfill.schema.json +322 -0
  13. package/dist/assets/App-Z4MU-H_F.js +916 -0
  14. package/dist/assets/_baseUniq-ClWoCPFl.js +1 -0
  15. package/dist/assets/arc-DY26UIVo.js +1 -0
  16. package/dist/assets/architectureDiagram-Q4EWVU46-6Ggq4DqJ.js +36 -0
  17. package/dist/assets/blockDiagram-DXYQGD6D-CH3Ked0l.js +132 -0
  18. package/dist/assets/{browser-BSM23If2.js → browser-BWkrenen.js} +6 -6
  19. package/dist/assets/{c4Diagram-LMCZKHZV-DZf45Fbz.js → c4Diagram-AHTNJAMY-Bk8dYilu.js} +1 -1
  20. package/dist/assets/channel-SnZzzh7k.js +1 -0
  21. package/dist/assets/{chunk-JWPE2WC7-_7ujgd_Q.js → chunk-4BX2VUAB-BMR0XaAQ.js} +1 -1
  22. package/dist/assets/chunk-4TB4RGXK-JytR14a9.js +206 -0
  23. package/dist/assets/{chunk-XXDRQBXY-DfdVhbmA.js → chunk-55IACEB6-B4Q97BCP.js} +1 -1
  24. package/dist/assets/{chunk-VR4S4FIN-Dt9NZ67m.js → chunk-EDXVE4YY-R_qarkSf.js} +1 -1
  25. package/dist/assets/{chunk-5VM5RSS4-BY4_PV5H.js → chunk-FMBD7UC4-C9s7KR9m.js} +1 -1
  26. package/dist/assets/chunk-OYMX7WX6-BySQzVxc.js +231 -0
  27. package/dist/assets/{chunk-2Q5K7J3B-Dn1spZYu.js → chunk-QZHKN3VN-DdpSYZsW.js} +1 -1
  28. package/dist/assets/{chunk-32BRIVSS-pURGrJDk.js → chunk-YZCP3GAM-iE_tzriw.js} +1 -1
  29. package/dist/assets/classDiagram-6PBFFD2Q-CBu92dSH.js +1 -0
  30. package/dist/assets/classDiagram-v2-HSJHXN6E-CBu92dSH.js +1 -0
  31. package/dist/assets/clone-DgklGjHm.js +1 -0
  32. package/dist/assets/{cose-bilkent-JH36ORCC-BieYif4o.js → cose-bilkent-S5V4N54A-BdlU6ZX_.js} +1 -1
  33. package/dist/assets/dagre-KV5264BT-Cp3F5KTn.js +4 -0
  34. package/dist/assets/diagram-5BDNPKRD-DiR6_2q_.js +10 -0
  35. package/dist/assets/diagram-G4DWMVQ6-w0i-p5HX.js +24 -0
  36. package/dist/assets/diagram-MMDJMWI5-tIHhwUv3.js +43 -0
  37. package/dist/assets/diagram-TYMM5635-BAeY3B19.js +24 -0
  38. package/dist/assets/erDiagram-SMLLAGMA-Ckx_Knko.js +85 -0
  39. package/dist/assets/flowDiagram-DWJPFMVM-DeoNka6J.js +162 -0
  40. package/dist/assets/ganttDiagram-T4ZO3ILL-BmGnFbEg.js +292 -0
  41. package/dist/assets/gitGraphDiagram-UUTBAWPF-Dk48IHsx.js +106 -0
  42. package/dist/assets/graph-BNzKGvoy.js +1 -0
  43. package/dist/assets/graph-CI_1htl0.js +331 -0
  44. package/dist/assets/index-BEfrNBp8.js +123 -0
  45. package/dist/assets/index-yyrA5OZd.css +1 -0
  46. package/dist/assets/infoDiagram-42DDH7IO-BRf827i0.js +2 -0
  47. package/dist/assets/{ishikawaDiagram-FXEZZL3T-CzEB9fQS.js → ishikawaDiagram-UXIWVN3A-0kCZaeCM.js} +5 -5
  48. package/dist/assets/{journeyDiagram-5HDEW3XC-Bz8TCdz2.js → journeyDiagram-VCZTEJTY-rvU7ayRt.js} +1 -1
  49. package/dist/assets/{kanban-definition-HUTT4EX6-tozrMoV_.js → kanban-definition-6JOO6SKY-DpQwX1C5.js} +7 -7
  50. package/dist/assets/katex-DkKDou_j.js +257 -0
  51. package/dist/assets/layout-BI8cXFPI.js +1 -0
  52. package/dist/assets/{linear-Ck7gpa5N.js → linear-a0glcDiw.js} +1 -1
  53. package/dist/assets/min-vPHfnXcC.js +1 -0
  54. package/dist/assets/{mindmap-definition-LN4V7U3C-DTcHO0DJ.js → mindmap-definition-QFDTVHPH-D14eF-7C.js} +7 -7
  55. package/dist/assets/mobile-B7m9EO9D.js +17 -0
  56. package/dist/assets/pieDiagram-DEJITSTG-Cno-gETh.js +30 -0
  57. package/dist/assets/quadrantDiagram-34T5L4WZ-BUQM1Hfm.js +7 -0
  58. package/dist/assets/{requirementDiagram-TGXJPOKE-bnI2zJeT.js → requirementDiagram-MS252O5E-pOXlN2-q.js} +3 -3
  59. package/dist/assets/sankeyDiagram-XADWPNL6-Crynd3_b.js +10 -0
  60. package/dist/assets/sequenceDiagram-FGHM5R23-D9fZdCM8.js +157 -0
  61. package/dist/assets/stateDiagram-FHFEXIEX-CW9qVec8.js +1 -0
  62. package/dist/assets/stateDiagram-v2-QKLJ7IA2-DkVLzHbY.js +1 -0
  63. package/dist/assets/{timeline-definition-FHXFAJF6-D267GQFF.js → timeline-definition-GMOUNBTQ-BcHzhm_8.js} +3 -3
  64. package/dist/assets/vennDiagram-DHZGUBPP-BfytJcWk.js +34 -0
  65. package/dist/assets/wardley-RL74JXVD-DLj-IjyB.js +162 -0
  66. package/dist/assets/wardleyDiagram-NUSXRM2D-Ds0Ue68c.js +20 -0
  67. package/dist/assets/xychartDiagram-5P7HB3ND-vjWDXFL6.js +7 -0
  68. package/dist/index.html +3 -3
  69. package/lib/agent-string.js +7 -5
  70. package/lib/append-integrity.js +81 -0
  71. package/lib/backfill-evidence.js +109 -0
  72. package/lib/bug-escalation.js +39 -4
  73. package/lib/build-stream-schema.js +3 -1
  74. package/lib/build-stream-writer.js +25 -0
  75. package/lib/build.js +1624 -195
  76. package/lib/canon-guard.js +245 -0
  77. package/lib/canon-override.js +196 -0
  78. package/lib/canon-registry.js +291 -0
  79. package/lib/cli-commands.js +144 -0
  80. package/lib/codex-preflight.js +50 -15
  81. package/lib/colleague/context.js +215 -0
  82. package/lib/colleague/writeback.js +95 -0
  83. package/lib/completion-gate.js +1421 -0
  84. package/lib/completion-writer.js +47 -47
  85. package/lib/consumer-fanout.js +105 -11
  86. package/lib/coverage-gate.js +200 -0
  87. package/lib/dir-lock.js +170 -0
  88. package/lib/dispatch-ledger.js +301 -0
  89. package/lib/dispatch-metrics.js +236 -0
  90. package/lib/experiment-judge.js +6 -1
  91. package/lib/feature-json.js +1 -1
  92. package/lib/feature-reconciler.js +8 -0
  93. package/lib/feature-validator.js +64 -1
  94. package/lib/feature-writer.js +66 -2
  95. package/lib/fluid/factory.js +167 -0
  96. package/lib/fluid/ideabox-dates.js +73 -0
  97. package/lib/fluid/ideabox-migrate.js +154 -0
  98. package/lib/fluid/ideabox-ops.js +585 -0
  99. package/lib/fluid/ideabox-view.js +146 -0
  100. package/lib/fluid/import-ideabox.js +186 -0
  101. package/lib/fluid/local-provider.js +606 -0
  102. package/lib/fluid/provider.js +684 -0
  103. package/lib/fluid/record-shape.js +214 -0
  104. package/lib/fluid/record-store.js +328 -0
  105. package/lib/fluid/render-ideabox.js +261 -0
  106. package/lib/fluid/schema.js +40 -0
  107. package/lib/fluid/smartmemory-provider.js +1695 -0
  108. package/lib/gsd.js +63 -14
  109. package/lib/guard-cli.js +175 -0
  110. package/lib/guard-custody.js +141 -0
  111. package/lib/guard-descriptors.js +530 -0
  112. package/lib/guard-enrol.js +254 -0
  113. package/lib/health-score.js +1 -1
  114. package/lib/hooks-status.js +32 -3
  115. package/lib/ideabox-cli.js +315 -0
  116. package/lib/ideabox.js +121 -21
  117. package/lib/judgment/store/index.js +166 -0
  118. package/lib/judgment/store/records.js +184 -25
  119. package/lib/judgment/trace.js +380 -0
  120. package/lib/judgment-attest.js +259 -0
  121. package/lib/judgment-decision-write.js +277 -0
  122. package/lib/judgment-decisions.js +466 -0
  123. package/lib/judgment-gen.js +375 -22
  124. package/lib/judgment-verify.js +153 -0
  125. package/lib/judgment-writer.js +2842 -262
  126. package/lib/lane-gate.js +2 -0
  127. package/lib/lifecycle-modes.js +4 -4
  128. package/lib/lineage.js +400 -0
  129. package/lib/local-claude-connector.js +250 -54
  130. package/lib/maya-client.js +302 -0
  131. package/lib/maya-config.js +53 -0
  132. package/lib/maya-identity.js +283 -0
  133. package/lib/mcp-enforcement.js +21 -35
  134. package/lib/migrate-anon.js +5 -0
  135. package/lib/migrate-roadmap.js +15 -0
  136. package/lib/new.js +13 -1
  137. package/lib/pipeline-compat.js +104 -0
  138. package/lib/policy-catalog.js +295 -0
  139. package/lib/policy-check.js +0 -0
  140. package/lib/process-termination.js +98 -0
  141. package/lib/resolve-workspace.js +5 -1
  142. package/lib/result-normalizer.js +428 -153
  143. package/lib/review-normalize.js +4 -0
  144. package/lib/roadmap-errors.js +65 -0
  145. package/lib/roadmap-preservers.js +24 -4
  146. package/lib/roadmap-residue.js +299 -0
  147. package/lib/smartmemory-client.js +614 -78
  148. package/lib/smartmemory-config.js +54 -0
  149. package/lib/smartmemory-ingest.js +19 -2
  150. package/lib/step-prompt.js +7 -6
  151. package/lib/stratum-engine.js +53 -4
  152. package/lib/stratum-mcp-client.js +391 -31
  153. package/lib/test-bootstrap.js +31 -0
  154. package/lib/tool-inventory.js +122 -0
  155. package/lib/version-check.js +91 -19
  156. package/lib/vision-writer.js +88 -1
  157. package/package.json +7 -6
  158. package/pipelines/bug-fix.stratum.yaml +205 -211
  159. package/pipelines/build-quick.profiles.json +12 -0
  160. package/pipelines/build-quick.stratum.yaml +263 -350
  161. package/pipelines/content.stratum.yaml +81 -77
  162. package/pipelines/coverage-sweep.stratum.yaml +49 -30
  163. package/pipelines/plan.stratum.yaml +76 -86
  164. package/pipelines/refactor.stratum.yaml +125 -125
  165. package/pipelines/research.stratum.yaml +56 -58
  166. package/pipelines/review-fix.profiles.json +6 -0
  167. package/pipelines/review-fix.stratum.yaml +110 -83
  168. package/presets/team-feature.profiles.json +6 -0
  169. package/presets/team-feature.stratum.yaml +93 -66
  170. package/presets/team-research.profiles.json +6 -0
  171. package/presets/team-research.stratum.yaml +89 -80
  172. package/presets/team-review.profiles.json +8 -0
  173. package/presets/team-review.stratum.yaml +98 -80
  174. package/scripts/cost-census.mjs +70 -0
  175. package/scripts/guard-sign/compose-guard-sign.sh +62 -0
  176. package/server/agent-health.js +22 -0
  177. package/server/agent-hooks.js +14 -1
  178. package/server/agent-server.js +5 -248
  179. package/server/agent-spawn.js +3 -4
  180. package/server/agent-workspace.js +294 -0
  181. package/server/build-routes.js +6 -5
  182. package/server/build-stream-bridge.js +53 -0
  183. package/server/cc-session-watcher.js +4 -1
  184. package/server/coalescing-buffer.js +7 -1
  185. package/server/completion-projection.js +228 -0
  186. package/server/compose-mcp-tools.js +124 -24
  187. package/server/compose-mcp.js +91 -790
  188. package/server/decision-event-emit.js +41 -2
  189. package/server/decision-event-id.js +17 -0
  190. package/server/decision-events-snapshot.js +3 -0
  191. package/server/design-routes.js +14 -8
  192. package/server/feature-scan.js +76 -2
  193. package/server/file-watcher.js +170 -21
  194. package/server/ideabox-routes.js +166 -224
  195. package/server/index.js +70 -100
  196. package/server/lifecycle-guard.js +240 -10
  197. package/server/lifecycle-phase-history.js +276 -0
  198. package/server/maya-routes.js +507 -0
  199. package/server/mcp-tool-defs.js +940 -0
  200. package/server/mcp-tool-policy.js +35 -3
  201. package/server/model-tiers.js +22 -5
  202. package/server/pipeline-routes.js +21 -11
  203. package/server/project-root.js +58 -19
  204. package/server/remote-utils.js +3 -1
  205. package/server/schema-validator.js +7 -1
  206. package/server/session-manager.js +5 -6
  207. package/server/session-routes.js +3 -1
  208. package/server/stratum-client.js +57 -10
  209. package/server/stratum-sync.js +6 -3
  210. package/server/summarizer.js +3 -4
  211. package/server/supervisor.js +0 -1
  212. package/server/vision-routes.js +208 -98
  213. package/server/vision-server.js +86 -23
  214. package/server/vision-store.js +60 -6
  215. package/server/vision-utils.js +3 -4
  216. package/server/workspace-activity.js +18 -0
  217. package/server/workspace-middleware.js +2 -2
  218. package/server/workspace-runtime.js +243 -0
  219. package/server/worktree-gc.js +1 -0
  220. package/dist/assets/App-BG3ngu8H.js +0 -896
  221. package/dist/assets/abnfDiagram-VRR7QNED-CjB_sD3D.js +0 -1
  222. package/dist/assets/arc-_v4hR_uD.js +0 -1
  223. package/dist/assets/architectureDiagram-ZJ3FMSHR-DreJmzXQ.js +0 -36
  224. package/dist/assets/blockDiagram-677ZJIJ3-BG9-c0O1.js +0 -132
  225. package/dist/assets/channel-B3U5wFAT.js +0 -1
  226. package/dist/assets/chunk-EX3LRPZG-DdELs1qP.js +0 -231
  227. package/dist/assets/chunk-MOJQB5TN-D-ky35G-.js +0 -88
  228. package/dist/assets/chunk-RYQCIY6F-Dag_kVlO.js +0 -1
  229. package/dist/assets/chunk-V7JOEXUC-BtewURat.js +0 -206
  230. package/dist/assets/classDiagram-OUVF2IWQ-B6fCN-ht.js +0 -1
  231. package/dist/assets/classDiagram-v2-EOCWNBFH-B6fCN-ht.js +0 -1
  232. package/dist/assets/cynefin-VYW2F7L2-CT2BA6KE.js +0 -178
  233. package/dist/assets/cynefinDiagram-TSTJHNR4-Bh6exbyg.js +0 -62
  234. package/dist/assets/dagre-VKFMJZFB-aXMLSmQL.js +0 -4
  235. package/dist/assets/diagram-FQU43EPY-Dr7JAOuQ.js +0 -3
  236. package/dist/assets/diagram-G47NLZAW-DUvA3FQK.js +0 -24
  237. package/dist/assets/diagram-NH7WQ7WH-BQUARqcu.js +0 -24
  238. package/dist/assets/diagram-OA4YK3LP-dDUc1zHi.js +0 -30
  239. package/dist/assets/diagram-WEI45ONY-B2h5Qlb1.js +0 -41
  240. package/dist/assets/ebnfDiagram-CCIWWBDH-DThRGupB.js +0 -1
  241. package/dist/assets/erDiagram-Q63AITRT-BUCsprO2.js +0 -85
  242. package/dist/assets/flowDiagram-23GEKE2U-DXtNNi6r.js +0 -156
  243. package/dist/assets/ganttDiagram-NO4QXBWP-D4zbBHh_.js +0 -292
  244. package/dist/assets/gitGraphDiagram-IHSO6WYX-DpoQws0W.js +0 -106
  245. package/dist/assets/graph-BXPQrYYB.js +0 -331
  246. package/dist/assets/graph-C9eacEi8.js +0 -1
  247. package/dist/assets/index-3ZH5eMcZ.js +0 -119
  248. package/dist/assets/index-LIwREYgH.css +0 -1
  249. package/dist/assets/infoDiagram-FWYZ7A6U-Bbas2GAo.js +0 -2
  250. package/dist/assets/katex-C5jXJg4s.js +0 -257
  251. package/dist/assets/layout-DEXfKzaS.js +0 -1
  252. package/dist/assets/map-Czzmt4hB.js +0 -1
  253. package/dist/assets/mobile-CaoXUwAr.js +0 -17
  254. package/dist/assets/pegDiagram-2B236MQR-CHiINrNy.js +0 -1
  255. package/dist/assets/pieDiagram-ENE6RG2P-CfS4YFlR.js +0 -39
  256. package/dist/assets/quadrantDiagram-ABIIQ3AL-CadesS9w.js +0 -7
  257. package/dist/assets/railroadDiagram-RFXS5EU6-CgWEspBN.js +0 -1
  258. package/dist/assets/sankeyDiagram-HTMAVEWB-YWKFgOGw.js +0 -40
  259. package/dist/assets/sequenceDiagram-DBY2YBRQ-BvkNOyF9.js +0 -162
  260. package/dist/assets/sizeCapture-X5ZJPWSS-DlFPA2yO.js +0 -1
  261. package/dist/assets/stateDiagram-2N3HPSRC-h8NIx0kQ.js +0 -1
  262. package/dist/assets/stateDiagram-v2-6OUMAXLB-DjPgZtJ9.js +0 -1
  263. package/dist/assets/swimlanes-5IMT3BWC-CT5n22kG.js +0 -2
  264. package/dist/assets/swimlanesDiagram-G3AALYLV-Dn318Bhq.js +0 -8
  265. package/dist/assets/vennDiagram-L72KCM5P-Dj-wWLYG.js +0 -34
  266. package/dist/assets/wardleyDiagram-EHGQE667-BxCeYxkG.js +0 -78
  267. package/dist/assets/xychartDiagram-FW5EYKEG-DMFqWn7z.js +0 -7
  268. package/lib/staleness.js +0 -87
  269. package/server/ideabox-cache.js +0 -77
package/lib/lane-gate.js CHANGED
@@ -87,6 +87,7 @@ export async function applyFrontTriage({ featureCode, request, provider, cachedF
87
87
  triageTier: tier,
88
88
  lane,
89
89
  estimateSource,
90
+ triageConfidence: front.confidence,
90
91
  profile: buildProfile,
91
92
  triageTimestamp: new Date().toISOString(),
92
93
  };
@@ -109,6 +110,7 @@ export async function applyFrontTriage({ featureCode, request, provider, cachedF
109
110
  buildProfile,
110
111
  tier,
111
112
  lane,
113
+ confidence: front.confidence,
112
114
  tierLabel: fields.complexity,
113
115
  rationale: front.rationale,
114
116
  cachedFeature: updated,
@@ -51,7 +51,7 @@ export const LIFECYCLE_MODES = {
51
51
  ship: [],
52
52
  },
53
53
  skippable: ['prd', 'architecture', 'report'],
54
- terminal: ['complete', 'killed'],
54
+ terminal: ['complete', 'killed', 'complete_backfilled'],
55
55
  genesis: 'explore_design',
56
56
  completablePhase: 'ship', // the phase that → complete (was hard-coded by name)
57
57
  phaseArtifacts: ['design.md', 'prd.md', 'architecture.md', 'blueprint.md', 'plan.md', 'report.md'],
@@ -92,7 +92,7 @@ export const LIFECYCLE_MODES = {
92
92
  ship: [],
93
93
  },
94
94
  skippable: [],
95
- terminal: ['complete', 'killed'],
95
+ terminal: ['complete', 'killed', 'complete_backfilled'],
96
96
  genesis: 'reproduce',
97
97
  completablePhase: 'ship',
98
98
  phaseArtifacts: [], // empty → assess uses the global default (preserves today's bug behavior)
@@ -121,7 +121,7 @@ export const LIFECYCLE_MODES = {
121
121
  ship: [],
122
122
  },
123
123
  skippable: [],
124
- terminal: ['complete', 'killed'],
124
+ terminal: ['complete', 'killed', 'complete_backfilled'],
125
125
  genesis: 'explore_design',
126
126
  completablePhase: 'ship',
127
127
  phaseArtifacts: ['design.md', 'plan.md'],
@@ -163,7 +163,7 @@ export const LIFECYCLE_MODES = {
163
163
  inconclusive: ['under_test', 'open'],
164
164
  },
165
165
  skippable: [],
166
- terminal: ['superseded', 'dissolved', 'complete', 'killed'],
166
+ terminal: ['superseded', 'dissolved', 'complete', 'killed', 'complete_backfilled'],
167
167
  genesis: 'open',
168
168
  completablePhase: 'resolved',
169
169
  phaseArtifacts: [],
package/lib/lineage.js ADDED
@@ -0,0 +1,400 @@
1
+ /**
2
+ * lineage.js — COMP-PROV-LINEAGE: W3C PROV-O artifact lineage (vocabulary only).
3
+ *
4
+ * Compose artifacts form a derivation chain — design.md produces blueprint.md
5
+ * produces plan.md produces report.md — but nothing recorded it, so when an
6
+ * upstream artifact changes, downstream artifacts went stale silently.
7
+ *
8
+ * We borrow the W3C PROV-O vocabulary (https://www.w3.org/TR/prov-o/), NOT the
9
+ * RDF stack. See docs/features/COMP-PROV-LINEAGE/prov-o-mapping.md for the full
10
+ * term mapping. In Compose terms:
11
+ *
12
+ * prov:Entity → a lifecycle artifact (design.md, blueprint.md, ...)
13
+ * prov:Activity → a lifecycle phase (explore_design, blueprint, ...)
14
+ * prov:wasGeneratedBy → the phase that produced this artifact (Entity → Activity)
15
+ * prov:wasDerivedFrom → the upstream artifact(s) this was built on (Entity → Entity)
16
+ *
17
+ * `wasDerivedFrom` is the load-bearing edge: staleness becomes a graph
18
+ * reachability query over it rather than bespoke logic.
19
+ *
20
+ * Storage: the lineage lives as HTML-comment markers in the first lines of each
21
+ * artifact (COMP-PROV-LINEAGE decision, embedded markers over a feature.json
22
+ * block — canonical artifacts stay auto-discovered, never registered):
23
+ *
24
+ * <!-- wasGeneratedBy: blueprint -->
25
+ * <!-- wasDerivedFrom: design.md -->
26
+ *
27
+ * The reachability query does NOT require the markers to have been stamped: for
28
+ * canonical artifacts it falls back to CANONICAL_CHAIN, so it works on any
29
+ * feature folder today. Markers only override the default derivation.
30
+ *
31
+ * Explicitly out of scope: RDF runtime, triple store, SPARQL, JSON-LD. A
32
+ * JSON-LD export stays possible later by walking these same edges.
33
+ */
34
+
35
+ import { readFileSync, writeFileSync, existsSync, statSync, utimesSync } from 'node:fs';
36
+ import { join } from 'node:path';
37
+ import { artifactsOf, edgeEvidenceOf } from './lifecycle-modes.js';
38
+
39
+ /**
40
+ * Canonical derivation chain for the `build` lifecycle, in derivation order.
41
+ * Each entry is { file, phase } where `phase` is the prov:Activity that
42
+ * generates the prov:Entity `file`.
43
+ *
44
+ * Ordered so that entry N wasDerivedFrom the nearest EXISTING predecessor
45
+ * (predecessors may be skipped — prd/architecture/report are skippable phases).
46
+ *
47
+ * Mirrors lib/lifecycle-modes.js build mode. A test
48
+ * (test/lineage.test.js) asserts this stays consistent with
49
+ * `artifactsOf('build')` / `edgeEvidenceOf('build')` so the two never drift.
50
+ */
51
+ export const CANONICAL_CHAIN = [
52
+ { file: 'design.md', phase: 'explore_design' },
53
+ { file: 'prd.md', phase: 'prd' },
54
+ { file: 'architecture.md', phase: 'architecture' },
55
+ { file: 'blueprint.md', phase: 'blueprint' },
56
+ { file: 'plan.md', phase: 'plan' },
57
+ { file: 'report.md', phase: 'report' },
58
+ ];
59
+
60
+ const CHAIN_FILES = CANONICAL_CHAIN.map((e) => e.file);
61
+ const PHASE_BY_FILE = new Map(CANONICAL_CHAIN.map((e) => [e.file, e.phase]));
62
+
63
+ /**
64
+ * The prov:Activity (phase) that generates a canonical artifact, or null if the
65
+ * file is not a known canonical artifact.
66
+ * @param {string} filename
67
+ * @returns {string|null}
68
+ */
69
+ export function generatingPhaseOf(filename) {
70
+ return PHASE_BY_FILE.get(filename) ?? null;
71
+ }
72
+
73
+ /**
74
+ * Extract PROV-O lineage markers from the first lines of artifact text.
75
+ * Reads only the header region (first 8 lines) — a cheap header scan.
76
+ *
77
+ * @param {string} content
78
+ * @returns {{ wasGeneratedBy: string|null, wasDerivedFrom: string[] }}
79
+ */
80
+ export function extractLineageMarkers(content) {
81
+ const lines = content.split('\n').slice(0, 8);
82
+ let wasGeneratedBy = null;
83
+ let wasDerivedFrom = null; // null = no usable marker present
84
+ for (const line of lines) {
85
+ // Anchored to a full line (optional surrounding whitespace) so a marker must
86
+ // BE the line, not merely appear inside prose or a fenced code example.
87
+ const g = line.match(/^\s*<!--\s*wasGeneratedBy:\s*([\w_-]+)\s*-->\s*$/);
88
+ if (g) wasGeneratedBy = g[1];
89
+ const d = line.match(/^\s*<!--\s*wasDerivedFrom:\s*([^>]*?)\s*-->\s*$/);
90
+ if (d) {
91
+ const parents = d[1]
92
+ .split(',')
93
+ .map((s) => s.trim())
94
+ // Parents are sibling artifact filenames — never a path. Drop anything
95
+ // with a separator or traversal so a hand-edited marker cannot point the
96
+ // derivation graph outside the feature folder.
97
+ .filter((s) => s && !s.includes('/') && !s.includes('\\') && !s.includes('..'));
98
+ // An empty marker (`<!-- wasDerivedFrom: -->`) is treated as no override
99
+ // rather than an explicit "[]" state — Compose has no mid-chain
100
+ // origin-override use case, and the phantom [] broke stamp idempotency.
101
+ wasDerivedFrom = parents.length > 0 ? parents : null;
102
+ }
103
+ }
104
+ return { wasGeneratedBy, wasDerivedFrom };
105
+ }
106
+
107
+ /**
108
+ * Compute the canonical lineage for an artifact filename given which sibling
109
+ * artifacts actually exist in the feature folder. `wasDerivedFrom` is the
110
+ * nearest EXISTING predecessor in CANONICAL_CHAIN (so skipped phases don't
111
+ * break the edge); the first artifact in the chain derives from nothing.
112
+ *
113
+ * @param {string} filename
114
+ * @param {string[]} existingFiles filenames present in the feature folder
115
+ * @returns {{ entity: string, wasGeneratedBy: string|null, wasDerivedFrom: string[] }}
116
+ */
117
+ export function canonicalLineageOf(filename, existingFiles) {
118
+ const idx = CHAIN_FILES.indexOf(filename);
119
+ if (idx === -1) {
120
+ return { entity: filename, wasGeneratedBy: null, wasDerivedFrom: [] };
121
+ }
122
+ const present = new Set(existingFiles);
123
+ let wasDerivedFrom = [];
124
+ for (let i = idx - 1; i >= 0; i--) {
125
+ if (present.has(CHAIN_FILES[i])) {
126
+ wasDerivedFrom = [CHAIN_FILES[i]];
127
+ break;
128
+ }
129
+ }
130
+ return {
131
+ entity: filename,
132
+ wasGeneratedBy: PHASE_BY_FILE.get(filename) ?? null,
133
+ wasDerivedFrom,
134
+ };
135
+ }
136
+
137
+ /**
138
+ * Resolve the effective lineage for an on-disk artifact: markers override the
139
+ * canonical chain. Returns null if the file does not exist.
140
+ *
141
+ * @param {string} featureDir
142
+ * @param {string} filename
143
+ * @param {string[]} [existingFiles] optional precomputed sibling list
144
+ * @returns {{ entity: string, wasGeneratedBy: string|null, wasDerivedFrom: string[], source: 'marker'|'canonical' }|null}
145
+ */
146
+ export function lineageOf(featureDir, filename, existingFiles) {
147
+ const filePath = join(featureDir, filename);
148
+ if (!existsSync(filePath)) return null;
149
+ const siblings = existingFiles ?? CHAIN_FILES.filter((f) => existsSync(join(featureDir, f)));
150
+
151
+ let content;
152
+ try {
153
+ content = readFileSync(filePath, 'utf-8');
154
+ } catch {
155
+ content = '';
156
+ }
157
+ const markers = extractLineageMarkers(content);
158
+ const canonical = canonicalLineageOf(filename, siblings);
159
+
160
+ // Markers override; fall back to canonical per-field.
161
+ const usesMarker = markers.wasGeneratedBy !== null || markers.wasDerivedFrom !== null;
162
+ return {
163
+ entity: filename,
164
+ wasGeneratedBy: markers.wasGeneratedBy ?? canonical.wasGeneratedBy,
165
+ wasDerivedFrom: markers.wasDerivedFrom ?? canonical.wasDerivedFrom,
166
+ source: usesMarker ? 'marker' : 'canonical',
167
+ };
168
+ }
169
+
170
+ /**
171
+ * Build the derivation graph (child -> parents) for the canonical artifacts that
172
+ * exist in a feature folder, honouring marker overrides.
173
+ *
174
+ * @param {string} featureDir
175
+ * @returns {Map<string, string[]>} filename -> its wasDerivedFrom parents
176
+ */
177
+ export function buildDerivationGraph(featureDir) {
178
+ const existing = CHAIN_FILES.filter((f) => existsSync(join(featureDir, f)));
179
+ const graph = new Map();
180
+ for (const file of existing) {
181
+ const lin = lineageOf(featureDir, file, existing);
182
+ graph.set(file, lin ? lin.wasDerivedFrom : []);
183
+ }
184
+ return graph;
185
+ }
186
+
187
+ /**
188
+ * Reachability query — the core deliverable.
189
+ *
190
+ * Given an artifact that changed, return the downstream artifacts (transitive
191
+ * descendants via wasDerivedFrom) that are now STALE: they exist, they descend
192
+ * from the changed artifact, and their last-modified time is older than the
193
+ * changed artifact's (they were derived before the upstream changed).
194
+ *
195
+ * @param {string} featureDir
196
+ * @param {string} changedFile filename of the artifact that changed
197
+ * @returns {Array<{ file: string, stale: boolean, derivedFromChanged: boolean, mtimeMs: number }>}
198
+ * descendants of changedFile, stale ones first
199
+ */
200
+ export function findStaleDescendants(featureDir, changedFile) {
201
+ const graph = buildDerivationGraph(featureDir); // child -> parents
202
+
203
+ // Invert to parent -> children for forward reachability.
204
+ const children = new Map();
205
+ for (const [child, parents] of graph) {
206
+ for (const parent of parents) {
207
+ if (!children.has(parent)) children.set(parent, []);
208
+ children.get(parent).push(child);
209
+ }
210
+ }
211
+
212
+ // BFS forward from changedFile to collect all transitive descendants.
213
+ const descendants = new Set();
214
+ const queue = [...(children.get(changedFile) ?? [])];
215
+ while (queue.length > 0) {
216
+ const node = queue.shift();
217
+ if (descendants.has(node)) continue;
218
+ descendants.add(node);
219
+ for (const c of children.get(node) ?? []) queue.push(c);
220
+ }
221
+
222
+ const changedPath = join(featureDir, changedFile);
223
+ let changedMtime = 0;
224
+ try {
225
+ changedMtime = statSync(changedPath).mtimeMs;
226
+ } catch {
227
+ return []; // changed file gone — nothing to compare against
228
+ }
229
+
230
+ const results = [];
231
+ for (const file of descendants) {
232
+ let mtimeMs = 0;
233
+ try {
234
+ mtimeMs = statSync(join(featureDir, file)).mtimeMs;
235
+ } catch {
236
+ continue;
237
+ }
238
+ results.push({
239
+ file,
240
+ stale: mtimeMs < changedMtime,
241
+ derivedFromChanged: true,
242
+ mtimeMs,
243
+ });
244
+ }
245
+ // Stale first, then by chain order for stable output.
246
+ results.sort((a, b) => {
247
+ if (a.stale !== b.stale) return a.stale ? -1 : 1;
248
+ return CHAIN_FILES.indexOf(a.file) - CHAIN_FILES.indexOf(b.file);
249
+ });
250
+ return results;
251
+ }
252
+
253
+ /**
254
+ * Global staleness scan — the changed-file-free form of the reachability query,
255
+ * for callers (the build health signal) that want "which artifacts are stale?"
256
+ * without a specific changed anchor.
257
+ *
258
+ * An artifact is stale when any of its transitive ancestors (via wasDerivedFrom)
259
+ * has a newer mtime than the artifact itself — i.e. an upstream it was built on
260
+ * has changed since. Returns one row per existing canonical artifact, shape
261
+ * `{ file, stale, staleAgainst }`, compatible with scoreDocFreshness (which only
262
+ * reads `.stale`).
263
+ *
264
+ * @param {string} featureDir
265
+ * @returns {Array<{ file: string, stale: boolean, staleAgainst: string[] }>}
266
+ */
267
+ export function findStaleArtifacts(featureDir) {
268
+ const graph = buildDerivationGraph(featureDir); // child -> parents
269
+
270
+ const mtime = new Map();
271
+ for (const file of graph.keys()) {
272
+ try {
273
+ mtime.set(file, statSync(join(featureDir, file)).mtimeMs);
274
+ } catch { /* unreadable — omit from comparison */ }
275
+ }
276
+
277
+ const results = [];
278
+ for (const file of graph.keys()) {
279
+ if (!mtime.has(file)) continue;
280
+
281
+ // Collect transitive ancestors (cycle-safe).
282
+ const ancestors = new Set();
283
+ const queue = [...(graph.get(file) ?? [])];
284
+ while (queue.length > 0) {
285
+ const p = queue.shift();
286
+ if (ancestors.has(p)) continue;
287
+ ancestors.add(p);
288
+ for (const gp of graph.get(p) ?? []) queue.push(gp);
289
+ }
290
+
291
+ const staleAgainst = [];
292
+ for (const anc of ancestors) {
293
+ if (mtime.has(anc) && mtime.get(anc) > mtime.get(file)) staleAgainst.push(anc);
294
+ }
295
+ results.push({ file, stale: staleAgainst.length > 0, staleAgainst });
296
+ }
297
+
298
+ results.sort((a, b) => CHAIN_FILES.indexOf(a.file) - CHAIN_FILES.indexOf(b.file));
299
+ return results;
300
+ }
301
+
302
+ /**
303
+ * Render the PROV-O marker header lines for an artifact's lineage.
304
+ * @param {{ wasGeneratedBy: string|null, wasDerivedFrom: string[] }} lineage
305
+ * @returns {string[]} marker lines (may be empty)
306
+ */
307
+ function renderMarkers(lineage) {
308
+ const out = [];
309
+ if (lineage.wasGeneratedBy) out.push(`<!-- wasGeneratedBy: ${lineage.wasGeneratedBy} -->`);
310
+ if (lineage.wasDerivedFrom && lineage.wasDerivedFrom.length > 0) {
311
+ out.push(`<!-- wasDerivedFrom: ${lineage.wasDerivedFrom.join(', ')} -->`);
312
+ }
313
+ return out;
314
+ }
315
+
316
+ const MARKER_RE = /^<!--\s*(?:wasGeneratedBy|wasDerivedFrom):[^>]*-->\s*$/;
317
+
318
+ /**
319
+ * Stamp PROV-O lineage markers into an artifact's content, idempotently.
320
+ * Existing wasGeneratedBy/wasDerivedFrom marker lines are removed and replaced,
321
+ * so re-stamping never duplicates. A leading `<!-- phase: -->` marker, if any
322
+ * artifact still carries one, is left untouched and lineage is inserted after it.
323
+ * Returns the new content (unchanged if lineage is empty and no markers present).
324
+ *
325
+ * Pure string transform — does not touch the filesystem. Callers persist.
326
+ *
327
+ * @param {string} content
328
+ * @param {{ wasGeneratedBy: string|null, wasDerivedFrom: string[] }} lineage
329
+ * @returns {string}
330
+ */
331
+ export function stampLineageContent(content, lineage) {
332
+ const markers = renderMarkers(lineage);
333
+ const lines = content.split('\n');
334
+
335
+ // Remove any existing lineage markers (anywhere in the header region or body —
336
+ // they are only ever ours), preserving everything else.
337
+ const kept = lines.filter((line) => !MARKER_RE.test(line.trim()));
338
+
339
+ if (markers.length === 0) return kept.join('\n');
340
+
341
+ // Insert markers directly after a leading `<!-- phase: -->` marker if present,
342
+ // otherwise at the very top, so all lineage lives together in the header.
343
+ let insertAt = 0;
344
+ if (kept[0] && /^<!--\s*phase:/.test(kept[0].trim())) insertAt = 1;
345
+ kept.splice(insertAt, 0, ...markers);
346
+ return kept.join('\n');
347
+ }
348
+
349
+ /**
350
+ * Stamp PROV-O lineage markers onto every existing canonical artifact in a
351
+ * feature folder, on disk. Effective lineage comes from `lineageOf`, so any
352
+ * hand-authored marker override is preserved (re-written identically, no diff).
353
+ * Idempotent: running twice makes no second change.
354
+ *
355
+ * This is the "lifecycle writer" surface — the point at which lineage markers
356
+ * are materialised into artifacts during the lifecycle (invoked by
357
+ * `compose lineage stamp`).
358
+ *
359
+ * @param {string} featureDir
360
+ * @returns {Array<{ file: string, changed: boolean, wasGeneratedBy: string|null, wasDerivedFrom: string[] }>}
361
+ */
362
+ export function stampFeatureLineage(featureDir) {
363
+ const existing = CHAIN_FILES.filter((f) => existsSync(join(featureDir, f)));
364
+ const out = [];
365
+ for (const file of existing) {
366
+ const filePath = join(featureDir, file);
367
+ let content;
368
+ try {
369
+ content = readFileSync(filePath, 'utf-8');
370
+ } catch {
371
+ continue;
372
+ }
373
+ const lin = lineageOf(featureDir, file, existing);
374
+ const next = stampLineageContent(content, lin);
375
+ const changed = next !== content;
376
+ if (changed) {
377
+ // Stamping lineage markers is a metadata annotation, NOT a regeneration of
378
+ // the artifact — it must not reset the mtime that findStaleDescendants uses
379
+ // as the derivation clock, or a retrofit stamp would erase existing
380
+ // staleness. Preserve the original mtime across the write.
381
+ let mtime = null;
382
+ try {
383
+ mtime = statSync(filePath).mtime;
384
+ } catch { /* no prior stat — leave mtime as written */ }
385
+ writeFileSync(filePath, next);
386
+ if (mtime) {
387
+ try {
388
+ utimesSync(filePath, mtime, mtime);
389
+ } catch { /* best effort */ }
390
+ }
391
+ }
392
+ out.push({
393
+ file,
394
+ changed,
395
+ wasGeneratedBy: lin.wasGeneratedBy,
396
+ wasDerivedFrom: lin.wasDerivedFrom,
397
+ });
398
+ }
399
+ return out;
400
+ }