carto-md 2.0.9 → 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.
- package/CONTRIBUTING.md +19 -15
- package/README.md +192 -415
- package/docs/api/README.md +99 -0
- package/docs/api/did_we_discuss_this.md +34 -0
- package/docs/api/dismiss_suggestion.md +34 -0
- package/docs/api/explain_change_in_natural_language.md +33 -0
- package/docs/api/find_consumers_of_api.md +34 -0
- package/docs/api/get_action_patterns.md +32 -0
- package/docs/api/get_active_drift.md +32 -0
- package/docs/api/get_active_suggestions.md +25 -0
- package/docs/api/get_ai_cost_attribution.md +32 -0
- package/docs/api/get_arch_events.md +42 -0
- package/docs/api/get_architectural_drift.md +37 -0
- package/docs/api/get_architecture.md +25 -0
- package/docs/api/get_blast_radius.md +34 -0
- package/docs/api/get_canonical_pattern.md +39 -0
- package/docs/api/get_change_plan.md +34 -0
- package/docs/api/get_change_velocity.md +32 -0
- package/docs/api/get_churn_vs_blast_radius.md +32 -0
- package/docs/api/get_complexity_trend.md +39 -0
- package/docs/api/get_context.md +34 -0
- package/docs/api/get_conventions.md +32 -0
- package/docs/api/get_cross_domain.md +25 -0
- package/docs/api/get_cross_language_call_graph.md +25 -0
- package/docs/api/get_cross_repo_blast_radius.md +34 -0
- package/docs/api/get_cross_team_coupling.md +25 -0
- package/docs/api/get_data_flow.md +33 -0
- package/docs/api/get_dead_code_with_confidence.md +32 -0
- package/docs/api/get_decision_log.md +32 -0
- package/docs/api/get_dependency_surface.md +25 -0
- package/docs/api/get_domain.md +34 -0
- package/docs/api/get_domain_evolution.md +39 -0
- package/docs/api/get_domain_health.md +32 -0
- package/docs/api/get_domains_list.md +25 -0
- package/docs/api/get_drift_digest.md +32 -0
- package/docs/api/get_env_vars.md +32 -0
- package/docs/api/get_evolution_delta.md +36 -0
- package/docs/api/get_file_ownership.md +33 -0
- package/docs/api/get_file_summary.md +34 -0
- package/docs/api/get_high_impact_files.md +32 -0
- package/docs/api/get_hot_in_prod_no_tests.md +34 -0
- package/docs/api/get_hotspot_files.md +37 -0
- package/docs/api/get_iac_resources.md +25 -0
- package/docs/api/get_interface_contract.md +33 -0
- package/docs/api/get_intervention_history.md +32 -0
- package/docs/api/get_invariants.md +37 -0
- package/docs/api/get_llm_enrichment.md +33 -0
- package/docs/api/get_microservice_cut_points.md +32 -0
- package/docs/api/get_microservices_migration_cut_points.md +25 -0
- package/docs/api/get_minimal_context_for_intent.md +39 -0
- package/docs/api/get_models.md +32 -0
- package/docs/api/get_neighbors.md +39 -0
- package/docs/api/get_org_architecture.md +25 -0
- package/docs/api/get_org_domain_mapping.md +25 -0
- package/docs/api/get_pending_decisions.md +32 -0
- package/docs/api/get_predictive_risk.md +32 -0
- package/docs/api/get_progressive_disclosure_tree.md +25 -0
- package/docs/api/get_recent_decisions.md +37 -0
- package/docs/api/get_risk_weighted_blast_radius.md +32 -0
- package/docs/api/get_routes.md +25 -0
- package/docs/api/get_safety_checklist.md +33 -0
- package/docs/api/get_semantic_diff.md +34 -0
- package/docs/api/get_service_boundary_violations.md +25 -0
- package/docs/api/get_service_dependency_graph.md +25 -0
- package/docs/api/get_session_context.md +32 -0
- package/docs/api/get_similar_patterns.md +39 -0
- package/docs/api/get_stale_docs.md +25 -0
- package/docs/api/get_structure.md +25 -0
- package/docs/api/get_temporal_context.md +34 -0
- package/docs/api/get_test_coverage_map.md +25 -0
- package/docs/api/get_token_budget_report.md +36 -0
- package/docs/api/get_upgrade_risk.md +25 -0
- package/docs/api/get_working_memory.md +25 -0
- package/docs/api/ingest_otlp_traces.md +34 -0
- package/docs/api/scaffold_for_intent.md +34 -0
- package/docs/api/search_routes.md +34 -0
- package/docs/api/simulate_change_impact.md +37 -0
- package/docs/api/validate_change.md +39 -0
- package/docs/api/validate_diff.md +39 -0
- package/docs/concepts/anci.md +87 -0
- package/docs/concepts/blast-radius.md +66 -0
- package/docs/concepts/domains.md +91 -0
- package/docs/concepts/import-graph.md +102 -0
- package/docs/concepts/mcp-integration.md +148 -0
- package/docs/guides/adding-feature-safely.md +101 -0
- package/docs/guides/ci-integration.md +175 -0
- package/docs/guides/monorepo-setup.md +121 -0
- package/docs/guides/onboarding-new-engineer.md +121 -0
- package/docs/guides/pre-merge-review.md +139 -0
- package/docs/migration/v1-to-v2.md +110 -0
- package/docs/quickstart.md +95 -0
- package/docs/scale.md +2 -2
- package/docs/troubleshooting.md +180 -0
- package/package.json +10 -4
- package/scripts/gen-api-docs.js +170 -0
- package/src/acp/agent.js +83 -11
- package/src/acp/config.js +64 -0
- package/src/acp/persistence.js +146 -0
- package/src/acp/providers/anthropic.js +179 -27
- package/src/acp/providers/index.js +15 -2
- package/src/acp/providers/openai.js +164 -38
- package/src/acp/providers/sse.js +82 -0
- package/src/acp/safety.js +128 -0
- package/src/acp/session.js +73 -0
- package/src/adjacent/call-graph.js +170 -0
- package/src/adjacent/iac.js +167 -0
- package/src/adjacent/llm-enrich.js +35 -0
- package/src/adjacent/runtime.js +216 -0
- package/src/adjacent/semantic-diff.js +143 -0
- package/src/agents/scan-structure.js +1 -1
- package/src/ai/context-builder.js +215 -0
- package/src/ai/retrieval/lexical.js +122 -0
- package/src/ai/retrieval/rrf.js +121 -0
- package/src/ai/retrieval/semantic.js +35 -0
- package/src/ai/retrieval/structural.js +82 -0
- package/src/ai/tools.js +423 -0
- package/src/anci/emit.js +2 -2
- package/src/bitmap/index.js +1 -1
- package/src/bitmap/tools.js +2 -2
- package/src/brain/conventions/index.js +185 -0
- package/src/brain/index.js +31 -0
- package/src/brain/invariants/index.js +252 -0
- package/src/brain/procedural/index.js +181 -0
- package/src/brain/suggestions/index.js +153 -0
- package/src/brain/working/index.js +170 -0
- package/src/cli/check.js +47 -1
- package/src/cli/diff.js +83 -0
- package/src/cli/doctor.js +270 -0
- package/src/cli/explain.js +61 -0
- package/src/cli/index.js +101 -0
- package/src/cli/init.js +143 -5
- package/src/cli/org.js +172 -0
- package/src/cli/pr-impact.js +59 -2
- package/src/cli/serve.js +1 -1
- package/src/cli/status.js +211 -0
- package/src/cli/sync.js +2 -2
- package/src/cli/temporal.js +188 -0
- package/src/cli/validate.js +201 -0
- package/src/cli/watch.js +4 -4
- package/src/cli/why.js +101 -0
- package/src/extractors/frameworks.js +236 -0
- package/src/extractors/languages/dart.js +138 -0
- package/src/extractors/languages/go.js +11 -1
- package/src/extractors/languages/javascript.js +13 -3
- package/src/extractors/languages/kotlin.js +169 -0
- package/src/extractors/languages/php.js +195 -0
- package/src/extractors/languages/python.js +12 -1
- package/src/extractors/languages/swift.js +140 -0
- package/src/extractors/languages/typescript.js +15 -2
- package/src/extractors/plugin-api.js +102 -0
- package/src/mcp/files-without-tests.js +285 -0
- package/src/mcp/middleware/index.js +451 -0
- package/src/mcp/server.js +2292 -0
- package/src/mcp/validate.js +1 -1
- package/src/org/detect.js +262 -0
- package/src/org/queries.js +144 -0
- package/src/org/store.js +173 -0
- package/src/org/sync.js +106 -0
- package/src/predictive/cut-points.js +83 -0
- package/src/predictive/drift-digest.js +88 -0
- package/src/predictive/ownership.js +145 -0
- package/src/predictive/risk-score.js +121 -0
- package/src/predictive/validate-change.js +55 -0
- package/src/store/store-adapter.js +3 -3
- package/src/store/{sync-v2.js → sync.js} +53 -13
- package/src/temporal/backfill.js +211 -0
- package/src/temporal/delta.js +85 -0
- package/src/temporal/events.js +180 -0
- package/src/temporal/queries.js +358 -0
- package/src/temporal/snapshot.js +151 -0
- package/src/temporal/store.js +400 -0
- 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
|
+
};
|