carto-md 2.0.8 → 2.1.0

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 (182) hide show
  1. package/CONTRIBUTING.md +19 -15
  2. package/README.md +203 -286
  3. package/docs/anci/v0.1-DRAFT.md +420 -0
  4. package/docs/api/README.md +99 -0
  5. package/docs/api/did_we_discuss_this.md +34 -0
  6. package/docs/api/dismiss_suggestion.md +34 -0
  7. package/docs/api/explain_change_in_natural_language.md +33 -0
  8. package/docs/api/find_consumers_of_api.md +34 -0
  9. package/docs/api/get_action_patterns.md +32 -0
  10. package/docs/api/get_active_drift.md +32 -0
  11. package/docs/api/get_active_suggestions.md +25 -0
  12. package/docs/api/get_ai_cost_attribution.md +32 -0
  13. package/docs/api/get_arch_events.md +42 -0
  14. package/docs/api/get_architectural_drift.md +37 -0
  15. package/docs/api/get_architecture.md +25 -0
  16. package/docs/api/get_blast_radius.md +34 -0
  17. package/docs/api/get_canonical_pattern.md +39 -0
  18. package/docs/api/get_change_plan.md +34 -0
  19. package/docs/api/get_change_velocity.md +32 -0
  20. package/docs/api/get_churn_vs_blast_radius.md +32 -0
  21. package/docs/api/get_complexity_trend.md +39 -0
  22. package/docs/api/get_context.md +34 -0
  23. package/docs/api/get_conventions.md +32 -0
  24. package/docs/api/get_cross_domain.md +25 -0
  25. package/docs/api/get_cross_language_call_graph.md +25 -0
  26. package/docs/api/get_cross_repo_blast_radius.md +34 -0
  27. package/docs/api/get_cross_team_coupling.md +25 -0
  28. package/docs/api/get_data_flow.md +33 -0
  29. package/docs/api/get_dead_code_with_confidence.md +32 -0
  30. package/docs/api/get_decision_log.md +32 -0
  31. package/docs/api/get_dependency_surface.md +25 -0
  32. package/docs/api/get_domain.md +34 -0
  33. package/docs/api/get_domain_evolution.md +39 -0
  34. package/docs/api/get_domain_health.md +32 -0
  35. package/docs/api/get_domains_list.md +25 -0
  36. package/docs/api/get_drift_digest.md +32 -0
  37. package/docs/api/get_env_vars.md +32 -0
  38. package/docs/api/get_evolution_delta.md +36 -0
  39. package/docs/api/get_file_ownership.md +33 -0
  40. package/docs/api/get_file_summary.md +34 -0
  41. package/docs/api/get_high_impact_files.md +32 -0
  42. package/docs/api/get_hot_in_prod_no_tests.md +34 -0
  43. package/docs/api/get_hotspot_files.md +37 -0
  44. package/docs/api/get_iac_resources.md +25 -0
  45. package/docs/api/get_interface_contract.md +33 -0
  46. package/docs/api/get_intervention_history.md +32 -0
  47. package/docs/api/get_invariants.md +37 -0
  48. package/docs/api/get_llm_enrichment.md +33 -0
  49. package/docs/api/get_microservice_cut_points.md +32 -0
  50. package/docs/api/get_microservices_migration_cut_points.md +25 -0
  51. package/docs/api/get_minimal_context_for_intent.md +39 -0
  52. package/docs/api/get_models.md +32 -0
  53. package/docs/api/get_neighbors.md +39 -0
  54. package/docs/api/get_org_architecture.md +25 -0
  55. package/docs/api/get_org_domain_mapping.md +25 -0
  56. package/docs/api/get_pending_decisions.md +32 -0
  57. package/docs/api/get_predictive_risk.md +32 -0
  58. package/docs/api/get_progressive_disclosure_tree.md +25 -0
  59. package/docs/api/get_recent_decisions.md +37 -0
  60. package/docs/api/get_risk_weighted_blast_radius.md +32 -0
  61. package/docs/api/get_routes.md +25 -0
  62. package/docs/api/get_safety_checklist.md +33 -0
  63. package/docs/api/get_semantic_diff.md +34 -0
  64. package/docs/api/get_service_boundary_violations.md +25 -0
  65. package/docs/api/get_service_dependency_graph.md +25 -0
  66. package/docs/api/get_session_context.md +32 -0
  67. package/docs/api/get_similar_patterns.md +39 -0
  68. package/docs/api/get_stale_docs.md +25 -0
  69. package/docs/api/get_structure.md +25 -0
  70. package/docs/api/get_temporal_context.md +34 -0
  71. package/docs/api/get_test_coverage_map.md +25 -0
  72. package/docs/api/get_token_budget_report.md +36 -0
  73. package/docs/api/get_upgrade_risk.md +25 -0
  74. package/docs/api/get_working_memory.md +25 -0
  75. package/docs/api/ingest_otlp_traces.md +34 -0
  76. package/docs/api/scaffold_for_intent.md +34 -0
  77. package/docs/api/search_routes.md +34 -0
  78. package/docs/api/simulate_change_impact.md +37 -0
  79. package/docs/api/validate_change.md +39 -0
  80. package/docs/api/validate_diff.md +39 -0
  81. package/docs/concepts/anci.md +87 -0
  82. package/docs/concepts/blast-radius.md +66 -0
  83. package/docs/concepts/domains.md +91 -0
  84. package/docs/concepts/import-graph.md +102 -0
  85. package/docs/concepts/mcp-integration.md +148 -0
  86. package/docs/guides/adding-feature-safely.md +101 -0
  87. package/docs/guides/ci-integration.md +175 -0
  88. package/docs/guides/monorepo-setup.md +121 -0
  89. package/docs/guides/onboarding-new-engineer.md +121 -0
  90. package/docs/guides/pre-merge-review.md +139 -0
  91. package/docs/migration/v1-to-v2.md +110 -0
  92. package/docs/quickstart.md +95 -0
  93. package/docs/scale.md +129 -0
  94. package/docs/troubleshooting.md +180 -0
  95. package/package.json +12 -5
  96. package/scripts/gen-api-docs.js +170 -0
  97. package/scripts/postinstall.js +391 -24
  98. package/src/acp/agent.js +83 -11
  99. package/src/acp/config.js +64 -0
  100. package/src/acp/persistence.js +146 -0
  101. package/src/acp/providers/anthropic.js +179 -27
  102. package/src/acp/providers/index.js +15 -2
  103. package/src/acp/providers/openai.js +164 -38
  104. package/src/acp/providers/sse.js +82 -0
  105. package/src/acp/safety.js +128 -0
  106. package/src/acp/session.js +73 -0
  107. package/src/adjacent/call-graph.js +170 -0
  108. package/src/adjacent/iac.js +167 -0
  109. package/src/adjacent/llm-enrich.js +35 -0
  110. package/src/adjacent/runtime.js +216 -0
  111. package/src/adjacent/semantic-diff.js +143 -0
  112. package/src/agents/leiden.js +4 -4
  113. package/src/agents/scan-structure.js +2 -2
  114. package/src/ai/context-builder.js +215 -0
  115. package/src/ai/retrieval/lexical.js +122 -0
  116. package/src/ai/retrieval/rrf.js +121 -0
  117. package/src/ai/retrieval/semantic.js +35 -0
  118. package/src/ai/retrieval/structural.js +82 -0
  119. package/src/ai/tools.js +423 -0
  120. package/src/anci/consumer.js +305 -0
  121. package/src/anci/deserialize.js +160 -0
  122. package/src/anci/emit.js +85 -0
  123. package/src/anci/serialize.js +264 -0
  124. package/src/anci/yaml.js +401 -0
  125. package/src/bitmap/index.js +1 -1
  126. package/src/bitmap/tools.js +2 -2
  127. package/src/brain/conventions/index.js +185 -0
  128. package/src/brain/index.js +31 -0
  129. package/src/brain/invariants/index.js +252 -0
  130. package/src/brain/procedural/index.js +181 -0
  131. package/src/brain/suggestions/index.js +153 -0
  132. package/src/brain/working/index.js +170 -0
  133. package/src/cli/anci.js +237 -0
  134. package/src/cli/check.js +47 -1
  135. package/src/cli/diff.js +83 -0
  136. package/src/cli/doctor.js +270 -0
  137. package/src/cli/explain.js +61 -0
  138. package/src/cli/index.js +115 -0
  139. package/src/cli/init.js +144 -6
  140. package/src/cli/inspect.js +1 -1
  141. package/src/cli/org.js +172 -0
  142. package/src/cli/pr-impact.js +554 -0
  143. package/src/cli/serve.js +1 -1
  144. package/src/cli/status.js +211 -0
  145. package/src/cli/sync.js +2 -2
  146. package/src/cli/temporal.js +188 -0
  147. package/src/cli/validate.js +201 -0
  148. package/src/cli/watch.js +4 -4
  149. package/src/cli/why.js +101 -0
  150. package/src/extractors/frameworks.js +236 -0
  151. package/src/extractors/languages/dart.js +138 -0
  152. package/src/extractors/languages/go.js +11 -1
  153. package/src/extractors/languages/javascript.js +16 -6
  154. package/src/extractors/languages/kotlin.js +169 -0
  155. package/src/extractors/languages/php.js +195 -0
  156. package/src/extractors/languages/python.js +12 -1
  157. package/src/extractors/languages/swift.js +140 -0
  158. package/src/extractors/languages/typescript.js +15 -2
  159. package/src/extractors/plugin-api.js +102 -0
  160. package/src/mcp/change-plan.js +8 -8
  161. package/src/mcp/files-without-tests.js +285 -0
  162. package/src/mcp/middleware/index.js +451 -0
  163. package/src/mcp/server.js +2292 -0
  164. package/src/mcp/validate.js +1 -1
  165. package/src/org/detect.js +262 -0
  166. package/src/org/queries.js +144 -0
  167. package/src/org/store.js +173 -0
  168. package/src/org/sync.js +106 -0
  169. package/src/predictive/cut-points.js +83 -0
  170. package/src/predictive/drift-digest.js +88 -0
  171. package/src/predictive/ownership.js +145 -0
  172. package/src/predictive/risk-score.js +121 -0
  173. package/src/predictive/validate-change.js +55 -0
  174. package/src/store/store-adapter.js +3 -3
  175. package/src/store/{sync-v2.js → sync.js} +105 -16
  176. package/src/temporal/backfill.js +211 -0
  177. package/src/temporal/delta.js +85 -0
  178. package/src/temporal/events.js +180 -0
  179. package/src/temporal/queries.js +358 -0
  180. package/src/temporal/snapshot.js +151 -0
  181. package/src/temporal/store.js +400 -0
  182. package/src/mcp/server-v2.js +0 -986
