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,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 { runSyncV2 } = require('../store/sync-v2');
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 runSyncV2({
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-v2');
8
- const { runSyncV2 } = require('../store/sync-v2');
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 runSyncV2({
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 runSyncV2({ projectRoot, output: path.join(projectRoot, 'AGENTS.md') });
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}`);