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,83 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Microservice cut-point finder.
5
+ *
6
+ * Identifies natural microservice boundaries by finding clusters with
7
+ * high internal cohesion and low external coupling.
8
+ *
9
+ * Method:
10
+ * 1. For each domain D in the store, count
11
+ * - intra_edges imports where both ends are in D
12
+ * - external_edges imports from D to ¬D, plus imports from ¬D to D
13
+ * 2. cohesion = intra_edges / (intra_edges + external_edges)
14
+ * 3. A domain with cohesion >= 0.7 and size >= 10 is a candidate cut-point.
15
+ *
16
+ * Output: per-domain { domain, files, intra_edges, external_edges,
17
+ * cohesion_score, candidate: boolean }.
18
+ */
19
+
20
+ function findCutPoints({ store, threshold = 0.7, minSize = 10 } = {}) {
21
+ if (!store || !store.db) return { cut_points: [], all_domains: [] };
22
+
23
+ const allDomains = store.getDomainsList();
24
+ const intraEdges = store.db.prepare(`
25
+ SELECT d.name as domain, COUNT(*) as c
26
+ FROM imports i
27
+ JOIN files f1 ON i.from_file_id = f1.id
28
+ JOIN files f2 ON i.to_file_id = f2.id
29
+ JOIN domains d ON f1.domain_id = d.id
30
+ WHERE f1.domain_id = f2.domain_id AND i.to_file_id IS NOT NULL
31
+ GROUP BY d.name
32
+ `).all();
33
+ const intraMap = new Map(intraEdges.map(r => [r.domain, r.c]));
34
+
35
+ const externalEdges = store.db.prepare(`
36
+ SELECT d.name as domain, COUNT(*) as c
37
+ FROM imports i
38
+ JOIN files f1 ON i.from_file_id = f1.id
39
+ JOIN files f2 ON i.to_file_id = f2.id
40
+ JOIN domains d ON f1.domain_id = d.id
41
+ WHERE f1.domain_id != f2.domain_id AND i.to_file_id IS NOT NULL
42
+ GROUP BY d.name
43
+ `).all();
44
+ const externalMap = new Map(externalEdges.map(r => [r.domain, r.c]));
45
+
46
+ // Inbound external edges: imports INTO domain D from other domains.
47
+ const inboundExt = store.db.prepare(`
48
+ SELECT d.name as domain, COUNT(*) as c
49
+ FROM imports i
50
+ JOIN files f1 ON i.from_file_id = f1.id
51
+ JOIN files f2 ON i.to_file_id = f2.id
52
+ JOIN domains d ON f2.domain_id = d.id
53
+ WHERE f1.domain_id != f2.domain_id AND i.to_file_id IS NOT NULL
54
+ GROUP BY d.name
55
+ `).all();
56
+ const inboundMap = new Map(inboundExt.map(r => [r.domain, r.c]));
57
+
58
+ const out = [];
59
+ for (const d of allDomains) {
60
+ const intra = intraMap.get(d.name) || 0;
61
+ const outbound = externalMap.get(d.name) || 0;
62
+ const inbound = inboundMap.get(d.name) || 0;
63
+ const external = outbound + inbound;
64
+ const total = intra + external;
65
+ const cohesion = total === 0 ? 0 : intra / total;
66
+ out.push({
67
+ domain: d.name,
68
+ files: d.fileCount,
69
+ intra_edges: intra,
70
+ outbound_edges: outbound,
71
+ inbound_edges: inbound,
72
+ cohesion: Math.round(cohesion * 1000) / 1000,
73
+ candidate: cohesion >= threshold && d.fileCount >= minSize,
74
+ });
75
+ }
76
+ out.sort((a, b) => b.cohesion - a.cohesion);
77
+ return {
78
+ cut_points: out.filter(x => x.candidate),
79
+ all_domains: out,
80
+ };
81
+ }
82
+
83
+ module.exports = { findCutPoints };
@@ -0,0 +1,88 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Weekly drift digest renderer.
5
+ *
6
+ * Produces a CLI-renderable weekly architectural digest as markdown.
7
+ * Slack delivery is left to whatever cron + webhook the user wires up;
8
+ * this module only renders the body.
9
+ *
10
+ * Content:
11
+ * 1. Domain growth/shrink over `time_range` (default 7d)
12
+ * 2. Top hotspots (commit_count × blast_radius)
13
+ * 3. New architectural events
14
+ * 4. New cross-domain edges introduced
15
+ * 5. Top files by predictive risk score
16
+ */
17
+
18
+ function renderDriftDigest({ store, temporalStore, projectRoot, timeRange = '7d' }) {
19
+ if (!store) return '# Drift Digest\n\n_No store available._';
20
+
21
+ const lines = [`# Drift Digest (${timeRange})`];
22
+ lines.push(`\nGenerated ${new Date().toISOString()}\n`);
23
+
24
+ // ── Domain drift ────────────────────────────────────────────────
25
+ if (temporalStore) {
26
+ try {
27
+ const q = require('../temporal/queries');
28
+ const drift = q.getArchitecturalDrift(temporalStore, { timeRange });
29
+ lines.push('## Domain drift');
30
+ lines.push(`Trend: **${drift.trend}**.`);
31
+ if (drift.byDomain && drift.byDomain.length > 0) {
32
+ lines.push('| Domain | Before | After | Δ |');
33
+ lines.push('|--------|-------:|------:|---|');
34
+ for (const d of drift.byDomain.slice(0, 10)) {
35
+ lines.push(`| ${d.domain} | ${d.before} | ${d.after} | ${d.delta > 0 ? '+' + d.delta : d.delta} |`);
36
+ }
37
+ }
38
+ } catch {}
39
+
40
+ // ── Hotspots ────────────────────────────────────────────────
41
+ try {
42
+ const q = require('../temporal/queries');
43
+ const h = q.getHotspotFiles(temporalStore, { timeRange, limit: 10 });
44
+ if (h.hotspots && h.hotspots.length > 0) {
45
+ lines.push('\n## Top hotspots');
46
+ lines.push('| File | Commits | Blast | Score |');
47
+ lines.push('|------|--------:|------:|------:|');
48
+ for (const x of h.hotspots) {
49
+ lines.push(`| ${x.file_path} | ${x.commit_count} | ${x.blast_radius} | ${x.score} |`);
50
+ }
51
+ }
52
+ } catch {}
53
+
54
+ // ── Events ──────────────────────────────────────────────────
55
+ try {
56
+ const q = require('../temporal/queries');
57
+ const e = q.getArchEvents(temporalStore, { timeRange, limit: 20 });
58
+ if (e.events && e.events.length > 0) {
59
+ lines.push('\n## Architectural events');
60
+ lines.push('| When | Severity | Kind | Target |');
61
+ lines.push('|------|----------|------|--------|');
62
+ for (const ev of e.events) {
63
+ const when = new Date(ev.ts).toISOString();
64
+ const target = ev.domain || ev.file_path || '';
65
+ lines.push(`| ${when} | ${ev.severity} | ${ev.kind} | ${target} |`);
66
+ }
67
+ }
68
+ } catch {}
69
+ } else {
70
+ lines.push('## Temporal data unavailable\n\nRun `carto temporal init` to enable drift, hotspot, and events sections.');
71
+ }
72
+
73
+ // ── Predictive risk top 10 ────────────────────────────────
74
+ try {
75
+ const { scoreFiles } = require('./risk-score');
76
+ const ranked = scoreFiles({ store, temporalStore, projectRoot }).slice(0, 10);
77
+ if (ranked.length > 0) {
78
+ lines.push('\n## Predicted-risk top 10');
79
+ lines.push('| File | Score |');
80
+ lines.push('|------|------:|');
81
+ for (const f of ranked) lines.push(`| ${f.path} | ${f.score} |`);
82
+ }
83
+ } catch {}
84
+
85
+ return lines.join('\n');
86
+ }
87
+
88
+ module.exports = { renderDriftDigest };
@@ -0,0 +1,145 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Team intelligence — implicit ownership detection + cross-team coupling.
5
+ *
6
+ * Detects "who touches this file most" from `git blame` and `git log`
7
+ * stats. Returns per-file and per-domain owners. From there we can
8
+ * surface cross-team coupling — Domain A is mostly touched by Alice but
9
+ * imports from Domain B, which Alice never touches → coordination cost.
10
+ *
11
+ * All git-shell-outs are wrapped: 10 s timeout, bounded output, errors
12
+ * fail soft to empty.
13
+ */
14
+
15
+ const { execFileSync } = require('child_process');
16
+
17
+ function gitLog(projectRoot, args, timeoutMs = 10_000) {
18
+ try {
19
+ return execFileSync('git', ['-C', projectRoot, ...args], {
20
+ encoding: 'utf-8',
21
+ stdio: ['ignore', 'pipe', 'ignore'],
22
+ timeout: timeoutMs,
23
+ maxBuffer: 16 * 1024 * 1024,
24
+ });
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * ownersForFile({ projectRoot, file }) → { file, top_author, authors: [{ name, lines }] }
32
+ *
33
+ * Uses `git blame --line-porcelain` to count lines per author. Fails soft
34
+ * to { file, authors: [] } when git is unavailable / file not tracked.
35
+ */
36
+ function ownersForFile({ projectRoot, file }) {
37
+ const out = gitLog(projectRoot, ['blame', '--line-porcelain', '--', file]);
38
+ if (!out) return { file, authors: [], top_author: null };
39
+ const counts = new Map();
40
+ for (const line of out.split('\n')) {
41
+ if (line.startsWith('author ')) {
42
+ const name = line.slice('author '.length);
43
+ counts.set(name, (counts.get(name) || 0) + 1);
44
+ }
45
+ }
46
+ const authors = Array.from(counts.entries())
47
+ .map(([name, lines]) => ({ name, lines }))
48
+ .sort((a, b) => b.lines - a.lines);
49
+ return { file, authors, top_author: authors[0] ? authors[0].name : null };
50
+ }
51
+
52
+ /**
53
+ * ownersForDomain({ store, projectRoot, domain }) — aggregates per-file
54
+ * blame counts across the files in `domain`. Returns top authors.
55
+ */
56
+ function ownersForDomain({ store, projectRoot, domain, maxFiles = 100 }) {
57
+ if (!store || !store.db) return { domain, authors: [], top_author: null };
58
+ const files = store.db.prepare(`
59
+ SELECT f.path FROM files f
60
+ JOIN domains d ON f.domain_id = d.id
61
+ WHERE d.name = ?
62
+ ORDER BY f.centrality DESC LIMIT ?
63
+ `).all(domain, maxFiles).map(r => r.path);
64
+ const counts = new Map();
65
+ for (const f of files) {
66
+ const r = ownersForFile({ projectRoot, file: f });
67
+ for (const a of r.authors) {
68
+ counts.set(a.name, (counts.get(a.name) || 0) + a.lines);
69
+ }
70
+ }
71
+ const authors = Array.from(counts.entries())
72
+ .map(([name, lines]) => ({ name, lines }))
73
+ .sort((a, b) => b.lines - a.lines);
74
+ return { domain, authors, top_author: authors[0] ? authors[0].name : null };
75
+ }
76
+
77
+ /**
78
+ * crossTeamCoupling({ store, projectRoot }) → warnings
79
+ *
80
+ * For each cross-domain edge in the graph, look up the owner of the
81
+ * source file and the owner of the target file. If they differ AND the
82
+ * source-side owner has never touched the target file, surface a
83
+ * coordination warning.
84
+ */
85
+ function crossTeamCoupling({ store, projectRoot, maxEdges = 50 }) {
86
+ if (!store) return { warnings: [] };
87
+ const edges = (store.getCrossDomainDeps() || []).slice(0, maxEdges);
88
+ const ownerCache = new Map();
89
+ const warnings = [];
90
+
91
+ for (const e of edges) {
92
+ if (!ownerCache.has(e.from)) ownerCache.set(e.from, ownersForFile({ projectRoot, file: e.from }).top_author);
93
+ if (!ownerCache.has(e.to)) ownerCache.set(e.to, ownersForFile({ projectRoot, file: e.to }).top_author);
94
+ const fromOwner = ownerCache.get(e.from);
95
+ const toOwner = ownerCache.get(e.to);
96
+ if (fromOwner && toOwner && fromOwner !== toOwner) {
97
+ warnings.push({
98
+ from_file: e.from,
99
+ to_file: e.to,
100
+ from_domain: e.fromDomain || null,
101
+ to_domain: e.toDomain || null,
102
+ from_owner: fromOwner,
103
+ to_owner: toOwner,
104
+ });
105
+ }
106
+ }
107
+ return { warnings };
108
+ }
109
+
110
+ /**
111
+ * aiCostAttribution({ store, hours }) → per-client metrics
112
+ *
113
+ * Cheap proxy for "which AI client is creating the most cross-domain
114
+ * coupling?". Reads `ai_sessions.client_name` + joins to `decisions` and
115
+ * counts violations per client.
116
+ */
117
+ function aiCostAttribution({ store, hours = 168 } = {}) {
118
+ if (!store || !store.db) return { clients: [] };
119
+ const since = Date.now() - hours * 60 * 60 * 1000;
120
+ const rows = store.db.prepare(`
121
+ SELECT s.client_name as client, COUNT(d.id) as decisions
122
+ FROM ai_sessions s
123
+ LEFT JOIN decisions d ON d.session_id = s.id
124
+ WHERE s.started_at >= ? OR d.ts >= ?
125
+ GROUP BY s.client_name
126
+ `).all(since, since);
127
+ const violations = store.db.prepare(`
128
+ SELECT s.client_name as client, COUNT(i.id) as violations
129
+ FROM ai_sessions s
130
+ LEFT JOIN interventions i ON i.session_id = s.id
131
+ WHERE s.started_at >= ? OR i.ts >= ?
132
+ GROUP BY s.client_name
133
+ `).all(since, since);
134
+ const vMap = new Map(violations.map(r => [r.client, r.violations]));
135
+ return {
136
+ hours,
137
+ clients: rows.map(r => ({
138
+ client: r.client || '(unknown)',
139
+ decisions: r.decisions || 0,
140
+ violations: vMap.get(r.client) || 0,
141
+ })).sort((a, b) => b.decisions - a.decisions),
142
+ };
143
+ }
144
+
145
+ module.exports = { ownersForFile, ownersForDomain, crossTeamCoupling, aiCostAttribution };
@@ -0,0 +1,121 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Predictive risk scoring.
5
+ *
6
+ * Combines temporal + structural + intervention history into a single
7
+ * 0–1 score per file: P(this file causes the next incident).
8
+ *
9
+ * Inputs:
10
+ * - blast_radius how many transitive dependents (centrality column)
11
+ * - commit_count temporal churn
12
+ * - cross_domain is the file in cross-domain edges
13
+ * - intervention_count past HIGH-severity interventions
14
+ * - test_present has a detected test file
15
+ *
16
+ * Formula (additive, normalized to 0–1):
17
+ * risk = 0.30 * normalized_blast
18
+ * + 0.25 * normalized_churn
19
+ * + 0.20 * cross_domain ? 1 : 0
20
+ * + 0.15 * normalized_interventions
21
+ * + 0.10 * test_present ? 0 : 1
22
+ *
23
+ * Each term capped at 1 before weighting. Final score is the weighted sum;
24
+ * naturally in [0, 1] because weights sum to 1.
25
+ */
26
+
27
+ const path = require('path');
28
+
29
+ const WEIGHTS = {
30
+ blast: 0.30,
31
+ churn: 0.25,
32
+ cross: 0.20,
33
+ intervention: 0.15,
34
+ noTest: 0.10,
35
+ };
36
+
37
+ function clampUnit(x) {
38
+ if (!Number.isFinite(x)) return 0;
39
+ return Math.max(0, Math.min(1, x));
40
+ }
41
+
42
+ /**
43
+ * scoreFiles({ store, temporalStore, projectRoot, files? }) → ranked Array
44
+ *
45
+ * If `files` is provided, scores only those; otherwise scores all files.
46
+ * Returns [{ path, score, components }] sorted by score DESC.
47
+ */
48
+ function scoreFiles({ store, temporalStore = null, projectRoot, files = null }) {
49
+ if (!store || !store.db) return [];
50
+
51
+ // Normalization bases.
52
+ const maxBlast = (store.db.prepare('SELECT MAX(centrality) as m FROM files').get() || {}).m || 1;
53
+ const maxChurn = temporalStore && temporalStore.db
54
+ ? (temporalStore.db.prepare('SELECT MAX(commit_count) as m FROM file_churn').get() || {}).m || 1
55
+ : 1;
56
+
57
+ // Cross-domain edges set.
58
+ const crossSet = new Set();
59
+ try {
60
+ const rows = store.getCrossDomainDeps() || [];
61
+ for (const r of rows) crossSet.add(r.from);
62
+ } catch {}
63
+
64
+ // Intervention counts per file.
65
+ const ivCounts = new Map();
66
+ try {
67
+ const rows = store.db.prepare(`
68
+ SELECT file, COUNT(*) as c FROM interventions
69
+ WHERE file IS NOT NULL AND severity IN ('HIGH', 'critical')
70
+ GROUP BY file
71
+ `).all();
72
+ for (const r of rows) ivCounts.set(r.file, r.c);
73
+ } catch {}
74
+ const maxIv = Math.max(1, ...ivCounts.values());
75
+
76
+ // Tests-present set, via the files-without-tests detector.
77
+ const { filesWithoutTests } = require('../mcp/files-without-tests');
78
+ let withoutTests = new Set();
79
+ try {
80
+ const allFiles = files || store.db.prepare('SELECT path FROM files LIMIT 2000').all().map(r => r.path);
81
+ const r = filesWithoutTests(projectRoot, allFiles);
82
+ withoutTests = new Set(r.files || []);
83
+ } catch {}
84
+
85
+ // Score each file.
86
+ const rows = files
87
+ ? store.db.prepare(
88
+ `SELECT path, centrality FROM files WHERE path IN (${files.map(() => '?').join(',')})`
89
+ ).all(...files)
90
+ : store.db.prepare('SELECT path, centrality FROM files').all();
91
+
92
+ const out = [];
93
+ for (const r of rows) {
94
+ const blast = clampUnit((r.centrality || 0) / Math.max(1, maxBlast));
95
+ let churn = 0;
96
+ if (temporalStore && temporalStore.db) {
97
+ const ch = temporalStore.getFileChurn(r.path);
98
+ if (ch) churn = clampUnit((ch.commit_count || 0) / Math.max(1, maxChurn));
99
+ }
100
+ const cross = crossSet.has(r.path) ? 1 : 0;
101
+ const iv = clampUnit((ivCounts.get(r.path) || 0) / Math.max(1, maxIv));
102
+ const noTest = withoutTests.has(r.path) ? 1 : 0;
103
+
104
+ const score =
105
+ WEIGHTS.blast * blast +
106
+ WEIGHTS.churn * churn +
107
+ WEIGHTS.cross * cross +
108
+ WEIGHTS.intervention * iv +
109
+ WEIGHTS.noTest * noTest;
110
+
111
+ out.push({
112
+ path: r.path,
113
+ score: Math.round(score * 1000) / 1000,
114
+ components: { blast, churn, cross, intervention: iv, no_test: noTest },
115
+ });
116
+ }
117
+ out.sort((a, b) => b.score - a.score);
118
+ return out;
119
+ }
120
+
121
+ module.exports = { scoreFiles, WEIGHTS };
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * validate_change — pre-write governance.
5
+ *
6
+ * `validateDiff` validates a unified diff after the edit. `validateChange`
7
+ * is the pre-write counterpart: given a file path plus the full proposed
8
+ * content, synthesize a minimal diff against current on-disk content and
9
+ * run the same validation pipeline. This is the API an IDE extension
10
+ * calls in `onWillSaveTextDocument`.
11
+ *
12
+ * Output: same shape as `validateDiff`.
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const { validateDiff } = require('../mcp/validate');
18
+
19
+ function validateChange({ store, projectRoot, file, content }) {
20
+ if (!store || !file || typeof content !== 'string') {
21
+ return { risk: 'SAFE', files_changed: [], violations: [], suggestions: [], reason: 'invalid_args' };
22
+ }
23
+
24
+ const full = path.resolve(projectRoot, file);
25
+ let prior = '';
26
+ try { prior = fs.readFileSync(full, 'utf-8'); } catch {}
27
+ // If file already at this content: SAFE no-op.
28
+ if (prior === content) {
29
+ return { risk: 'SAFE', files_changed: [], violations: [], suggestions: [], reason: 'no_change' };
30
+ }
31
+
32
+ // Synthesize a unified diff. The diff parser only needs added imports
33
+ // + line counts to compute risk; we don't need a fully-correct hunk header.
34
+ const diff = synthesizeDiff(file, prior, content);
35
+ return validateDiff({ store, diff });
36
+ }
37
+
38
+ /**
39
+ * synthesizeDiff(relPath, before, after) — produces a unified diff that
40
+ * `parseDiff` accepts. Hunk headers approximate the line counts.
41
+ */
42
+ function synthesizeDiff(relPath, before, after) {
43
+ const beforeLines = (before || '').split('\n');
44
+ const afterLines = (after || '').split('\n');
45
+ const lines = [];
46
+ lines.push(`diff --git a/${relPath} b/${relPath}`);
47
+ lines.push(`--- a/${relPath}`);
48
+ lines.push(`+++ b/${relPath}`);
49
+ lines.push(`@@ -1,${beforeLines.length} +1,${afterLines.length} @@`);
50
+ for (const l of beforeLines) lines.push(`-${l}`);
51
+ for (const l of afterLines) lines.push(`+${l}`);
52
+ return lines.join('\n');
53
+ }
54
+
55
+ module.exports = { validateChange, synthesizeDiff };
@@ -3,7 +3,7 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { SQLiteStore } = require('./sqlite-store');
6
- const { runSyncV2 } = require('./sync-v2');
6
+ const { runSync } = require('./sync');
7
7
 
8
8
  /**
9
9
  * StoreAdapter — V1-compatible API wrapping SQLiteStore.
@@ -18,7 +18,7 @@ class StoreAdapter {
18
18
 
19
19
  /**
20
20
  * index(projectRoot, opts?)
21
- * Opens the SQLite DB; runs runSyncV2 if DB is missing.
21
+ * Opens the SQLite DB; runs runSync if DB is missing.
22
22
  * opts.force — re-extract all files
23
23
  * opts.writeOutputs — if false, skip AGENTS.md + context file generation (default: false)
24
24
  */
@@ -28,7 +28,7 @@ class StoreAdapter {
28
28
  const dbExists = fs.existsSync(dbPath);
29
29
 
30
30
  if (!dbExists || opts.force) {
31
- await runSyncV2({
31
+ await runSync({
32
32
  projectRoot,
33
33
  output: opts.writeOutputs ? path.resolve(projectRoot, 'AGENTS.md') : null,
34
34
  });