@@ -0,0 +1,400 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * TemporalStore — `.carto/carto-temporal.db` schema + accessors.
5
+ *
6
+ * The temporal layer turns the index into something that compounds with use:
7
+ *
8
+ * "AUTH gained 3 cross-domain imports this month, grew 18 files, and
9
+ * lost stability when payments/billing.ts moved out — and six months
10
+ * from now will tell you which file historically causes the most
11
+ * regressions."
12
+ *
13
+ * It lives in a separate SQLite file so that:
14
+ * - The main `carto.db` stays tight (it's the hot path for every MCP query).
15
+ * - Temporal storage can grow to tens of MB (a year of XOR deltas) without
16
+ * dragging on bitmap rebuilds.
17
+ * - The file is fully optional — Carto runs fine without it.
18
+ *
19
+ * Schema:
20
+ * - snapshots one row per `carto sync` snapshot
21
+ * - file_domains_at file→domain assignment at each snapshot
22
+ * - file_churn per-file commit count + last-modified ts (rolling)
23
+ * - arch_events significant architectural events (split, merge, new)
24
+ * - deltas XOR'd forward adjacency between consecutive snapshots
25
+ *
26
+ * Properties the tests pin down:
27
+ * 1. Opening readonly never crashes when the file is missing —
28
+ * `getStoreReadonly()` returns null cleanly.
29
+ * 2. Backfilling tens of thousands of commits never blocks on schema
30
+ * mutation — `CREATE TABLE IF NOT EXISTS`, WAL mode, bulk inserts in a
31
+ * single transaction.
32
+ * 3. Snapshot inserts are idempotent on (commit_sha, source) so re-running
33
+ * `carto temporal init` doesn't duplicate rows.
34
+ * 4. No Carto-specific logic in here — `snapshot.js`, `queries.js`, and
35
+ * `backfill.js` build on top of this primitive.
36
+ */
37
+
38
+ const fs = require('fs');
39
+ const path = require('path');
40
+ const Database = require('better-sqlite3');
41
+
42
+ const SCHEMA_VERSION = '1';
43
+ const TEMPORAL_DB_FILENAME = 'carto-temporal.db';
44
+
45
+ class TemporalStore {
46
+ constructor(projectRoot) {
47
+ this._projectRoot = projectRoot;
48
+ this._db = null;
49
+ }
50
+
51
+ /** Path to the temporal DB file. */
52
+ get dbPath() {
53
+ return path.join(this._projectRoot, '.carto', TEMPORAL_DB_FILENAME);
54
+ }
55
+
56
+ /**
57
+ * open(opts) — open or create the temporal DB.
58
+ *
59
+ * Options:
60
+ * readonly — open RO; throws if file is missing (use openIfExists for
61
+ * the graceful path).
62
+ */
63
+ open(opts = {}) {
64
+ const cartoDir = path.join(this._projectRoot, '.carto');
65
+
66
+ if (opts.readonly) {
67
+ this._db = new Database(this.dbPath, { readonly: true, fileMustExist: true });
68
+ this._db.pragma('busy_timeout = 5000');
69
+ return this;
70
+ }
71
+
72
+ fs.mkdirSync(cartoDir, { recursive: true });
73
+ try {
74
+ this._db = new Database(this.dbPath);
75
+ } catch (err) {
76
+ // Corrupt — recreate
77
+ try { fs.unlinkSync(this.dbPath); } catch {}
78
+ try { fs.unlinkSync(this.dbPath + '-wal'); } catch {}
79
+ try { fs.unlinkSync(this.dbPath + '-shm'); } catch {}
80
+ this._db = new Database(this.dbPath);
81
+ }
82
+
83
+ this._db.pragma('journal_mode = WAL');
84
+ this._db.pragma('synchronous = NORMAL');
85
+ this._db.pragma('busy_timeout = 5000');
86
+ this._db.pragma('cache_size = -32000');
87
+ this._ensureSchema();
88
+ return this;
89
+ }
90
+
91
+ /** Like open() but returns null if the file is missing or unreadable. */
92
+ static openIfExists(projectRoot, opts = {}) {
93
+ const p = path.join(projectRoot, '.carto', TEMPORAL_DB_FILENAME);
94
+ if (!fs.existsSync(p)) return null;
95
+ try {
96
+ return new TemporalStore(projectRoot).open(opts);
97
+ } catch {
98
+ return null;
99
+ }
100
+ }
101
+
102
+ close() {
103
+ if (this._db) {
104
+ try { this._db.close(); } catch {}
105
+ this._db = null;
106
+ }
107
+ }
108
+
109
+ get db() { return this._db; }
110
+
111
+ _ensureSchema() {
112
+ const cur = this.getMeta('schema_version');
113
+ if (cur === SCHEMA_VERSION) return;
114
+
115
+ this._db.exec(`
116
+ CREATE TABLE IF NOT EXISTS meta (
117
+ key TEXT PRIMARY KEY,
118
+ value TEXT
119
+ );
120
+ CREATE TABLE IF NOT EXISTS snapshots (
121
+ id INTEGER PRIMARY KEY,
122
+ ts INTEGER NOT NULL,
123
+ commit_sha TEXT,
124
+ source TEXT NOT NULL,
125
+ file_count INTEGER DEFAULT 0,
126
+ edge_count INTEGER DEFAULT 0,
127
+ domain_count INTEGER DEFAULT 0,
128
+ summary_json TEXT
129
+ );
130
+ CREATE INDEX IF NOT EXISTS idx_snapshots_ts ON snapshots(ts DESC);
131
+ CREATE INDEX IF NOT EXISTS idx_snapshots_sha ON snapshots(commit_sha);
132
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_snapshots_sha_source ON snapshots(commit_sha, source);
133
+
134
+ CREATE TABLE IF NOT EXISTS file_domains_at (
135
+ snapshot_id INTEGER NOT NULL,
136
+ file_path TEXT NOT NULL,
137
+ domain_name TEXT,
138
+ PRIMARY KEY (snapshot_id, file_path)
139
+ );
140
+ CREATE INDEX IF NOT EXISTS idx_fda_snap ON file_domains_at(snapshot_id);
141
+ CREATE INDEX IF NOT EXISTS idx_fda_path ON file_domains_at(file_path);
142
+
143
+ CREATE TABLE IF NOT EXISTS file_churn (
144
+ file_path TEXT PRIMARY KEY,
145
+ commit_count INTEGER DEFAULT 0,
146
+ first_seen_ts INTEGER,
147
+ last_modified_ts INTEGER,
148
+ blast_radius INTEGER DEFAULT 0
149
+ );
150
+ CREATE INDEX IF NOT EXISTS idx_churn_count ON file_churn(commit_count DESC);
151
+ CREATE INDEX IF NOT EXISTS idx_churn_last ON file_churn(last_modified_ts DESC);
152
+
153
+ CREATE TABLE IF NOT EXISTS arch_events (
154
+ id INTEGER PRIMARY KEY,
155
+ ts INTEGER NOT NULL,
156
+ severity TEXT NOT NULL,
157
+ kind TEXT NOT NULL,
158
+ domain TEXT,
159
+ file_path TEXT,
160
+ detail_json TEXT,
161
+ snapshot_id INTEGER
162
+ );
163
+ CREATE INDEX IF NOT EXISTS idx_events_ts ON arch_events(ts DESC);
164
+ CREATE INDEX IF NOT EXISTS idx_events_kind ON arch_events(kind);
165
+ CREATE INDEX IF NOT EXISTS idx_events_severity ON arch_events(severity);
166
+
167
+ CREATE TABLE IF NOT EXISTS deltas (
168
+ id INTEGER PRIMARY KEY,
169
+ snapshot_id INTEGER NOT NULL,
170
+ prev_snapshot_id INTEGER,
171
+ kind TEXT NOT NULL,
172
+ delta_blob BLOB NOT NULL,
173
+ bit_size INTEGER NOT NULL,
174
+ FOREIGN KEY (snapshot_id) REFERENCES snapshots(id)
175
+ );
176
+ CREATE INDEX IF NOT EXISTS idx_deltas_snap ON deltas(snapshot_id);
177
+ `);
178
+
179
+ this.setMeta('schema_version', SCHEMA_VERSION);
180
+ this.setMeta('created_at', String(Date.now()));
181
+ }
182
+
183
+ // ── Meta helpers ──────────────────────────────────────────────────
184
+ getMeta(key) {
185
+ if (!this._db) return null;
186
+ try {
187
+ const row = this._db
188
+ .prepare('SELECT value FROM meta WHERE key = ?')
189
+ .get(key);
190
+ return row ? row.value : null;
191
+ } catch {
192
+ return null;
193
+ }
194
+ }
195
+
196
+ setMeta(key, value) {
197
+ if (!this._db) return;
198
+ this._db
199
+ .prepare('INSERT INTO meta(key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value')
200
+ .run(key, String(value));
201
+ }
202
+
203
+ // ── Snapshot writes ───────────────────────────────────────────────
204
+ /**
205
+ * insertSnapshot({ ts, commit_sha, source, summary }) → snapshot_id
206
+ *
207
+ * `source` is one of:
208
+ * - 'sync' — captured after a successful `carto sync`
209
+ * - 'commit' — backfilled from a git commit
210
+ * - 'backfill' — explicit `carto temporal init` row
211
+ *
212
+ * Idempotent on (commit_sha, source). If a row with the same SHA + source
213
+ * already exists, the original id is returned (no update; snapshots are
214
+ * historical facts).
215
+ */
216
+ insertSnapshot({ ts, commit_sha = null, source, summary = null }) {
217
+ if (!this._db) throw new Error('insertSnapshot: store not open');
218
+ const summaryJson = summary ? JSON.stringify(summary) : null;
219
+ const fileCount = summary && Number.isFinite(summary.file_count) ? summary.file_count : 0;
220
+ const edgeCount = summary && Number.isFinite(summary.edge_count) ? summary.edge_count : 0;
221
+ const domainCount = summary && Number.isFinite(summary.domain_count) ? summary.domain_count : 0;
222
+
223
+ if (commit_sha) {
224
+ const existing = this._db
225
+ .prepare('SELECT id FROM snapshots WHERE commit_sha = ? AND source = ?')
226
+ .get(commit_sha, source);
227
+ if (existing) return existing.id;
228
+ }
229
+
230
+ const info = this._db
231
+ .prepare(`
232
+ INSERT INTO snapshots (ts, commit_sha, source, file_count, edge_count, domain_count, summary_json)
233
+ VALUES (?, ?, ?, ?, ?, ?, ?)
234
+ `)
235
+ .run(ts, commit_sha, source, fileCount, edgeCount, domainCount, summaryJson);
236
+ return info.lastInsertRowid;
237
+ }
238
+
239
+ /** Bulk insert file→domain mappings for a snapshot. */
240
+ insertFileDomains(snapshotId, mappings) {
241
+ if (!this._db || !mappings || mappings.length === 0) return;
242
+ const stmt = this._db.prepare(
243
+ 'INSERT OR REPLACE INTO file_domains_at (snapshot_id, file_path, domain_name) VALUES (?, ?, ?)'
244
+ );
245
+ const tx = this._db.transaction(() => {
246
+ for (const m of mappings) stmt.run(snapshotId, m.file_path, m.domain_name || null);
247
+ });
248
+ tx();
249
+ }
250
+
251
+ /**
252
+ * recordCommitChurn(commitTs, changedFiles)
253
+ *
254
+ * Records that `changedFiles` were modified at `commitTs`. Increments
255
+ * commit_count, sets first_seen_ts (if not set), updates last_modified_ts.
256
+ * Used by `carto temporal init` to build the churn table.
257
+ */
258
+ recordCommitChurn(commitTs, changedFiles) {
259
+ if (!this._db || !Array.isArray(changedFiles) || changedFiles.length === 0) return;
260
+ const stmt = this._db.prepare(`
261
+ INSERT INTO file_churn (file_path, commit_count, first_seen_ts, last_modified_ts)
262
+ VALUES (?, 1, ?, ?)
263
+ ON CONFLICT(file_path) DO UPDATE SET
264
+ commit_count = commit_count + 1,
265
+ first_seen_ts = MIN(first_seen_ts, excluded.first_seen_ts),
266
+ last_modified_ts = MAX(last_modified_ts, excluded.last_modified_ts)
267
+ `);
268
+ const tx = this._db.transaction(() => {
269
+ for (const f of changedFiles) {
270
+ if (typeof f === 'string' && f.length > 0) {
271
+ stmt.run(f, commitTs, commitTs);
272
+ }
273
+ }
274
+ });
275
+ tx();
276
+ }
277
+
278
+ /** Replace blast_radius column on file_churn. Idempotent. */
279
+ updateBlastRadii(map) {
280
+ if (!this._db || !map) return;
281
+ const stmt = this._db.prepare(
282
+ 'UPDATE file_churn SET blast_radius = ? WHERE file_path = ?'
283
+ );
284
+ const tx = this._db.transaction(() => {
285
+ for (const [filePath, radius] of map) {
286
+ stmt.run(Number.isFinite(radius) ? radius : 0, filePath);
287
+ }
288
+ });
289
+ tx();
290
+ }
291
+
292
+ /** Insert an architectural event. */
293
+ insertEvent({ ts, severity, kind, domain = null, file_path = null, detail = null, snapshot_id = null }) {
294
+ if (!this._db) return;
295
+ const detailJson = detail ? JSON.stringify(detail) : null;
296
+ this._db.prepare(`
297
+ INSERT INTO arch_events (ts, severity, kind, domain, file_path, detail_json, snapshot_id)
298
+ VALUES (?, ?, ?, ?, ?, ?, ?)
299
+ `).run(ts, severity, kind, domain, file_path, detailJson, snapshot_id);
300
+ }
301
+
302
+ /** Insert XOR delta blob. */
303
+ insertDelta(snapshotId, prevSnapshotId, kind, deltaBlob, bitSize) {
304
+ if (!this._db) return;
305
+ this._db.prepare(`
306
+ INSERT INTO deltas (snapshot_id, prev_snapshot_id, kind, delta_blob, bit_size)
307
+ VALUES (?, ?, ?, ?, ?)
308
+ `).run(snapshotId, prevSnapshotId, kind, deltaBlob, bitSize);
309
+ }
310
+
311
+ // ── Reads ─────────────────────────────────────────────────────────
312
+ getMostRecentSnapshot(source = null) {
313
+ if (!this._db) return null;
314
+ const sql = source
315
+ ? 'SELECT * FROM snapshots WHERE source = ? ORDER BY ts DESC LIMIT 1'
316
+ : 'SELECT * FROM snapshots ORDER BY ts DESC LIMIT 1';
317
+ return source ? this._db.prepare(sql).get(source) : this._db.prepare(sql).get();
318
+ }
319
+
320
+ /** Get snapshots between two timestamps. */
321
+ getSnapshotsBetween(startTs, endTs) {
322
+ if (!this._db) return [];
323
+ return this._db
324
+ .prepare('SELECT * FROM snapshots WHERE ts >= ? AND ts <= ? ORDER BY ts ASC')
325
+ .all(startTs, endTs);
326
+ }
327
+
328
+ getSnapshotById(id) {
329
+ if (!this._db) return null;
330
+ return this._db.prepare('SELECT * FROM snapshots WHERE id = ?').get(id);
331
+ }
332
+
333
+ getFileDomainsAt(snapshotId) {
334
+ if (!this._db) return [];
335
+ return this._db
336
+ .prepare('SELECT file_path, domain_name FROM file_domains_at WHERE snapshot_id = ?')
337
+ .all(snapshotId);
338
+ }
339
+
340
+ getFileChurn(filePath) {
341
+ if (!this._db) return null;
342
+ return this._db.prepare('SELECT * FROM file_churn WHERE file_path = ?').get(filePath);
343
+ }
344
+
345
+ getTopChurned(limit = 20, sinceTs = null) {
346
+ if (!this._db) return [];
347
+ if (sinceTs) {
348
+ return this._db.prepare(`
349
+ SELECT * FROM file_churn
350
+ WHERE last_modified_ts >= ?
351
+ ORDER BY commit_count DESC, blast_radius DESC
352
+ LIMIT ?
353
+ `).all(sinceTs, limit);
354
+ }
355
+ return this._db.prepare(`
356
+ SELECT * FROM file_churn
357
+ ORDER BY commit_count DESC, blast_radius DESC
358
+ LIMIT ?
359
+ `).all(limit);
360
+ }
361
+
362
+ /** All churn rows (used by hotspot queries). */
363
+ getAllChurn() {
364
+ if (!this._db) return [];
365
+ return this._db.prepare('SELECT * FROM file_churn').all();
366
+ }
367
+
368
+ getArchEvents({ severity = null, sinceTs = null, kind = null, limit = 100 } = {}) {
369
+ if (!this._db) return [];
370
+ const where = [];
371
+ const args = [];
372
+ if (severity) { where.push('severity = ?'); args.push(severity); }
373
+ if (kind) { where.push('kind = ?'); args.push(kind); }
374
+ if (sinceTs) { where.push('ts >= ?'); args.push(sinceTs); }
375
+ const whereSql = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
376
+ return this._db.prepare(
377
+ `SELECT * FROM arch_events ${whereSql} ORDER BY ts DESC LIMIT ?`
378
+ ).all(...args, limit);
379
+ }
380
+
381
+ countSnapshots() {
382
+ if (!this._db) return 0;
383
+ const r = this._db.prepare('SELECT COUNT(*) as c FROM snapshots').get();
384
+ return r ? r.c : 0;
385
+ }
386
+
387
+ countCommits() {
388
+ if (!this._db) return 0;
389
+ const r = this._db.prepare(
390
+ "SELECT COUNT(*) as c FROM snapshots WHERE source = 'commit'"
391
+ ).get();
392
+ return r ? r.c : 0;
393
+ }
394
+ }
395
+
396
+ module.exports = {
397
+ TemporalStore,
398
+ SCHEMA_VERSION,
399
+ TEMPORAL_DB_FILENAME,
400
+ };