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
@@ -20,7 +20,7 @@
20
20
  * microseconds; the budget is for diff parsing + result aggregation.
21
21
  *
22
22
  * Persistence: `recordSideEffects` is a separate function so
23
- * the read-only MCP `getStore()` path stays read-only — server-v2 opens
23
+ * the read-only MCP `getStore()` path stays read-only — server opens
24
24
  * a brief writer connection, calls validateDiff first (read-only), then
25
25
  * recordSideEffects, then closes. Tests can pass a writer directly as
26
26
  * `opts.writer` to inspect the rows produced.
@@ -0,0 +1,262 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Seven cross-repo edge detection methods.
5
+ *
6
+ * 1. npm scope — `@company/*` in package.json + import statements
7
+ * 2. PyPI — internal-pypi-prefix from pyproject.toml imports
8
+ * 3. go-mod — `go.mod` `require` blocks (private module prefix)
9
+ * 4. Maven group ID — `pom.xml` <groupId> prefix
10
+ * 5. gRPC — `.proto` files importing other `.proto` files
11
+ * 6. OpenAPI — `openapi.yaml` / `swagger.json` schema sharing
12
+ * 7. Shared DB tables — same table names across SQL migration files
13
+ *
14
+ * Each detector returns Array<{ edge_kind, from_file?, target, to_repo?, detail }>.
15
+ * `to_repo` is left null when we can't resolve the producer repo from the
16
+ * target alone; the org-sync step does a second pass to fill it in
17
+ * (target → repo lookup via npm name, pypi name, and so on).
18
+ */
19
+
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+
23
+ const CODE_EXTS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.py']);
24
+
25
+ /**
26
+ * scanRepo({ repoRoot, scopes }) → Array<edge>
27
+ *
28
+ * `scopes` is the runtime config: known internal npm scopes, pypi
29
+ * prefixes, go module prefixes, maven group prefixes. The detector
30
+ * filters to edges that match those scopes.
31
+ *
32
+ * scopes = {
33
+ * npm: ['@mycompany'],
34
+ * pypi: ['mycompany-'],
35
+ * go: ['github.com/mycompany'],
36
+ * maven: ['com.mycompany'],
37
+ * }
38
+ */
39
+ function scanRepo({ repoRoot, scopes = {} }) {
40
+ if (!repoRoot) return [];
41
+ const out = [];
42
+ out.push(...detectNpm(repoRoot, scopes.npm || []));
43
+ out.push(...detectPython(repoRoot, scopes.pypi || []));
44
+ out.push(...detectGo(repoRoot, scopes.go || []));
45
+ out.push(...detectMaven(repoRoot, scopes.maven || []));
46
+ out.push(...detectProto(repoRoot));
47
+ out.push(...detectOpenApi(repoRoot));
48
+ out.push(...detectSqlMigrations(repoRoot));
49
+ return out;
50
+ }
51
+
52
+ // ── 1. npm ────────────────────────────────────────────────────────
53
+ function detectNpm(repoRoot, scopes) {
54
+ const out = [];
55
+ if (scopes.length === 0) return out;
56
+
57
+ // From package.json deps + devDeps.
58
+ const pkgPath = path.join(repoRoot, 'package.json');
59
+ if (fs.existsSync(pkgPath)) {
60
+ try {
61
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
62
+ const all = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
63
+ for (const name of Object.keys(all)) {
64
+ if (scopes.some(sc => name.startsWith(sc + '/') || name === sc)) {
65
+ out.push({ edge_kind: 'npm', from_file: 'package.json', target: name, detail: { version: all[name] } });
66
+ }
67
+ }
68
+ } catch {}
69
+ }
70
+ // From import statements in source files (deeper than package.json).
71
+ walkSource(repoRoot, (rel, content) => {
72
+ const ext = path.extname(rel);
73
+ if (!['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'].includes(ext)) return;
74
+ // import x from '@scope/y' / require('@scope/y')
75
+ const re = /(?:from|require)\s*\(?\s*['"]([@\w][^'"]*)['"]/g;
76
+ let m;
77
+ while ((m = re.exec(content)) !== null) {
78
+ const spec = m[1];
79
+ if (scopes.some(sc => spec === sc || spec.startsWith(sc + '/'))) {
80
+ // Normalize target: @scope/pkg or @scope/pkg/sub → @scope/pkg
81
+ const parts = spec.split('/');
82
+ const target = parts[0].startsWith('@') ? `${parts[0]}/${parts[1] || ''}` : parts[0];
83
+ out.push({ edge_kind: 'npm', from_file: rel, target });
84
+ }
85
+ }
86
+ });
87
+ return dedupe(out);
88
+ }
89
+
90
+ // ── 2. PyPI ──────────────────────────────────────────────────────
91
+ function detectPython(repoRoot, prefixes) {
92
+ const out = [];
93
+ if (prefixes.length === 0) return out;
94
+
95
+ // pyproject.toml dependencies
96
+ const pyproject = path.join(repoRoot, 'pyproject.toml');
97
+ if (fs.existsSync(pyproject)) {
98
+ try {
99
+ const text = fs.readFileSync(pyproject, 'utf-8');
100
+ // Match `name = "..."` lines under [tool.poetry.dependencies] or [project.dependencies]
101
+ const re = /^\s*([a-zA-Z][\w\-]+)\s*=\s*["']([^"']+)["']/gm;
102
+ let m;
103
+ while ((m = re.exec(text)) !== null) {
104
+ if (prefixes.some(p => m[1].startsWith(p))) {
105
+ out.push({ edge_kind: 'pypi', from_file: 'pyproject.toml', target: m[1], detail: { version: m[2] } });
106
+ }
107
+ }
108
+ } catch {}
109
+ }
110
+ // Source files: from <prefix>foo import ...
111
+ walkSource(repoRoot, (rel, content) => {
112
+ if (!rel.endsWith('.py')) return;
113
+ const re = /^(?:import\s+|from\s+)([A-Za-z][\w.]+)/gm;
114
+ let m;
115
+ while ((m = re.exec(content)) !== null) {
116
+ const mod = m[1];
117
+ const topLevel = mod.split('.')[0];
118
+ if (prefixes.some(p => topLevel.startsWith(p))) {
119
+ out.push({ edge_kind: 'pypi', from_file: rel, target: topLevel });
120
+ }
121
+ }
122
+ });
123
+ return dedupe(out);
124
+ }
125
+
126
+ // ── 3. Go ────────────────────────────────────────────────────────
127
+ function detectGo(repoRoot, prefixes) {
128
+ const out = [];
129
+ if (prefixes.length === 0) return out;
130
+ const gomod = path.join(repoRoot, 'go.mod');
131
+ if (fs.existsSync(gomod)) {
132
+ try {
133
+ const text = fs.readFileSync(gomod, 'utf-8');
134
+ const block = /require\s*\(([^)]*)\)/m.exec(text);
135
+ const lines = block ? block[1].split('\n') : text.split('\n');
136
+ for (const line of lines) {
137
+ const m = /^\s*([\w./\-]+)\s+(\S+)/.exec(line);
138
+ if (m && prefixes.some(p => m[1].startsWith(p))) {
139
+ out.push({ edge_kind: 'go-mod', from_file: 'go.mod', target: m[1], detail: { version: m[2] } });
140
+ }
141
+ }
142
+ } catch {}
143
+ }
144
+ return dedupe(out);
145
+ }
146
+
147
+ // ── 4. Maven ─────────────────────────────────────────────────────
148
+ function detectMaven(repoRoot, prefixes) {
149
+ const out = [];
150
+ if (prefixes.length === 0) return out;
151
+ const pom = path.join(repoRoot, 'pom.xml');
152
+ if (fs.existsSync(pom)) {
153
+ try {
154
+ const text = fs.readFileSync(pom, 'utf-8');
155
+ const re = /<dependency>[\s\S]*?<groupId>([^<]+)<\/groupId>\s*<artifactId>([^<]+)<\/artifactId>(?:\s*<version>([^<]+)<\/version>)?/g;
156
+ let m;
157
+ while ((m = re.exec(text)) !== null) {
158
+ if (prefixes.some(p => m[1].startsWith(p))) {
159
+ out.push({
160
+ edge_kind: 'maven',
161
+ from_file: 'pom.xml',
162
+ target: `${m[1]}:${m[2]}`,
163
+ detail: { version: m[3] || null },
164
+ });
165
+ }
166
+ }
167
+ } catch {}
168
+ }
169
+ return dedupe(out);
170
+ }
171
+
172
+ // ── 5. gRPC / .proto ─────────────────────────────────────────────
173
+ function detectProto(repoRoot) {
174
+ const out = [];
175
+ walkSource(repoRoot, (rel, content) => {
176
+ if (!rel.endsWith('.proto')) return;
177
+ const re = /^\s*import\s+["']([^"']+)["']/gm;
178
+ let m;
179
+ while ((m = re.exec(content)) !== null) {
180
+ out.push({ edge_kind: 'grpc', from_file: rel, target: m[1] });
181
+ }
182
+ });
183
+ return dedupe(out);
184
+ }
185
+
186
+ // ── 6. OpenAPI / Swagger ────────────────────────────────────────
187
+ function detectOpenApi(repoRoot) {
188
+ const out = [];
189
+ walkSource(repoRoot, (rel, content) => {
190
+ const lower = rel.toLowerCase();
191
+ if (!/openapi\.(ya?ml|json)$|swagger\.(ya?ml|json)$/.test(lower)) return;
192
+ // Best-effort: capture `$ref: '...'` external refs.
193
+ const re = /\$ref\s*:\s*['"]([^'"#]+)(?:#[^'"]*)?['"]/g;
194
+ let m;
195
+ while ((m = re.exec(content)) !== null) {
196
+ if (m[1].startsWith('http') || m[1].startsWith('../')) {
197
+ out.push({ edge_kind: 'openapi', from_file: rel, target: m[1] });
198
+ }
199
+ }
200
+ // Also surface the file itself as a schema-producer (for cross-repo
201
+ // consumption detection in the second pass).
202
+ out.push({ edge_kind: 'openapi', from_file: rel, target: rel, detail: { kind: 'producer' } });
203
+ });
204
+ return dedupe(out);
205
+ }
206
+
207
+ // ── 7. Shared SQL tables ───────────────────────────────────────
208
+ function detectSqlMigrations(repoRoot) {
209
+ const out = [];
210
+ walkSource(repoRoot, (rel, content) => {
211
+ const lower = rel.toLowerCase();
212
+ if (!lower.endsWith('.sql')) return;
213
+ if (!/migration|migrate/.test(lower)) return;
214
+ const re = /\bCREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["`]?(\w+)["`]?/gi;
215
+ let m;
216
+ while ((m = re.exec(content)) !== null) {
217
+ out.push({ edge_kind: 'db-table', from_file: rel, target: m[1] });
218
+ }
219
+ });
220
+ return dedupe(out);
221
+ }
222
+
223
+ // ── helpers ────────────────────────────────────────────────────
224
+ function walkSource(repoRoot, visit) {
225
+ const IGNORE_DIRS = new Set(['node_modules', '.git', 'dist', 'build', 'out', '.next', '.turbo', 'coverage', 'vendor']);
226
+ const walk = (dir) => {
227
+ let entries; try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
228
+ for (const e of entries) {
229
+ if (e.name.startsWith('.') && e.name !== '.proto') continue;
230
+ if (IGNORE_DIRS.has(e.name)) continue;
231
+ const full = path.join(dir, e.name);
232
+ if (e.isDirectory()) walk(full);
233
+ else if (e.isFile()) {
234
+ const rel = path.relative(repoRoot, full).replace(/\\/g, '/');
235
+ let content;
236
+ try {
237
+ const stat = fs.statSync(full);
238
+ if (stat.size > 4 * 1024 * 1024) return; // skip huge files
239
+ content = fs.readFileSync(full, 'utf-8');
240
+ } catch { continue; }
241
+ visit(rel, content);
242
+ }
243
+ }
244
+ };
245
+ walk(repoRoot);
246
+ }
247
+
248
+ function dedupe(edges) {
249
+ const seen = new Set();
250
+ return edges.filter(e => {
251
+ const key = `${e.edge_kind}::${e.from_file || ''}::${e.target}`;
252
+ if (seen.has(key)) return false;
253
+ seen.add(key);
254
+ return true;
255
+ });
256
+ }
257
+
258
+ module.exports = {
259
+ scanRepo,
260
+ detectNpm, detectPython, detectGo, detectMaven,
261
+ detectProto, detectOpenApi, detectSqlMigrations,
262
+ };
@@ -0,0 +1,144 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Org-wide query functions backing the 7 cross-repo MCP tools.
5
+ */
6
+
7
+ const path = require('path');
8
+ const fs = require('fs');
9
+
10
+ /**
11
+ * orgArchitectureOverview(orgStore) → { repos, summary }
12
+ */
13
+ function orgArchitectureOverview(orgStore) {
14
+ if (!orgStore || !orgStore.db) return { repos: [], summary: {} };
15
+ const repos = orgStore.listRepos();
16
+ const summary = {
17
+ total_repos: repos.length,
18
+ total_edges: orgStore.db.prepare('SELECT COUNT(*) as c FROM cross_repo_edges').get().c,
19
+ edges_by_kind: orgStore.db.prepare(`
20
+ SELECT edge_kind, COUNT(*) as c FROM cross_repo_edges GROUP BY edge_kind
21
+ `).all(),
22
+ };
23
+ return { repos, summary };
24
+ }
25
+
26
+ /**
27
+ * serviceDependencyGraph(orgStore) → { nodes, edges }
28
+ *
29
+ * Each repo is a node; cross-repo edges are aggregated by (from_repo, to_repo).
30
+ */
31
+ function serviceDependencyGraph(orgStore) {
32
+ if (!orgStore || !orgStore.db) return { nodes: [], edges: [] };
33
+ const repos = orgStore.listRepos();
34
+ const edgeRows = orgStore.db.prepare(`
35
+ SELECT from_repo, to_repo, edge_kind, COUNT(*) as count
36
+ FROM cross_repo_edges
37
+ WHERE to_repo IS NOT NULL
38
+ GROUP BY from_repo, to_repo, edge_kind
39
+ `).all();
40
+ return {
41
+ nodes: repos.map(r => ({ name: r.name, root_path: r.root_path })),
42
+ edges: edgeRows,
43
+ };
44
+ }
45
+
46
+ /**
47
+ * crossRepoBlastRadius(orgStore, fromRepo) → { downstream_repos, paths }
48
+ *
49
+ * Returns the set of repos that depend on `fromRepo` (one hop). Deeper
50
+ * hops would need a transitive closure pass; this is direct-only.
51
+ */
52
+ function crossRepoBlastRadius(orgStore, fromRepo) {
53
+ if (!orgStore || !orgStore.db || !fromRepo) return { downstream_repos: [], paths: [] };
54
+ const rows = orgStore.db.prepare(`
55
+ SELECT from_repo, edge_kind, target, COUNT(*) as count
56
+ FROM cross_repo_edges
57
+ WHERE to_repo = ?
58
+ GROUP BY from_repo, edge_kind
59
+ `).all(fromRepo);
60
+ const downstream = [...new Set(rows.map(r => r.from_repo))];
61
+ return { downstream_repos: downstream, paths: rows };
62
+ }
63
+
64
+ /**
65
+ * findConsumersOfApi(orgStore, target) → list of consuming repos
66
+ *
67
+ * "Who depends on this npm/pypi/go module?"
68
+ */
69
+ function findConsumersOfApi(orgStore, target) {
70
+ if (!orgStore || !orgStore.db || !target) return [];
71
+ return orgStore.db.prepare(`
72
+ SELECT from_repo, edge_kind, from_file
73
+ FROM cross_repo_edges
74
+ WHERE target = ? OR target LIKE ?
75
+ ORDER BY from_repo, edge_kind
76
+ `).all(target, target + '/%');
77
+ }
78
+
79
+ /**
80
+ * orgDomainMapping(orgStore) — for each repo's domain (from its carto.db
81
+ * if present), aggregate cross-domain edges org-wide.
82
+ */
83
+ function orgDomainMapping(orgStore) {
84
+ if (!orgStore || !orgStore.db) return { domains: [] };
85
+ const repos = orgStore.listRepos();
86
+ const domains = [];
87
+ for (const repo of repos) {
88
+ if (!repo.carto_db_path || !fs.existsSync(repo.carto_db_path)) continue;
89
+ try {
90
+ const Database = require('better-sqlite3');
91
+ const db = new Database(repo.carto_db_path, { readonly: true, fileMustExist: true });
92
+ const rows = db.prepare('SELECT name, file_count FROM domains').all();
93
+ for (const r of rows) domains.push({ repo: repo.name, domain: r.name, file_count: r.file_count });
94
+ db.close();
95
+ } catch {}
96
+ }
97
+ return { domains };
98
+ }
99
+
100
+ /**
101
+ * serviceBoundaryViolations(orgStore) → boundary-crossing edges where one
102
+ * repo reaches into another's private surface (heuristic: target path
103
+ * includes `internal`, `private`, `_lib`, etc.).
104
+ */
105
+ function serviceBoundaryViolations(orgStore) {
106
+ if (!orgStore || !orgStore.db) return { violations: [] };
107
+ const rows = orgStore.db.prepare(`
108
+ SELECT * FROM cross_repo_edges
109
+ WHERE target LIKE '%internal%' OR target LIKE '%private%' OR target LIKE '%_lib%'
110
+ OR target LIKE '%/_%'
111
+ `).all();
112
+ return { violations: rows };
113
+ }
114
+
115
+ /**
116
+ * microservicesMigrationCutPoints(orgStore) → suggested order to split
117
+ *
118
+ * Heuristic: repos with the fewest outgoing edges + the most incoming
119
+ * edges are the most stable producers — extract those first. Repos with
120
+ * many outgoing edges are tightly coupled consumers — extract later.
121
+ */
122
+ function microservicesMigrationCutPoints(orgStore) {
123
+ if (!orgStore || !orgStore.db) return { order: [] };
124
+ const repos = orgStore.listRepos();
125
+ const out = [];
126
+ for (const repo of repos) {
127
+ const outgoing = orgStore.db.prepare('SELECT COUNT(*) as c FROM cross_repo_edges WHERE from_repo = ?').get(repo.name).c;
128
+ const incoming = orgStore.db.prepare('SELECT COUNT(*) as c FROM cross_repo_edges WHERE to_repo = ?').get(repo.name).c;
129
+ const stability = incoming / Math.max(1, outgoing + incoming);
130
+ out.push({ repo: repo.name, outgoing, incoming, stability: Math.round(stability * 100) / 100 });
131
+ }
132
+ out.sort((a, b) => b.stability - a.stability);
133
+ return { order: out };
134
+ }
135
+
136
+ module.exports = {
137
+ orgArchitectureOverview,
138
+ serviceDependencyGraph,
139
+ crossRepoBlastRadius,
140
+ findConsumersOfApi,
141
+ orgDomainMapping,
142
+ serviceBoundaryViolations,
143
+ microservicesMigrationCutPoints,
144
+ };
@@ -0,0 +1,173 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * OrgStore — `~/.carto/org.db` for cross-repo Carto data.
5
+ *
6
+ * Shared SQLite for polyrepo / monorepo-of-monorepos setups. Lives in the
7
+ * user's home directory so multiple projects in `carto org` can reference
8
+ * the same federated dataset.
9
+ *
10
+ * Two tables:
11
+ * - `repos` registered repos (one row per `carto org add`)
12
+ * - `cross_repo_edges` detected edges (npm, pypi, go-mod, maven, grpc,
13
+ * openapi, db-table)
14
+ *
15
+ * Concurrency: WAL mode. Multiple `carto org sync` calls in parallel
16
+ * serialize on the SQLite busy_timeout.
17
+ */
18
+
19
+ const fs = require('fs');
20
+ const os = require('os');
21
+ const path = require('path');
22
+ const Database = require('better-sqlite3');
23
+
24
+ const ORG_DB_FILENAME = 'org.db';
25
+ const SCHEMA_VERSION = '1';
26
+
27
+ function defaultOrgDbDir() {
28
+ return path.join(os.homedir(), '.carto');
29
+ }
30
+
31
+ class OrgStore {
32
+ constructor(orgDir = defaultOrgDbDir()) {
33
+ this._dir = orgDir;
34
+ this._db = null;
35
+ }
36
+
37
+ get dbPath() { return path.join(this._dir, ORG_DB_FILENAME); }
38
+
39
+ open() {
40
+ fs.mkdirSync(this._dir, { recursive: true });
41
+ try {
42
+ this._db = new Database(this.dbPath);
43
+ } catch {
44
+ try { fs.unlinkSync(this.dbPath); } catch {}
45
+ this._db = new Database(this.dbPath);
46
+ }
47
+ this._db.pragma('journal_mode = WAL');
48
+ this._db.pragma('synchronous = NORMAL');
49
+ this._db.pragma('busy_timeout = 5000');
50
+ this._ensureSchema();
51
+ return this;
52
+ }
53
+
54
+ static openIfExists(orgDir = defaultOrgDbDir()) {
55
+ const p = path.join(orgDir, ORG_DB_FILENAME);
56
+ if (!fs.existsSync(p)) return null;
57
+ try { return new OrgStore(orgDir).open(); } catch { return null; }
58
+ }
59
+
60
+ close() {
61
+ if (this._db) { try { this._db.close(); } catch {} this._db = null; }
62
+ }
63
+
64
+ get db() { return this._db; }
65
+
66
+ _ensureSchema() {
67
+ this._db.exec(`
68
+ CREATE TABLE IF NOT EXISTS org_meta (
69
+ key TEXT PRIMARY KEY,
70
+ value TEXT
71
+ );
72
+ CREATE TABLE IF NOT EXISTS repos (
73
+ id INTEGER PRIMARY KEY,
74
+ name TEXT UNIQUE NOT NULL,
75
+ root_path TEXT NOT NULL,
76
+ carto_db_path TEXT,
77
+ added_at INTEGER,
78
+ last_sync_at INTEGER
79
+ );
80
+ CREATE INDEX IF NOT EXISTS idx_repos_name ON repos(name);
81
+
82
+ CREATE TABLE IF NOT EXISTS cross_repo_edges (
83
+ id INTEGER PRIMARY KEY,
84
+ from_repo TEXT NOT NULL,
85
+ to_repo TEXT,
86
+ edge_kind TEXT NOT NULL,
87
+ from_file TEXT,
88
+ target TEXT,
89
+ detail_json TEXT
90
+ );
91
+ CREATE INDEX IF NOT EXISTS idx_edge_from_repo ON cross_repo_edges(from_repo);
92
+ CREATE INDEX IF NOT EXISTS idx_edge_to_repo ON cross_repo_edges(to_repo);
93
+ CREATE INDEX IF NOT EXISTS idx_edge_kind ON cross_repo_edges(edge_kind);
94
+ CREATE INDEX IF NOT EXISTS idx_edge_target ON cross_repo_edges(target);
95
+ `);
96
+ const row = this._db.prepare('SELECT value FROM org_meta WHERE key = ?').get('schema_version');
97
+ if (!row) this._db.prepare('INSERT INTO org_meta(key, value) VALUES(?, ?)').run('schema_version', SCHEMA_VERSION);
98
+ }
99
+
100
+ addRepo({ name, rootPath, cartoDbPath = null }) {
101
+ if (!name || !rootPath) throw new Error('addRepo: name and rootPath required');
102
+ const now = Date.now();
103
+ try {
104
+ this._db.prepare(`
105
+ INSERT INTO repos (name, root_path, carto_db_path, added_at)
106
+ VALUES (?, ?, ?, ?)
107
+ `).run(name, rootPath, cartoDbPath, now);
108
+ return { name, rootPath, added_at: now };
109
+ } catch (err) {
110
+ if (/UNIQUE/.test(err.message)) {
111
+ this._db.prepare(`
112
+ UPDATE repos SET root_path = ?, carto_db_path = ? WHERE name = ?
113
+ `).run(rootPath, cartoDbPath, name);
114
+ return { name, rootPath, updated: true };
115
+ }
116
+ throw err;
117
+ }
118
+ }
119
+
120
+ removeRepo(name) {
121
+ this._db.prepare('DELETE FROM repos WHERE name = ?').run(name);
122
+ this._db.prepare('DELETE FROM cross_repo_edges WHERE from_repo = ? OR to_repo = ?').run(name, name);
123
+ }
124
+
125
+ listRepos() {
126
+ return this._db.prepare('SELECT * FROM repos ORDER BY name ASC').all();
127
+ }
128
+
129
+ getRepo(name) {
130
+ return this._db.prepare('SELECT * FROM repos WHERE name = ?').get(name);
131
+ }
132
+
133
+ /**
134
+ * insertEdges(repoName, edges) — bulk insert. Each edge:
135
+ * { edge_kind, from_file?, target, to_repo?, detail? }
136
+ *
137
+ * Replaces all existing edges where `from_repo = repoName` first so
138
+ * re-syncing a repo doesn't accumulate stale edges.
139
+ */
140
+ insertEdges(repoName, edges) {
141
+ const tx = this._db.transaction(() => {
142
+ this._db.prepare('DELETE FROM cross_repo_edges WHERE from_repo = ?').run(repoName);
143
+ const stmt = this._db.prepare(`
144
+ INSERT INTO cross_repo_edges (from_repo, to_repo, edge_kind, from_file, target, detail_json)
145
+ VALUES (?, ?, ?, ?, ?, ?)
146
+ `);
147
+ for (const e of edges || []) {
148
+ stmt.run(
149
+ repoName,
150
+ e.to_repo || null,
151
+ e.edge_kind,
152
+ e.from_file || null,
153
+ e.target,
154
+ e.detail ? JSON.stringify(e.detail) : null,
155
+ );
156
+ }
157
+ this._db.prepare('UPDATE repos SET last_sync_at = ? WHERE name = ?').run(Date.now(), repoName);
158
+ });
159
+ tx();
160
+ }
161
+
162
+ getEdges({ from_repo = null, to_repo = null, edge_kind = null } = {}) {
163
+ const where = [];
164
+ const args = [];
165
+ if (from_repo) { where.push('from_repo = ?'); args.push(from_repo); }
166
+ if (to_repo) { where.push('to_repo = ?'); args.push(to_repo); }
167
+ if (edge_kind) { where.push('edge_kind = ?'); args.push(edge_kind); }
168
+ const w = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
169
+ return this._db.prepare(`SELECT * FROM cross_repo_edges ${w} ORDER BY edge_kind, target`).all(...args);
170
+ }
171
+ }
172
+
173
+ module.exports = { OrgStore, ORG_DB_FILENAME, defaultOrgDbDir };
@@ -0,0 +1,106 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Cross-repo sync — walk every registered repo, run detection, persist edges.
5
+ *
6
+ * Second pass: target → repo resolution. We resolve `to_repo` for npm/
7
+ * pypi/go-mod/maven edges when the target's name matches a registered
8
+ * repo's package metadata. Edges remain even when unresolved (target is
9
+ * still useful for "find consumers of @scope/foo").
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const { OrgStore, defaultOrgDbDir } = require('./store');
15
+ const { scanRepo } = require('./detect');
16
+
17
+ /**
18
+ * orgSync({ orgDir, scopes, dry }) → { repos, edges_inserted }
19
+ *
20
+ * `scopes`: { npm: [...], pypi: [...], go: [...], maven: [...] }
21
+ * `dry`: if true, don't persist; just return what would change.
22
+ */
23
+ function orgSync({ orgDir = defaultOrgDbDir(), scopes = {}, dry = false } = {}) {
24
+ const store = new OrgStore(orgDir).open();
25
+ try {
26
+ const repos = store.listRepos();
27
+ let inserted = 0;
28
+ const allEdges = new Map(); // repo.name → edges
29
+
30
+ for (const repo of repos) {
31
+ const edges = scanRepo({ repoRoot: repo.root_path, scopes });
32
+ allEdges.set(repo.name, edges);
33
+ }
34
+
35
+ // Build target → producer-repo map from each repo's package metadata.
36
+ const targetToRepo = buildTargetToRepoMap(repos);
37
+
38
+ // Resolve to_repo + persist.
39
+ for (const [repoName, edges] of allEdges) {
40
+ for (const e of edges) {
41
+ if (!e.to_repo) {
42
+ const target = e.target;
43
+ const producer = targetToRepo.get(`${e.edge_kind}::${target}`);
44
+ if (producer && producer !== repoName) e.to_repo = producer;
45
+ }
46
+ }
47
+ if (!dry) store.insertEdges(repoName, edges);
48
+ inserted += edges.length;
49
+ }
50
+
51
+ return {
52
+ repos: repos.length,
53
+ edges_inserted: inserted,
54
+ edges_by_repo: Object.fromEntries(
55
+ Array.from(allEdges.entries()).map(([name, e]) => [name, e.length])
56
+ ),
57
+ };
58
+ } finally {
59
+ store.close();
60
+ }
61
+ }
62
+
63
+ function buildTargetToRepoMap(repos) {
64
+ const map = new Map();
65
+ for (const repo of repos) {
66
+ // npm: package.json `name`
67
+ const pkgPath = path.join(repo.root_path, 'package.json');
68
+ if (fs.existsSync(pkgPath)) {
69
+ try {
70
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
71
+ if (pkg && pkg.name) map.set(`npm::${pkg.name}`, repo.name);
72
+ } catch {}
73
+ }
74
+ // pypi: pyproject.toml or setup.py
75
+ const pyproject = path.join(repo.root_path, 'pyproject.toml');
76
+ if (fs.existsSync(pyproject)) {
77
+ try {
78
+ const text = fs.readFileSync(pyproject, 'utf-8');
79
+ const m = /\bname\s*=\s*["']([^"']+)["']/.exec(text);
80
+ if (m) map.set(`pypi::${m[1]}`, repo.name);
81
+ } catch {}
82
+ }
83
+ // go: go.mod `module`
84
+ const gomod = path.join(repo.root_path, 'go.mod');
85
+ if (fs.existsSync(gomod)) {
86
+ try {
87
+ const text = fs.readFileSync(gomod, 'utf-8');
88
+ const m = /^module\s+(\S+)/m.exec(text);
89
+ if (m) map.set(`go-mod::${m[1]}`, repo.name);
90
+ } catch {}
91
+ }
92
+ // maven: pom.xml <groupId>:<artifactId>
93
+ const pom = path.join(repo.root_path, 'pom.xml');
94
+ if (fs.existsSync(pom)) {
95
+ try {
96
+ const text = fs.readFileSync(pom, 'utf-8');
97
+ const g = /<groupId>([^<]+)<\/groupId>/.exec(text);
98
+ const a = /<artifactId>([^<]+)<\/artifactId>/.exec(text);
99
+ if (g && a) map.set(`maven::${g[1]}:${a[1]}`, repo.name);
100
+ } catch {}
101
+ }
102
+ }
103
+ return map;
104
+ }
105
+
106
+ module.exports = { orgSync, buildTargetToRepoMap };