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.
- package/CONTRIBUTING.md +19 -15
- package/README.md +203 -286
- package/docs/anci/v0.1-DRAFT.md +420 -0
- 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 +129 -0
- package/docs/troubleshooting.md +180 -0
- package/package.json +12 -5
- package/scripts/gen-api-docs.js +170 -0
- package/scripts/postinstall.js +391 -24
- 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/leiden.js +4 -4
- package/src/agents/scan-structure.js +2 -2
- 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/consumer.js +305 -0
- package/src/anci/deserialize.js +160 -0
- package/src/anci/emit.js +85 -0
- package/src/anci/serialize.js +264 -0
- package/src/anci/yaml.js +401 -0
- 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/anci.js +237 -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 +115 -0
- package/src/cli/init.js +144 -6
- package/src/cli/inspect.js +1 -1
- package/src/cli/org.js +172 -0
- package/src/cli/pr-impact.js +554 -0
- 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 +16 -6
- 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/change-plan.js +8 -8
- 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} +105 -16
- 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,211 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `carto status` — one-screen health view of the index.
|
|
6
|
+
*
|
|
7
|
+
* Different from `carto inspect`:
|
|
8
|
+
* - `inspect` is a deep diagnostic dump (paths, sizes, mtimes,
|
|
9
|
+
* bitmap shape, top-impact, extraction errors). For debugging.
|
|
10
|
+
* - `status` is the quick "is my index in good shape right now?"
|
|
11
|
+
* answer. Single screen. Designed to be read in <2 seconds.
|
|
12
|
+
*
|
|
13
|
+
* Layout:
|
|
14
|
+
*
|
|
15
|
+
* carto · my-project · 847 files · 5 domains
|
|
16
|
+
* ──────────────────────────────────────────
|
|
17
|
+
* Index ✓ healthy
|
|
18
|
+
* Synced 2m ago (147 files, 5 domains, 1,204 imports)
|
|
19
|
+
* Bitmap ✓ fresh
|
|
20
|
+
* Schema v3
|
|
21
|
+
* Errors 0 extraction breadcrumbs
|
|
22
|
+
*
|
|
23
|
+
* Top domains:
|
|
24
|
+
* CORE (677 files)
|
|
25
|
+
* DATABASE (67 files)
|
|
26
|
+
* ...
|
|
27
|
+
*
|
|
28
|
+
* Use `carto inspect` for the full breakdown.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const fs = require('fs');
|
|
32
|
+
const path = require('path');
|
|
33
|
+
const { SQLiteStore } = require('../store/sqlite-store');
|
|
34
|
+
const { BITMAP_FILENAME } = require('../bitmap/sidecar');
|
|
35
|
+
|
|
36
|
+
function formatAge(ts) {
|
|
37
|
+
if (!ts) return 'never';
|
|
38
|
+
const ageSec = Math.max(0, Math.round((Date.now() - ts) / 1000));
|
|
39
|
+
if (ageSec < 60) return `${ageSec}s ago`;
|
|
40
|
+
if (ageSec < 3600) return `${Math.round(ageSec / 60)}m ago`;
|
|
41
|
+
if (ageSec < 86400) return `${Math.round(ageSec / 3600)}h ago`;
|
|
42
|
+
return `${Math.round(ageSec / 86400)}d ago`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* collect(projectRoot) → status snapshot object
|
|
47
|
+
* {
|
|
48
|
+
* healthy: bool,
|
|
49
|
+
* issues: string[], // human-readable issue lines
|
|
50
|
+
* dbExists, dbMtime, schemaVersion,
|
|
51
|
+
* bitmapExists, bitmapStale,
|
|
52
|
+
* totalFiles, totalRoutes, totalImportEdges,
|
|
53
|
+
* domains: [{ name, fileCount }, ...],
|
|
54
|
+
* extractionErrors,
|
|
55
|
+
* projectName,
|
|
56
|
+
* }
|
|
57
|
+
*
|
|
58
|
+
* Pure — no I/O on stdout. The renderer (and tests) consumes this.
|
|
59
|
+
*/
|
|
60
|
+
function collect(projectRoot) {
|
|
61
|
+
const cartoDir = path.join(projectRoot, '.carto');
|
|
62
|
+
const dbPath = path.join(cartoDir, 'carto.db');
|
|
63
|
+
const bitmapPath = path.join(cartoDir, BITMAP_FILENAME);
|
|
64
|
+
const issues = [];
|
|
65
|
+
|
|
66
|
+
const dbStat = (() => { try { return fs.statSync(dbPath); } catch { return null; } })();
|
|
67
|
+
const bmStat = (() => { try { return fs.statSync(bitmapPath); } catch { return null; } })();
|
|
68
|
+
|
|
69
|
+
const snapshot = {
|
|
70
|
+
projectName: path.basename(projectRoot),
|
|
71
|
+
healthy: true,
|
|
72
|
+
issues,
|
|
73
|
+
dbExists: !!dbStat,
|
|
74
|
+
dbMtime: dbStat ? dbStat.mtimeMs : null,
|
|
75
|
+
dbSize: dbStat ? dbStat.size : null,
|
|
76
|
+
bitmapExists: !!bmStat,
|
|
77
|
+
bitmapMtime: bmStat ? bmStat.mtimeMs : null,
|
|
78
|
+
bitmapStale: false,
|
|
79
|
+
schemaVersion: null,
|
|
80
|
+
totalFiles: 0,
|
|
81
|
+
totalRoutes: 0,
|
|
82
|
+
totalImportEdges: 0,
|
|
83
|
+
domains: [],
|
|
84
|
+
extractionErrors: 0,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
if (!snapshot.dbExists) {
|
|
88
|
+
snapshot.healthy = false;
|
|
89
|
+
issues.push(`No index at ${dbPath} — run \`carto init\`.`);
|
|
90
|
+
return snapshot;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// bitmap freshness — stale if missing or older than the DB. This is
|
|
94
|
+
// a *warning* (surfaced under issues), not a `healthy=false` trigger,
|
|
95
|
+
// because lazy-rebuild on next MCP query is the documented design —
|
|
96
|
+
// a missing/stale bitmap doesn't break anything, just costs the first
|
|
97
|
+
// query a few extra ms.
|
|
98
|
+
if (!snapshot.bitmapExists) {
|
|
99
|
+
snapshot.bitmapStale = true;
|
|
100
|
+
issues.push('Bitmap sidecar missing — will rebuild on next MCP query.');
|
|
101
|
+
} else if (snapshot.bitmapMtime < snapshot.dbMtime) {
|
|
102
|
+
snapshot.bitmapStale = true;
|
|
103
|
+
issues.push('Bitmap sidecar older than DB — will rebuild on next MCP query.');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Open SQLite readonly to harvest the rest.
|
|
107
|
+
let storeOpenFailed = false;
|
|
108
|
+
let store;
|
|
109
|
+
try {
|
|
110
|
+
store = new SQLiteStore(projectRoot);
|
|
111
|
+
store.open({ readonly: true });
|
|
112
|
+
const structure = store.getStructure();
|
|
113
|
+
// Schema version is a row in the `meta` table, not part of getStructure().
|
|
114
|
+
const sv = store.getMeta && store.getMeta('schema_version');
|
|
115
|
+
snapshot.schemaVersion = sv ? parseInt(sv, 10) : null;
|
|
116
|
+
snapshot.totalFiles = (structure.meta && structure.meta.totalFiles) || 0;
|
|
117
|
+
snapshot.totalRoutes = (structure.meta && structure.meta.totalRoutes) || 0;
|
|
118
|
+
snapshot.totalImportEdges = (structure.meta && structure.meta.totalImportEdges) || 0;
|
|
119
|
+
snapshot.domains = (store.getDomainsList() || []).map((d) => ({
|
|
120
|
+
name: d.name,
|
|
121
|
+
fileCount: d.fileCount,
|
|
122
|
+
}));
|
|
123
|
+
// Extraction errors — older DBs don't have the table; tolerate it.
|
|
124
|
+
try {
|
|
125
|
+
const row = store.db.prepare('SELECT COUNT(*) AS c FROM extraction_errors').get();
|
|
126
|
+
snapshot.extractionErrors = row.c || 0;
|
|
127
|
+
if (snapshot.extractionErrors > 0) {
|
|
128
|
+
issues.push(`${snapshot.extractionErrors} files had extractor errors — run \`carto inspect\`.`);
|
|
129
|
+
}
|
|
130
|
+
} catch { /* pre-v2 schema */ }
|
|
131
|
+
} catch (err) {
|
|
132
|
+
storeOpenFailed = true;
|
|
133
|
+
issues.push(`Could not open index: ${err.message}`);
|
|
134
|
+
} finally {
|
|
135
|
+
try { store && store.close(); } catch {}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// healthy=false only when something actually broken — DB missing or
|
|
139
|
+
// open failed. Warnings (bitmap stale, extraction errors) live in
|
|
140
|
+
// `issues` for visibility without flipping the healthy flag.
|
|
141
|
+
if (storeOpenFailed) snapshot.healthy = false;
|
|
142
|
+
return snapshot;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function render(snapshot) {
|
|
146
|
+
const lines = [];
|
|
147
|
+
const head = `carto · ${snapshot.projectName} · ${snapshot.totalFiles} files · ${snapshot.domains.length} domains`;
|
|
148
|
+
lines.push(head);
|
|
149
|
+
lines.push('─'.repeat(Math.min(head.length, 60)));
|
|
150
|
+
|
|
151
|
+
const ok = (b) => b ? '✓' : '✗';
|
|
152
|
+
if (!snapshot.dbExists) {
|
|
153
|
+
lines.push('Index ✗ missing — run `carto init`');
|
|
154
|
+
return lines.join('\n');
|
|
155
|
+
}
|
|
156
|
+
lines.push(`Index ${ok(snapshot.healthy)} ${snapshot.healthy ? 'healthy' : 'has issues'}`);
|
|
157
|
+
lines.push(`Synced ${formatAge(snapshot.dbMtime)} (${snapshot.totalFiles} files, ${snapshot.totalRoutes} routes, ${snapshot.totalImportEdges} imports)`);
|
|
158
|
+
lines.push(`Bitmap ${snapshot.bitmapStale ? '⚠ stale (rebuilds on next query)' : '✓ fresh'}`);
|
|
159
|
+
if (snapshot.schemaVersion !== null) {
|
|
160
|
+
lines.push(`Schema v${snapshot.schemaVersion}`);
|
|
161
|
+
}
|
|
162
|
+
lines.push(`Errors ${snapshot.extractionErrors} extraction breadcrumb${snapshot.extractionErrors === 1 ? '' : 's'}`);
|
|
163
|
+
|
|
164
|
+
if (snapshot.domains.length > 0) {
|
|
165
|
+
lines.push('');
|
|
166
|
+
lines.push('Top domains:');
|
|
167
|
+
const top = snapshot.domains.slice().sort((a, b) => b.fileCount - a.fileCount).slice(0, 6);
|
|
168
|
+
const widest = Math.max(...top.map((d) => d.name.length), 6);
|
|
169
|
+
for (const d of top) {
|
|
170
|
+
lines.push(` ${d.name.padEnd(widest)} (${d.fileCount} file${d.fileCount === 1 ? '' : 's'})`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (snapshot.issues.length > 0) {
|
|
175
|
+
lines.push('');
|
|
176
|
+
lines.push('Issues:');
|
|
177
|
+
for (const i of snapshot.issues) lines.push(` • ${i}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
lines.push('');
|
|
181
|
+
lines.push('Use `carto inspect` for the full breakdown.');
|
|
182
|
+
return lines.join('\n');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function run({ argv, stdout, stderr, projectRoot } = {}) {
|
|
186
|
+
argv = argv || process.argv.slice(3);
|
|
187
|
+
stdout = stdout || process.stdout;
|
|
188
|
+
stderr = stderr || process.stderr;
|
|
189
|
+
projectRoot = projectRoot || process.cwd();
|
|
190
|
+
|
|
191
|
+
const json = argv.includes('--json');
|
|
192
|
+
const help = argv.includes('--help') || argv.includes('-h');
|
|
193
|
+
if (help) {
|
|
194
|
+
stdout.write(`\nUsage: carto status [--json]\n\nQuick one-screen health view of the carto index.\nUse \`carto inspect\` for deeper diagnostics.\n\n`);
|
|
195
|
+
return 0;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let snapshot;
|
|
199
|
+
try { snapshot = collect(projectRoot); }
|
|
200
|
+
catch (err) {
|
|
201
|
+
stderr.write(`[CARTO] ${err.message}\n`);
|
|
202
|
+
return 1;
|
|
203
|
+
}
|
|
204
|
+
if (json) stdout.write(JSON.stringify(snapshot, null, 2) + '\n');
|
|
205
|
+
else stdout.write(render(snapshot) + '\n');
|
|
206
|
+
return snapshot.healthy ? 0 : 1;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
module.exports = { run, collect, render };
|
|
210
|
+
|
|
211
|
+
if (require.main === module) process.exit(run());
|
package/src/cli/sync.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const {
|
|
3
|
+
const { runSync } = require('../store/sync');
|
|
4
4
|
const { checkForUpdate } = require('./update-check');
|
|
5
5
|
|
|
6
6
|
async function run(projectRoot) {
|
|
@@ -20,7 +20,7 @@ async function run(projectRoot) {
|
|
|
20
20
|
process.exit(1);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
await
|
|
23
|
+
await runSync({
|
|
24
24
|
projectRoot,
|
|
25
25
|
output: path.resolve(projectRoot, config.output || 'AGENTS.md')
|
|
26
26
|
});
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `carto temporal` — temporal-layer CLI commands.
|
|
5
|
+
*
|
|
6
|
+
* Subcommands:
|
|
7
|
+
* carto temporal init — backfill from git history
|
|
8
|
+
* carto temporal status — show snapshot + commit + event counts
|
|
9
|
+
* carto temporal events — list recent architectural events
|
|
10
|
+
*
|
|
11
|
+
* The temporal store is auto-captured on every `carto sync` once initialized.
|
|
12
|
+
* `init` is one-time bootstrap to populate `file_churn` from git history.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const { TemporalStore } = require('../temporal/store');
|
|
18
|
+
const { backfillFromGit } = require('../temporal/backfill');
|
|
19
|
+
|
|
20
|
+
async function runInit({ since = null, maxCommits = 2000, projectRoot = process.cwd() } = {}) {
|
|
21
|
+
const cartoDir = path.join(projectRoot, '.carto');
|
|
22
|
+
if (!fs.existsSync(cartoDir)) {
|
|
23
|
+
console.error('[CARTO] No .carto/ found. Run `carto init` first.');
|
|
24
|
+
process.exit(2);
|
|
25
|
+
}
|
|
26
|
+
console.log('[CARTO] Backfilling temporal data from git history...');
|
|
27
|
+
const result = await backfillFromGit({
|
|
28
|
+
projectRoot,
|
|
29
|
+
maxCommits,
|
|
30
|
+
since,
|
|
31
|
+
onProgress: ({ done, files }) => {
|
|
32
|
+
process.stdout.write(`\r[CARTO] ${done} commits, ${files} file touches`);
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
if (result.errors.length > 0) process.stdout.write('\n');
|
|
36
|
+
else process.stdout.write('\n');
|
|
37
|
+
console.log(
|
|
38
|
+
`[CARTO] Backfill complete: ${result.commits} commits, ${result.files} file touches in ${result.elapsedMs}ms.`
|
|
39
|
+
);
|
|
40
|
+
if (result.errors.length > 0) {
|
|
41
|
+
for (const e of result.errors) console.warn(`[CARTO] ⚠ ${e}`);
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function runStatus({ projectRoot = process.cwd() } = {}) {
|
|
47
|
+
const temporal = TemporalStore.openIfExists(projectRoot, { readonly: true });
|
|
48
|
+
if (!temporal) {
|
|
49
|
+
console.log('[CARTO] No temporal database yet. Run `carto temporal init` to start.');
|
|
50
|
+
return { healthy: false, reason: 'missing' };
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const snaps = temporal.countSnapshots();
|
|
54
|
+
const commits = temporal.countCommits();
|
|
55
|
+
const recent = temporal.getMostRecentSnapshot();
|
|
56
|
+
const lastBackfill = temporal.getMeta('last_backfill_at');
|
|
57
|
+
const events = temporal.getArchEvents({ limit: 5 });
|
|
58
|
+
console.log(`# Carto Temporal Status\n`);
|
|
59
|
+
console.log(`- **Snapshots:** ${snaps}`);
|
|
60
|
+
console.log(`- **Commits:** ${commits}`);
|
|
61
|
+
if (recent) {
|
|
62
|
+
const when = new Date(recent.ts).toISOString();
|
|
63
|
+
console.log(`- **Latest:** ${when} (${recent.source})`);
|
|
64
|
+
}
|
|
65
|
+
if (lastBackfill) {
|
|
66
|
+
const when = new Date(parseInt(lastBackfill, 10)).toISOString();
|
|
67
|
+
console.log(`- **Last backfill:** ${when}`);
|
|
68
|
+
}
|
|
69
|
+
if (events.length > 0) {
|
|
70
|
+
console.log(`\n## Recent events`);
|
|
71
|
+
for (const e of events) {
|
|
72
|
+
const when = new Date(e.ts).toISOString();
|
|
73
|
+
const target = e.domain || e.file_path || '';
|
|
74
|
+
console.log(`- ${when} · ${e.severity} · ${e.kind}${target ? ` · ${target}` : ''}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { healthy: true, snapshots: snaps, commits, events: events.length };
|
|
78
|
+
} finally {
|
|
79
|
+
temporal.close();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function runEvents({ projectRoot = process.cwd(), severity = null, kind = null, timeRange = '90d', limit = 50 } = {}) {
|
|
84
|
+
const temporal = TemporalStore.openIfExists(projectRoot, { readonly: true });
|
|
85
|
+
if (!temporal) {
|
|
86
|
+
console.log('[CARTO] No temporal database. Run `carto temporal init`.');
|
|
87
|
+
return { events: [] };
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const { getArchEvents } = require('../temporal/queries');
|
|
91
|
+
const { events } = getArchEvents(temporal, { severity, kind, timeRange, limit });
|
|
92
|
+
if (events.length === 0) {
|
|
93
|
+
console.log(`No events in the last ${timeRange}.`);
|
|
94
|
+
return { events };
|
|
95
|
+
}
|
|
96
|
+
console.log(`# Architectural Events (${timeRange})\n`);
|
|
97
|
+
console.log(`| When | Severity | Kind | Target | Detail |`);
|
|
98
|
+
console.log(`|------|----------|------|--------|--------|`);
|
|
99
|
+
for (const e of events) {
|
|
100
|
+
const when = new Date(e.ts).toISOString();
|
|
101
|
+
const target = e.domain || e.file_path || '';
|
|
102
|
+
let detail = '';
|
|
103
|
+
if (e.detail_json) {
|
|
104
|
+
try {
|
|
105
|
+
const parsed = JSON.parse(e.detail_json);
|
|
106
|
+
detail = Object.entries(parsed).slice(0, 3).map(([k, v]) => `${k}=${v}`).join(', ');
|
|
107
|
+
} catch {}
|
|
108
|
+
}
|
|
109
|
+
console.log(`| ${when} | ${e.severity} | ${e.kind} | ${target} | ${detail} |`);
|
|
110
|
+
}
|
|
111
|
+
return { events };
|
|
112
|
+
} finally {
|
|
113
|
+
temporal.close();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function help() {
|
|
118
|
+
console.log(`carto temporal <subcommand>
|
|
119
|
+
|
|
120
|
+
Subcommands:
|
|
121
|
+
init [--since <range>] [--max-commits N]
|
|
122
|
+
Backfill from git history.
|
|
123
|
+
status Show snapshot/commit counts + recent events.
|
|
124
|
+
events [--severity LEVEL] [--kind KIND] [--time-range RANGE]
|
|
125
|
+
List architectural events.
|
|
126
|
+
|
|
127
|
+
Examples:
|
|
128
|
+
carto temporal init
|
|
129
|
+
carto temporal init --since "1 year ago"
|
|
130
|
+
carto temporal init --max-commits 500
|
|
131
|
+
carto temporal status
|
|
132
|
+
carto temporal events --severity critical
|
|
133
|
+
`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function main(args) {
|
|
137
|
+
const sub = args[0];
|
|
138
|
+
if (!sub || sub === 'help' || sub === '--help' || sub === '-h') {
|
|
139
|
+
help();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const opts = parseFlags(args.slice(1));
|
|
144
|
+
|
|
145
|
+
if (sub === 'init') {
|
|
146
|
+
await runInit({
|
|
147
|
+
since: opts['since'] || null,
|
|
148
|
+
maxCommits: opts['max-commits'] ? parseInt(opts['max-commits'], 10) : 2000,
|
|
149
|
+
});
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (sub === 'status') {
|
|
153
|
+
runStatus({});
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (sub === 'events') {
|
|
157
|
+
runEvents({
|
|
158
|
+
severity: opts['severity'] || null,
|
|
159
|
+
kind: opts['kind'] || null,
|
|
160
|
+
timeRange: opts['time-range'] || '90d',
|
|
161
|
+
limit: opts['limit'] ? parseInt(opts['limit'], 10) : 50,
|
|
162
|
+
});
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
console.error(`Unknown subcommand: ${sub}`);
|
|
166
|
+
help();
|
|
167
|
+
process.exit(2);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function parseFlags(args) {
|
|
171
|
+
const out = {};
|
|
172
|
+
for (let i = 0; i < args.length; i++) {
|
|
173
|
+
const a = args[i];
|
|
174
|
+
if (a.startsWith('--')) {
|
|
175
|
+
const key = a.slice(2);
|
|
176
|
+
const next = args[i + 1];
|
|
177
|
+
if (next && !next.startsWith('--')) {
|
|
178
|
+
out[key] = next;
|
|
179
|
+
i++;
|
|
180
|
+
} else {
|
|
181
|
+
out[key] = true;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
module.exports = { main, runInit, runStatus, runEvents };
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `carto validate` — diff-shaped validation CLI.
|
|
6
|
+
*
|
|
7
|
+
* Reads a unified diff from stdin (or `--diff-file <path>`) and prints
|
|
8
|
+
* the JSON result of `validateDiff()` to stdout. Designed for IDE
|
|
9
|
+
* extensions and middleware proxies that already speak diffs but don't
|
|
10
|
+
* want to import Carto internals.
|
|
11
|
+
*
|
|
12
|
+
* Contract:
|
|
13
|
+
*
|
|
14
|
+
* Input — unified diff text on stdin.
|
|
15
|
+
* Output — single JSON object on stdout (per `validateDiff()` shape).
|
|
16
|
+
* Exit — 0 on success regardless of risk, 1 on error (no index, bad
|
|
17
|
+
* input, etc.), 2 when `--fail-on <severity>` trips.
|
|
18
|
+
*
|
|
19
|
+
* Read-only: opens the SQLite store with readonly=true and never
|
|
20
|
+
* records side-effects to the episodic-memory tables. That persistence
|
|
21
|
+
* is owned by the MCP `validate_diff` tool — the IDE/middleware paths
|
|
22
|
+
* are inherently read-only consumers.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const path = require('path');
|
|
27
|
+
const { SQLiteStore } = require('../store/sqlite-store');
|
|
28
|
+
const { ensureBitmapFresh } = require('../bitmap/index');
|
|
29
|
+
const { validateDiff } = require('../mcp/validate');
|
|
30
|
+
|
|
31
|
+
const RISK_RANK = { SAFE: 0, LOW: 1, MEDIUM: 2, HIGH: 3 };
|
|
32
|
+
|
|
33
|
+
function parseArgs(argv) {
|
|
34
|
+
const args = {
|
|
35
|
+
diffFile: null,
|
|
36
|
+
projectRoot: process.cwd(),
|
|
37
|
+
failOn: null,
|
|
38
|
+
pretty: false,
|
|
39
|
+
help: false,
|
|
40
|
+
};
|
|
41
|
+
for (let i = 0; i < argv.length; i++) {
|
|
42
|
+
const a = argv[i];
|
|
43
|
+
switch (a) {
|
|
44
|
+
case '--diff-file': args.diffFile = argv[++i]; break;
|
|
45
|
+
case '--project': args.projectRoot = path.resolve(argv[++i]); break;
|
|
46
|
+
case '--fail-on': args.failOn = (argv[++i] || '').toUpperCase(); break;
|
|
47
|
+
case '--pretty': args.pretty = true; break;
|
|
48
|
+
case '--help':
|
|
49
|
+
case '-h': args.help = true; break;
|
|
50
|
+
default:
|
|
51
|
+
if (a.startsWith('--')) throw new Error(`unknown flag: ${a}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (args.failOn && !['HIGH', 'MEDIUM', 'LOW'].includes(args.failOn)) {
|
|
55
|
+
throw new Error(`--fail-on must be HIGH | MEDIUM | LOW (got '${args.failOn}')`);
|
|
56
|
+
}
|
|
57
|
+
return args;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function printUsage(stream) {
|
|
61
|
+
(stream || process.stdout).write(`
|
|
62
|
+
Usage: carto validate [options]
|
|
63
|
+
|
|
64
|
+
Reads a unified diff from stdin (or --diff-file) and prints the
|
|
65
|
+
validateDiff() JSON result to stdout. Used by carto's MCP middleware,
|
|
66
|
+
the VS Code extension, and any custom IDE integration.
|
|
67
|
+
|
|
68
|
+
Options:
|
|
69
|
+
--diff-file <path> Read diff from a file instead of stdin
|
|
70
|
+
--project <path> Project root (default: cwd)
|
|
71
|
+
--fail-on <severity> Exit 2 if risk >= severity (HIGH | MEDIUM | LOW)
|
|
72
|
+
--pretty Pretty-print the JSON output (default: compact)
|
|
73
|
+
--help, -h Show this help
|
|
74
|
+
|
|
75
|
+
Exit codes:
|
|
76
|
+
0 Normal — JSON written to stdout.
|
|
77
|
+
1 Misuse, missing index, or read error.
|
|
78
|
+
2 --fail-on threshold tripped.
|
|
79
|
+
|
|
80
|
+
JSON shape:
|
|
81
|
+
{ diff, blast_radius, violations, suggestions, risk }
|
|
82
|
+
See src/mcp/validate.js for full field documentation.
|
|
83
|
+
|
|
84
|
+
`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* readStdin() → Promise<string>
|
|
89
|
+
*
|
|
90
|
+
* Collect stdin until EOF. Done as a one-shot because validate is a
|
|
91
|
+
* batch operation — IDEs / middleware build a complete diff before
|
|
92
|
+
* shelling out, they don't stream.
|
|
93
|
+
*/
|
|
94
|
+
function readStdin() {
|
|
95
|
+
return new Promise((resolve, reject) => {
|
|
96
|
+
let data = '';
|
|
97
|
+
process.stdin.setEncoding('utf8');
|
|
98
|
+
process.stdin.on('data', (chunk) => { data += chunk; });
|
|
99
|
+
process.stdin.on('end', () => resolve(data));
|
|
100
|
+
process.stdin.on('error', reject);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* computeValidation(projectRoot, diffText) → result
|
|
106
|
+
*
|
|
107
|
+
* Pure function over a project root + diff. Opens the SQLite store
|
|
108
|
+
* read-only, builds (or reuses) the bitmap sidecar, runs validateDiff,
|
|
109
|
+
* closes the store, returns.
|
|
110
|
+
*
|
|
111
|
+
* If the bitmap can't be built (no index, schema mismatch, etc.), we
|
|
112
|
+
* fall through with sidecar=null — validateDiff degrades gracefully to
|
|
113
|
+
* no-blast-radius output rather than crashing.
|
|
114
|
+
*/
|
|
115
|
+
function computeValidation(projectRoot, diffText) {
|
|
116
|
+
const cartoDir = path.join(projectRoot, '.carto');
|
|
117
|
+
const dbPath = path.join(cartoDir, 'carto.db');
|
|
118
|
+
if (!fs.existsSync(dbPath)) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`No carto index at ${dbPath}. Run \`carto init\` first.`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
const store = new SQLiteStore(projectRoot);
|
|
124
|
+
store.open({ readonly: true });
|
|
125
|
+
let sidecar = null;
|
|
126
|
+
try {
|
|
127
|
+
sidecar = ensureBitmapFresh(cartoDir, store);
|
|
128
|
+
} catch {
|
|
129
|
+
// No bitmap → no blast radius, but the diff parser + cross-domain
|
|
130
|
+
// rules still produce useful output.
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
return validateDiff(store, sidecar, diffText);
|
|
134
|
+
} finally {
|
|
135
|
+
store.close();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* run({ argv, stdout, stderr, stdin }) → Promise<exitCode>
|
|
141
|
+
*
|
|
142
|
+
* Pure-ish entry point so tests can pass synthetic streams + capture
|
|
143
|
+
* stdout. Caller decides whether to `process.exit(code)`.
|
|
144
|
+
*/
|
|
145
|
+
async function run({ argv, stdout, stderr, stdin } = {}) {
|
|
146
|
+
argv = argv || process.argv.slice(3);
|
|
147
|
+
stdout = stdout || process.stdout;
|
|
148
|
+
stderr = stderr || process.stderr;
|
|
149
|
+
|
|
150
|
+
let args;
|
|
151
|
+
try { args = parseArgs(argv); }
|
|
152
|
+
catch (err) { stderr.write(`[CARTO] ${err.message}\n`); return 1; }
|
|
153
|
+
|
|
154
|
+
if (args.help) { printUsage(stdout); return 0; }
|
|
155
|
+
|
|
156
|
+
let diffText;
|
|
157
|
+
try {
|
|
158
|
+
if (args.diffFile) {
|
|
159
|
+
diffText = fs.readFileSync(args.diffFile, 'utf8');
|
|
160
|
+
} else if (stdin && typeof stdin.read === 'function' && stdin.isString) {
|
|
161
|
+
// Test path: caller passed a synthetic stdin shim with a string.
|
|
162
|
+
diffText = stdin.read();
|
|
163
|
+
} else {
|
|
164
|
+
diffText = await readStdin();
|
|
165
|
+
}
|
|
166
|
+
} catch (err) {
|
|
167
|
+
stderr.write(`[CARTO] failed to read diff: ${err.message}\n`);
|
|
168
|
+
return 1;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let result;
|
|
172
|
+
try {
|
|
173
|
+
result = computeValidation(args.projectRoot, diffText);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
stderr.write(`[CARTO] ${err.message}\n`);
|
|
176
|
+
return 1;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const out = args.pretty
|
|
180
|
+
? JSON.stringify(result, null, 2) + '\n'
|
|
181
|
+
: JSON.stringify(result) + '\n';
|
|
182
|
+
stdout.write(out);
|
|
183
|
+
|
|
184
|
+
if (args.failOn && RISK_RANK[result.risk] >= RISK_RANK[args.failOn]) return 2;
|
|
185
|
+
return 0;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
module.exports = {
|
|
189
|
+
run,
|
|
190
|
+
parseArgs,
|
|
191
|
+
computeValidation,
|
|
192
|
+
RISK_RANK,
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
if (require.main === module) {
|
|
196
|
+
run().then((code) => process.exit(code))
|
|
197
|
+
.catch((err) => {
|
|
198
|
+
process.stderr.write(`[CARTO] Fatal: ${err.message}\n`);
|
|
199
|
+
process.exit(1);
|
|
200
|
+
});
|
|
201
|
+
}
|
package/src/cli/watch.js
CHANGED
|
@@ -4,8 +4,8 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const chokidar = require('chokidar');
|
|
6
6
|
const { SQLiteStore } = require('../store/sqlite-store');
|
|
7
|
-
const { discoverFiles, generateOutputs, buildRoutesByFile, detectLanguage } = require('../store/sync
|
|
8
|
-
const {
|
|
7
|
+
const { discoverFiles, generateOutputs, buildRoutesByFile, detectLanguage } = require('../store/sync');
|
|
8
|
+
const { runSync } = require('../store/sync');
|
|
9
9
|
const { detectChangedFiles, hashFile, hashContent } = require('../store/change-detector');
|
|
10
10
|
const { loadLanguagePlugins, getPluginForFile } = require('../extractors/loader');
|
|
11
11
|
const { extractImports } = require('../extractors/imports');
|
|
@@ -177,7 +177,7 @@ async function run(projectRoot) {
|
|
|
177
177
|
|
|
178
178
|
// Initial full sync using V2
|
|
179
179
|
console.log('[CARTO] Starting initial sync...');
|
|
180
|
-
await
|
|
180
|
+
await runSync({
|
|
181
181
|
projectRoot,
|
|
182
182
|
output: path.join(projectRoot, 'AGENTS.md')
|
|
183
183
|
});
|
|
@@ -222,7 +222,7 @@ async function run(projectRoot) {
|
|
|
222
222
|
domainsDirty = false;
|
|
223
223
|
console.log('[CARTO] Idle 5min — reclustering domains...');
|
|
224
224
|
try {
|
|
225
|
-
await
|
|
225
|
+
await runSync({ projectRoot, output: path.join(projectRoot, 'AGENTS.md') });
|
|
226
226
|
console.log('[CARTO] Domain recluster complete.');
|
|
227
227
|
} catch (err) {
|
|
228
228
|
console.error(`[CARTO] Recluster error: ${err.message}`);
|