@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.
- package/.claude/skills/compose/SKILL.md +42 -88
- package/.compose-deps.json +1 -13
- package/README.md +72 -5
- package/bin/compose.js +754 -347
- package/bin/git-hooks/pre-push.template +29 -0
- package/bin/judgment-import.js +7 -0
- package/bin/judgment-migrate.js +387 -0
- package/contracts/comp-obs-contract.schema.json +9 -3
- package/contracts/feature-json.schema.json +5 -0
- package/contracts/fluid-record.schema.json +209 -0
- package/contracts/judgment-record.schema.json +425 -4
- package/contracts/lifecycle-backfill.schema.json +322 -0
- package/dist/assets/App-Z4MU-H_F.js +916 -0
- package/dist/assets/_baseUniq-ClWoCPFl.js +1 -0
- package/dist/assets/arc-DY26UIVo.js +1 -0
- package/dist/assets/architectureDiagram-Q4EWVU46-6Ggq4DqJ.js +36 -0
- package/dist/assets/blockDiagram-DXYQGD6D-CH3Ked0l.js +132 -0
- package/dist/assets/{browser-BSM23If2.js → browser-BWkrenen.js} +6 -6
- package/dist/assets/{c4Diagram-LMCZKHZV-DZf45Fbz.js → c4Diagram-AHTNJAMY-Bk8dYilu.js} +1 -1
- package/dist/assets/channel-SnZzzh7k.js +1 -0
- package/dist/assets/{chunk-JWPE2WC7-_7ujgd_Q.js → chunk-4BX2VUAB-BMR0XaAQ.js} +1 -1
- package/dist/assets/chunk-4TB4RGXK-JytR14a9.js +206 -0
- package/dist/assets/{chunk-XXDRQBXY-DfdVhbmA.js → chunk-55IACEB6-B4Q97BCP.js} +1 -1
- package/dist/assets/{chunk-VR4S4FIN-Dt9NZ67m.js → chunk-EDXVE4YY-R_qarkSf.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-BY4_PV5H.js → chunk-FMBD7UC4-C9s7KR9m.js} +1 -1
- package/dist/assets/chunk-OYMX7WX6-BySQzVxc.js +231 -0
- package/dist/assets/{chunk-2Q5K7J3B-Dn1spZYu.js → chunk-QZHKN3VN-DdpSYZsW.js} +1 -1
- package/dist/assets/{chunk-32BRIVSS-pURGrJDk.js → chunk-YZCP3GAM-iE_tzriw.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-CBu92dSH.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-CBu92dSH.js +1 -0
- package/dist/assets/clone-DgklGjHm.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-BieYif4o.js → cose-bilkent-S5V4N54A-BdlU6ZX_.js} +1 -1
- package/dist/assets/dagre-KV5264BT-Cp3F5KTn.js +4 -0
- package/dist/assets/diagram-5BDNPKRD-DiR6_2q_.js +10 -0
- package/dist/assets/diagram-G4DWMVQ6-w0i-p5HX.js +24 -0
- package/dist/assets/diagram-MMDJMWI5-tIHhwUv3.js +43 -0
- package/dist/assets/diagram-TYMM5635-BAeY3B19.js +24 -0
- package/dist/assets/erDiagram-SMLLAGMA-Ckx_Knko.js +85 -0
- package/dist/assets/flowDiagram-DWJPFMVM-DeoNka6J.js +162 -0
- package/dist/assets/ganttDiagram-T4ZO3ILL-BmGnFbEg.js +292 -0
- package/dist/assets/gitGraphDiagram-UUTBAWPF-Dk48IHsx.js +106 -0
- package/dist/assets/graph-BNzKGvoy.js +1 -0
- package/dist/assets/graph-CI_1htl0.js +331 -0
- package/dist/assets/index-BEfrNBp8.js +123 -0
- package/dist/assets/index-yyrA5OZd.css +1 -0
- package/dist/assets/infoDiagram-42DDH7IO-BRf827i0.js +2 -0
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-CzEB9fQS.js → ishikawaDiagram-UXIWVN3A-0kCZaeCM.js} +5 -5
- package/dist/assets/{journeyDiagram-5HDEW3XC-Bz8TCdz2.js → journeyDiagram-VCZTEJTY-rvU7ayRt.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-tozrMoV_.js → kanban-definition-6JOO6SKY-DpQwX1C5.js} +7 -7
- package/dist/assets/katex-DkKDou_j.js +257 -0
- package/dist/assets/layout-BI8cXFPI.js +1 -0
- package/dist/assets/{linear-Ck7gpa5N.js → linear-a0glcDiw.js} +1 -1
- package/dist/assets/min-vPHfnXcC.js +1 -0
- package/dist/assets/{mindmap-definition-LN4V7U3C-DTcHO0DJ.js → mindmap-definition-QFDTVHPH-D14eF-7C.js} +7 -7
- package/dist/assets/mobile-B7m9EO9D.js +17 -0
- package/dist/assets/pieDiagram-DEJITSTG-Cno-gETh.js +30 -0
- package/dist/assets/quadrantDiagram-34T5L4WZ-BUQM1Hfm.js +7 -0
- package/dist/assets/{requirementDiagram-TGXJPOKE-bnI2zJeT.js → requirementDiagram-MS252O5E-pOXlN2-q.js} +3 -3
- package/dist/assets/sankeyDiagram-XADWPNL6-Crynd3_b.js +10 -0
- package/dist/assets/sequenceDiagram-FGHM5R23-D9fZdCM8.js +157 -0
- package/dist/assets/stateDiagram-FHFEXIEX-CW9qVec8.js +1 -0
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-DkVLzHbY.js +1 -0
- package/dist/assets/{timeline-definition-FHXFAJF6-D267GQFF.js → timeline-definition-GMOUNBTQ-BcHzhm_8.js} +3 -3
- package/dist/assets/vennDiagram-DHZGUBPP-BfytJcWk.js +34 -0
- package/dist/assets/wardley-RL74JXVD-DLj-IjyB.js +162 -0
- package/dist/assets/wardleyDiagram-NUSXRM2D-Ds0Ue68c.js +20 -0
- package/dist/assets/xychartDiagram-5P7HB3ND-vjWDXFL6.js +7 -0
- package/dist/index.html +3 -3
- package/lib/agent-string.js +7 -5
- package/lib/append-integrity.js +81 -0
- package/lib/backfill-evidence.js +109 -0
- package/lib/bug-escalation.js +39 -4
- package/lib/build-stream-schema.js +3 -1
- package/lib/build-stream-writer.js +25 -0
- package/lib/build.js +1624 -195
- package/lib/canon-guard.js +245 -0
- package/lib/canon-override.js +196 -0
- package/lib/canon-registry.js +291 -0
- package/lib/cli-commands.js +144 -0
- package/lib/codex-preflight.js +50 -15
- package/lib/colleague/context.js +215 -0
- package/lib/colleague/writeback.js +95 -0
- package/lib/completion-gate.js +1421 -0
- package/lib/completion-writer.js +47 -47
- package/lib/consumer-fanout.js +105 -11
- package/lib/coverage-gate.js +200 -0
- package/lib/dir-lock.js +170 -0
- package/lib/dispatch-ledger.js +301 -0
- package/lib/dispatch-metrics.js +236 -0
- package/lib/experiment-judge.js +6 -1
- package/lib/feature-json.js +1 -1
- package/lib/feature-reconciler.js +8 -0
- package/lib/feature-validator.js +64 -1
- package/lib/feature-writer.js +66 -2
- package/lib/fluid/factory.js +167 -0
- package/lib/fluid/ideabox-dates.js +73 -0
- package/lib/fluid/ideabox-migrate.js +154 -0
- package/lib/fluid/ideabox-ops.js +585 -0
- package/lib/fluid/ideabox-view.js +146 -0
- package/lib/fluid/import-ideabox.js +186 -0
- package/lib/fluid/local-provider.js +606 -0
- package/lib/fluid/provider.js +684 -0
- package/lib/fluid/record-shape.js +214 -0
- package/lib/fluid/record-store.js +328 -0
- package/lib/fluid/render-ideabox.js +261 -0
- package/lib/fluid/schema.js +40 -0
- package/lib/fluid/smartmemory-provider.js +1695 -0
- package/lib/gsd.js +63 -14
- package/lib/guard-cli.js +175 -0
- package/lib/guard-custody.js +141 -0
- package/lib/guard-descriptors.js +530 -0
- package/lib/guard-enrol.js +254 -0
- package/lib/health-score.js +1 -1
- package/lib/hooks-status.js +32 -3
- package/lib/ideabox-cli.js +315 -0
- package/lib/ideabox.js +121 -21
- package/lib/judgment/store/index.js +166 -0
- package/lib/judgment/store/records.js +184 -25
- package/lib/judgment/trace.js +380 -0
- package/lib/judgment-attest.js +259 -0
- package/lib/judgment-decision-write.js +277 -0
- package/lib/judgment-decisions.js +466 -0
- package/lib/judgment-gen.js +375 -22
- package/lib/judgment-verify.js +153 -0
- package/lib/judgment-writer.js +2842 -262
- package/lib/lane-gate.js +2 -0
- package/lib/lifecycle-modes.js +4 -4
- package/lib/lineage.js +400 -0
- package/lib/local-claude-connector.js +250 -54
- package/lib/maya-client.js +302 -0
- package/lib/maya-config.js +53 -0
- package/lib/maya-identity.js +283 -0
- package/lib/mcp-enforcement.js +21 -35
- package/lib/migrate-anon.js +5 -0
- package/lib/migrate-roadmap.js +15 -0
- package/lib/new.js +13 -1
- package/lib/pipeline-compat.js +104 -0
- package/lib/policy-catalog.js +295 -0
- package/lib/policy-check.js +0 -0
- package/lib/process-termination.js +98 -0
- package/lib/resolve-workspace.js +5 -1
- package/lib/result-normalizer.js +428 -153
- package/lib/review-normalize.js +4 -0
- package/lib/roadmap-errors.js +65 -0
- package/lib/roadmap-preservers.js +24 -4
- package/lib/roadmap-residue.js +299 -0
- package/lib/smartmemory-client.js +614 -78
- package/lib/smartmemory-config.js +54 -0
- package/lib/smartmemory-ingest.js +19 -2
- package/lib/step-prompt.js +7 -6
- package/lib/stratum-engine.js +53 -4
- package/lib/stratum-mcp-client.js +391 -31
- package/lib/test-bootstrap.js +31 -0
- package/lib/tool-inventory.js +122 -0
- package/lib/version-check.js +91 -19
- package/lib/vision-writer.js +88 -1
- package/package.json +7 -6
- package/pipelines/bug-fix.stratum.yaml +205 -211
- package/pipelines/build-quick.profiles.json +12 -0
- package/pipelines/build-quick.stratum.yaml +263 -350
- package/pipelines/content.stratum.yaml +81 -77
- package/pipelines/coverage-sweep.stratum.yaml +49 -30
- package/pipelines/plan.stratum.yaml +76 -86
- package/pipelines/refactor.stratum.yaml +125 -125
- package/pipelines/research.stratum.yaml +56 -58
- package/pipelines/review-fix.profiles.json +6 -0
- package/pipelines/review-fix.stratum.yaml +110 -83
- package/presets/team-feature.profiles.json +6 -0
- package/presets/team-feature.stratum.yaml +93 -66
- package/presets/team-research.profiles.json +6 -0
- package/presets/team-research.stratum.yaml +89 -80
- package/presets/team-review.profiles.json +8 -0
- package/presets/team-review.stratum.yaml +98 -80
- package/scripts/cost-census.mjs +70 -0
- package/scripts/guard-sign/compose-guard-sign.sh +62 -0
- package/server/agent-health.js +22 -0
- package/server/agent-hooks.js +14 -1
- package/server/agent-server.js +5 -248
- package/server/agent-spawn.js +3 -4
- package/server/agent-workspace.js +294 -0
- package/server/build-routes.js +6 -5
- package/server/build-stream-bridge.js +53 -0
- package/server/cc-session-watcher.js +4 -1
- package/server/coalescing-buffer.js +7 -1
- package/server/completion-projection.js +228 -0
- package/server/compose-mcp-tools.js +124 -24
- package/server/compose-mcp.js +91 -790
- package/server/decision-event-emit.js +41 -2
- package/server/decision-event-id.js +17 -0
- package/server/decision-events-snapshot.js +3 -0
- package/server/design-routes.js +14 -8
- package/server/feature-scan.js +76 -2
- package/server/file-watcher.js +170 -21
- package/server/ideabox-routes.js +166 -224
- package/server/index.js +70 -100
- package/server/lifecycle-guard.js +240 -10
- package/server/lifecycle-phase-history.js +276 -0
- package/server/maya-routes.js +507 -0
- package/server/mcp-tool-defs.js +940 -0
- package/server/mcp-tool-policy.js +35 -3
- package/server/model-tiers.js +22 -5
- package/server/pipeline-routes.js +21 -11
- package/server/project-root.js +58 -19
- package/server/remote-utils.js +3 -1
- package/server/schema-validator.js +7 -1
- package/server/session-manager.js +5 -6
- package/server/session-routes.js +3 -1
- package/server/stratum-client.js +57 -10
- package/server/stratum-sync.js +6 -3
- package/server/summarizer.js +3 -4
- package/server/supervisor.js +0 -1
- package/server/vision-routes.js +208 -98
- package/server/vision-server.js +86 -23
- package/server/vision-store.js +60 -6
- package/server/vision-utils.js +3 -4
- package/server/workspace-activity.js +18 -0
- package/server/workspace-middleware.js +2 -2
- package/server/workspace-runtime.js +243 -0
- package/server/worktree-gc.js +1 -0
- package/dist/assets/App-BG3ngu8H.js +0 -896
- package/dist/assets/abnfDiagram-VRR7QNED-CjB_sD3D.js +0 -1
- package/dist/assets/arc-_v4hR_uD.js +0 -1
- package/dist/assets/architectureDiagram-ZJ3FMSHR-DreJmzXQ.js +0 -36
- package/dist/assets/blockDiagram-677ZJIJ3-BG9-c0O1.js +0 -132
- package/dist/assets/channel-B3U5wFAT.js +0 -1
- package/dist/assets/chunk-EX3LRPZG-DdELs1qP.js +0 -231
- package/dist/assets/chunk-MOJQB5TN-D-ky35G-.js +0 -88
- package/dist/assets/chunk-RYQCIY6F-Dag_kVlO.js +0 -1
- package/dist/assets/chunk-V7JOEXUC-BtewURat.js +0 -206
- package/dist/assets/classDiagram-OUVF2IWQ-B6fCN-ht.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-B6fCN-ht.js +0 -1
- package/dist/assets/cynefin-VYW2F7L2-CT2BA6KE.js +0 -178
- package/dist/assets/cynefinDiagram-TSTJHNR4-Bh6exbyg.js +0 -62
- package/dist/assets/dagre-VKFMJZFB-aXMLSmQL.js +0 -4
- package/dist/assets/diagram-FQU43EPY-Dr7JAOuQ.js +0 -3
- package/dist/assets/diagram-G47NLZAW-DUvA3FQK.js +0 -24
- package/dist/assets/diagram-NH7WQ7WH-BQUARqcu.js +0 -24
- package/dist/assets/diagram-OA4YK3LP-dDUc1zHi.js +0 -30
- package/dist/assets/diagram-WEI45ONY-B2h5Qlb1.js +0 -41
- package/dist/assets/ebnfDiagram-CCIWWBDH-DThRGupB.js +0 -1
- package/dist/assets/erDiagram-Q63AITRT-BUCsprO2.js +0 -85
- package/dist/assets/flowDiagram-23GEKE2U-DXtNNi6r.js +0 -156
- package/dist/assets/ganttDiagram-NO4QXBWP-D4zbBHh_.js +0 -292
- package/dist/assets/gitGraphDiagram-IHSO6WYX-DpoQws0W.js +0 -106
- package/dist/assets/graph-BXPQrYYB.js +0 -331
- package/dist/assets/graph-C9eacEi8.js +0 -1
- package/dist/assets/index-3ZH5eMcZ.js +0 -119
- package/dist/assets/index-LIwREYgH.css +0 -1
- package/dist/assets/infoDiagram-FWYZ7A6U-Bbas2GAo.js +0 -2
- package/dist/assets/katex-C5jXJg4s.js +0 -257
- package/dist/assets/layout-DEXfKzaS.js +0 -1
- package/dist/assets/map-Czzmt4hB.js +0 -1
- package/dist/assets/mobile-CaoXUwAr.js +0 -17
- package/dist/assets/pegDiagram-2B236MQR-CHiINrNy.js +0 -1
- package/dist/assets/pieDiagram-ENE6RG2P-CfS4YFlR.js +0 -39
- package/dist/assets/quadrantDiagram-ABIIQ3AL-CadesS9w.js +0 -7
- package/dist/assets/railroadDiagram-RFXS5EU6-CgWEspBN.js +0 -1
- package/dist/assets/sankeyDiagram-HTMAVEWB-YWKFgOGw.js +0 -40
- package/dist/assets/sequenceDiagram-DBY2YBRQ-BvkNOyF9.js +0 -162
- package/dist/assets/sizeCapture-X5ZJPWSS-DlFPA2yO.js +0 -1
- package/dist/assets/stateDiagram-2N3HPSRC-h8NIx0kQ.js +0 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-DjPgZtJ9.js +0 -1
- package/dist/assets/swimlanes-5IMT3BWC-CT5n22kG.js +0 -2
- package/dist/assets/swimlanesDiagram-G3AALYLV-Dn318Bhq.js +0 -8
- package/dist/assets/vennDiagram-L72KCM5P-Dj-wWLYG.js +0 -34
- package/dist/assets/wardleyDiagram-EHGQE667-BxCeYxkG.js +0 -78
- package/dist/assets/xychartDiagram-FW5EYKEG-DMFqWn7z.js +0 -7
- package/lib/staleness.js +0 -87
- 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,
|
package/lib/lifecycle-modes.js
CHANGED
|
@@ -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
|
+
}
|