carto-md 2.0.9 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/CONTRIBUTING.md +19 -15
  2. package/README.md +192 -415
  3. package/docs/api/README.md +99 -0
  4. package/docs/api/did_we_discuss_this.md +34 -0
  5. package/docs/api/dismiss_suggestion.md +34 -0
  6. package/docs/api/explain_change_in_natural_language.md +33 -0
  7. package/docs/api/find_consumers_of_api.md +34 -0
  8. package/docs/api/get_action_patterns.md +32 -0
  9. package/docs/api/get_active_drift.md +32 -0
  10. package/docs/api/get_active_suggestions.md +25 -0
  11. package/docs/api/get_ai_cost_attribution.md +32 -0
  12. package/docs/api/get_arch_events.md +42 -0
  13. package/docs/api/get_architectural_drift.md +37 -0
  14. package/docs/api/get_architecture.md +25 -0
  15. package/docs/api/get_blast_radius.md +34 -0
  16. package/docs/api/get_canonical_pattern.md +39 -0
  17. package/docs/api/get_change_plan.md +34 -0
  18. package/docs/api/get_change_velocity.md +32 -0
  19. package/docs/api/get_churn_vs_blast_radius.md +32 -0
  20. package/docs/api/get_complexity_trend.md +39 -0
  21. package/docs/api/get_context.md +34 -0
  22. package/docs/api/get_conventions.md +32 -0
  23. package/docs/api/get_cross_domain.md +25 -0
  24. package/docs/api/get_cross_language_call_graph.md +25 -0
  25. package/docs/api/get_cross_repo_blast_radius.md +34 -0
  26. package/docs/api/get_cross_team_coupling.md +25 -0
  27. package/docs/api/get_data_flow.md +33 -0
  28. package/docs/api/get_dead_code_with_confidence.md +32 -0
  29. package/docs/api/get_decision_log.md +32 -0
  30. package/docs/api/get_dependency_surface.md +25 -0
  31. package/docs/api/get_domain.md +34 -0
  32. package/docs/api/get_domain_evolution.md +39 -0
  33. package/docs/api/get_domain_health.md +32 -0
  34. package/docs/api/get_domains_list.md +25 -0
  35. package/docs/api/get_drift_digest.md +32 -0
  36. package/docs/api/get_env_vars.md +32 -0
  37. package/docs/api/get_evolution_delta.md +36 -0
  38. package/docs/api/get_file_ownership.md +33 -0
  39. package/docs/api/get_file_summary.md +34 -0
  40. package/docs/api/get_high_impact_files.md +32 -0
  41. package/docs/api/get_hot_in_prod_no_tests.md +34 -0
  42. package/docs/api/get_hotspot_files.md +37 -0
  43. package/docs/api/get_iac_resources.md +25 -0
  44. package/docs/api/get_interface_contract.md +33 -0
  45. package/docs/api/get_intervention_history.md +32 -0
  46. package/docs/api/get_invariants.md +37 -0
  47. package/docs/api/get_llm_enrichment.md +33 -0
  48. package/docs/api/get_microservice_cut_points.md +32 -0
  49. package/docs/api/get_microservices_migration_cut_points.md +25 -0
  50. package/docs/api/get_minimal_context_for_intent.md +39 -0
  51. package/docs/api/get_models.md +32 -0
  52. package/docs/api/get_neighbors.md +39 -0
  53. package/docs/api/get_org_architecture.md +25 -0
  54. package/docs/api/get_org_domain_mapping.md +25 -0
  55. package/docs/api/get_pending_decisions.md +32 -0
  56. package/docs/api/get_predictive_risk.md +32 -0
  57. package/docs/api/get_progressive_disclosure_tree.md +25 -0
  58. package/docs/api/get_recent_decisions.md +37 -0
  59. package/docs/api/get_risk_weighted_blast_radius.md +32 -0
  60. package/docs/api/get_routes.md +25 -0
  61. package/docs/api/get_safety_checklist.md +33 -0
  62. package/docs/api/get_semantic_diff.md +34 -0
  63. package/docs/api/get_service_boundary_violations.md +25 -0
  64. package/docs/api/get_service_dependency_graph.md +25 -0
  65. package/docs/api/get_session_context.md +32 -0
  66. package/docs/api/get_similar_patterns.md +39 -0
  67. package/docs/api/get_stale_docs.md +25 -0
  68. package/docs/api/get_structure.md +25 -0
  69. package/docs/api/get_temporal_context.md +34 -0
  70. package/docs/api/get_test_coverage_map.md +25 -0
  71. package/docs/api/get_token_budget_report.md +36 -0
  72. package/docs/api/get_upgrade_risk.md +25 -0
  73. package/docs/api/get_working_memory.md +25 -0
  74. package/docs/api/ingest_otlp_traces.md +34 -0
  75. package/docs/api/scaffold_for_intent.md +34 -0
  76. package/docs/api/search_routes.md +34 -0
  77. package/docs/api/simulate_change_impact.md +37 -0
  78. package/docs/api/validate_change.md +39 -0
  79. package/docs/api/validate_diff.md +39 -0
  80. package/docs/concepts/anci.md +87 -0
  81. package/docs/concepts/blast-radius.md +66 -0
  82. package/docs/concepts/domains.md +91 -0
  83. package/docs/concepts/import-graph.md +102 -0
  84. package/docs/concepts/mcp-integration.md +148 -0
  85. package/docs/guides/adding-feature-safely.md +101 -0
  86. package/docs/guides/ci-integration.md +175 -0
  87. package/docs/guides/monorepo-setup.md +121 -0
  88. package/docs/guides/onboarding-new-engineer.md +121 -0
  89. package/docs/guides/pre-merge-review.md +139 -0
  90. package/docs/migration/v1-to-v2.md +110 -0
  91. package/docs/quickstart.md +95 -0
  92. package/docs/scale.md +2 -2
  93. package/docs/troubleshooting.md +180 -0
  94. package/package.json +10 -4
  95. package/scripts/gen-api-docs.js +170 -0
  96. package/src/acp/agent.js +83 -11
  97. package/src/acp/config.js +64 -0
  98. package/src/acp/persistence.js +146 -0
  99. package/src/acp/providers/anthropic.js +179 -27
  100. package/src/acp/providers/index.js +15 -2
  101. package/src/acp/providers/openai.js +164 -38
  102. package/src/acp/providers/sse.js +82 -0
  103. package/src/acp/safety.js +128 -0
  104. package/src/acp/session.js +73 -0
  105. package/src/adjacent/call-graph.js +170 -0
  106. package/src/adjacent/iac.js +167 -0
  107. package/src/adjacent/llm-enrich.js +35 -0
  108. package/src/adjacent/runtime.js +216 -0
  109. package/src/adjacent/semantic-diff.js +143 -0
  110. package/src/agents/scan-structure.js +1 -1
  111. package/src/ai/context-builder.js +215 -0
  112. package/src/ai/retrieval/lexical.js +122 -0
  113. package/src/ai/retrieval/rrf.js +121 -0
  114. package/src/ai/retrieval/semantic.js +35 -0
  115. package/src/ai/retrieval/structural.js +82 -0
  116. package/src/ai/tools.js +423 -0
  117. package/src/anci/emit.js +2 -2
  118. package/src/bitmap/index.js +1 -1
  119. package/src/bitmap/tools.js +2 -2
  120. package/src/brain/conventions/index.js +185 -0
  121. package/src/brain/index.js +31 -0
  122. package/src/brain/invariants/index.js +252 -0
  123. package/src/brain/procedural/index.js +181 -0
  124. package/src/brain/suggestions/index.js +153 -0
  125. package/src/brain/working/index.js +170 -0
  126. package/src/cli/check.js +47 -1
  127. package/src/cli/diff.js +83 -0
  128. package/src/cli/doctor.js +270 -0
  129. package/src/cli/explain.js +61 -0
  130. package/src/cli/index.js +101 -0
  131. package/src/cli/init.js +143 -5
  132. package/src/cli/org.js +172 -0
  133. package/src/cli/pr-impact.js +59 -2
  134. package/src/cli/serve.js +1 -1
  135. package/src/cli/status.js +211 -0
  136. package/src/cli/sync.js +2 -2
  137. package/src/cli/temporal.js +188 -0
  138. package/src/cli/validate.js +201 -0
  139. package/src/cli/watch.js +4 -4
  140. package/src/cli/why.js +101 -0
  141. package/src/extractors/frameworks.js +236 -0
  142. package/src/extractors/languages/dart.js +138 -0
  143. package/src/extractors/languages/go.js +11 -1
  144. package/src/extractors/languages/javascript.js +13 -3
  145. package/src/extractors/languages/kotlin.js +169 -0
  146. package/src/extractors/languages/php.js +195 -0
  147. package/src/extractors/languages/python.js +12 -1
  148. package/src/extractors/languages/swift.js +140 -0
  149. package/src/extractors/languages/typescript.js +15 -2
  150. package/src/extractors/plugin-api.js +102 -0
  151. package/src/mcp/files-without-tests.js +285 -0
  152. package/src/mcp/middleware/index.js +451 -0
  153. package/src/mcp/server.js +2292 -0
  154. package/src/mcp/validate.js +1 -1
  155. package/src/org/detect.js +262 -0
  156. package/src/org/queries.js +144 -0
  157. package/src/org/store.js +173 -0
  158. package/src/org/sync.js +106 -0
  159. package/src/predictive/cut-points.js +83 -0
  160. package/src/predictive/drift-digest.js +88 -0
  161. package/src/predictive/ownership.js +145 -0
  162. package/src/predictive/risk-score.js +121 -0
  163. package/src/predictive/validate-change.js +55 -0
  164. package/src/store/store-adapter.js +3 -3
  165. package/src/store/{sync-v2.js → sync.js} +53 -13
  166. package/src/temporal/backfill.js +211 -0
  167. package/src/temporal/delta.js +85 -0
  168. package/src/temporal/events.js +180 -0
  169. package/src/temporal/queries.js +358 -0
  170. package/src/temporal/snapshot.js +151 -0
  171. package/src/temporal/store.js +400 -0
  172. package/src/mcp/server-v2.js +0 -986
@@ -0,0 +1,285 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Files-Without-Tests detector.
5
+ *
6
+ * Given a list of file paths (typically the union blast radius of a
7
+ * PR's diff), scan each one's *immediate filesystem neighborhood* for a
8
+ * sibling test file. Files with no detectable test sibling are returned
9
+ * as the "without-tests" set — that's the metric the PR comment surfaces.
10
+ *
11
+ * Deliberately conservative:
12
+ * - Never goes up more than one directory level.
13
+ * - Patterns are limited to per-language conventions that are widely
14
+ * followed; anything more aggressive produces false negatives that
15
+ * undermine the metric.
16
+ * - Files that don't look like source code (config, docs, images) are
17
+ * excluded — they don't need tests in the first place.
18
+ *
19
+ * Trade-off: a few false positives (file X has tests in a far-away
20
+ * `tests/` directory we didn't walk) are acceptable. The PR comment shows
21
+ * the number, not a witch-hunt list — reviewers can verify by eyeballing
22
+ * the names.
23
+ *
24
+ * Detection rules per file extension:
25
+ *
26
+ * JS/TS (.js .jsx .ts .tsx .mjs .cjs):
27
+ * - <base>.test.<ext> or <base>.spec.<ext> in the same dir
28
+ * - __tests__/<base>.test.<ext> in the parent dir
29
+ *
30
+ * Python (.py):
31
+ * - test_<base>.py or <base>_test.py in the same dir
32
+ * - tests/test_<base>.py one level up
33
+ *
34
+ * Go (.go):
35
+ * - <base>_test.go in the same dir (the Go-blessed convention)
36
+ *
37
+ * Rust (.rs):
38
+ * - tests/<base>.rs one level up (the Cargo convention)
39
+ * - Inline `#[cfg(test)]` blocks are intentionally not detected — that
40
+ * would require reading file content; an acceptable gap for now.
41
+ *
42
+ * Java (.java), C# (.cs), Kotlin (.kt), Ruby (.rb), C/C++ (.c, .cpp,
43
+ * .cc, .cxx, .h, .hpp):
44
+ * - Conventions vary too much across projects (src/test/java vs
45
+ * adjacent vs separate test repo). Returns "has tests" as UNKNOWN
46
+ * so those files don't count toward the without-tests metric.
47
+ * Keeps false positives low.
48
+ *
49
+ * Excluded entirely (don't appear in the metric):
50
+ * - Test fixture files (already a test): *.test.* *.spec.* *_test.* test_*.*
51
+ * - Non-source files: .md .yml .yaml .json .toml .lock .png .svg .html
52
+ * - .d.ts type-only declarations
53
+ * - Index barrels (`index.ts`, `index.js`) — typically just re-exports
54
+ */
55
+
56
+ const fs = require('fs');
57
+ const path = require('path');
58
+
59
+ const NON_SOURCE_EXTS = new Set([
60
+ '.md', '.markdown', '.txt',
61
+ '.yml', '.yaml', '.json', '.toml', '.ini', '.env',
62
+ '.lock', '.lockb',
63
+ '.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico',
64
+ '.html', '.htm', '.css', '.scss', '.sass', '.less',
65
+ '.gitignore', '.gitattributes', '.editorconfig',
66
+ '.sql', '.csv', '.tsv',
67
+ '.sh', '.bash', '.zsh', '.fish',
68
+ '.prisma', '.graphql', '.proto',
69
+ ]);
70
+
71
+ // Filenames that are likely barrels/re-exports — counting them
72
+ // as "without tests" creates noise. Strip the dir-default barrel set.
73
+ const SKIP_BASENAMES = new Set(['index']);
74
+
75
+ /**
76
+ * isTestFile(file) — true when the file itself IS a test (not a candidate).
77
+ */
78
+ function isTestFile(file) {
79
+ const base = path.posix.basename(file);
80
+ if (/\.(test|spec|stories)\./.test(base)) return true;
81
+ if (/^test_.+\.py$/.test(base)) return true;
82
+ if (/_test\.(go|py|ts|tsx|js|jsx|rs)$/.test(base)) return true;
83
+ return false;
84
+ }
85
+
86
+ /**
87
+ * isNonSourceFile(file) — true when the file's extension means it doesn't
88
+ * need a test. (Markdown, JSON, images, etc.) Type-only TS declarations
89
+ * are also excluded.
90
+ */
91
+ function isNonSourceFile(file) {
92
+ const base = path.posix.basename(file);
93
+ if (base.endsWith('.d.ts')) return true;
94
+ const ext = path.posix.extname(base).toLowerCase();
95
+ if (NON_SOURCE_EXTS.has(ext)) return true;
96
+ // Files with no extension are usually scripts/configs — exclude.
97
+ if (!ext) return true;
98
+ return false;
99
+ }
100
+
101
+ /**
102
+ * isIgnoredBasename(file) — true when this file is a barrel index that
103
+ * we don't expect to have unit tests.
104
+ */
105
+ function isIgnoredBasename(file) {
106
+ const base = path.posix.basename(file);
107
+ const dot = base.indexOf('.');
108
+ const stem = dot >= 0 ? base.slice(0, dot) : base;
109
+ return SKIP_BASENAMES.has(stem);
110
+ }
111
+
112
+ /**
113
+ * stemOf(file) → string
114
+ *
115
+ * Returns the filename stem with all extensions stripped, e.g.
116
+ * "src/auth/login.test.ts" → "login"
117
+ * "src/auth/login.ts" → "login"
118
+ * "user_test.go" → "user"
119
+ * "test_user.py" → "user"
120
+ */
121
+ function stemOf(file) {
122
+ let base = path.posix.basename(file);
123
+ // Strip leading "test_" (Python convention).
124
+ if (base.startsWith('test_')) base = base.slice(5);
125
+ // Strip first dot-suffix (handles .test.ts → leaves bare name).
126
+ const firstDot = base.indexOf('.');
127
+ if (firstDot > 0) base = base.slice(0, firstDot);
128
+ // Strip trailing "_test" (Go / Python convention).
129
+ if (base.endsWith('_test')) base = base.slice(0, -5);
130
+ return base;
131
+ }
132
+
133
+ /**
134
+ * Cached directory listing — checking dozens of candidate paths per
135
+ * file would issue too many syscalls. One readdir per dir, reused.
136
+ */
137
+ function buildListingCache() {
138
+ const cache = new Map();
139
+ return {
140
+ list(absDir) {
141
+ if (cache.has(absDir)) return cache.get(absDir);
142
+ let entries = [];
143
+ try {
144
+ entries = fs.readdirSync(absDir);
145
+ } catch {
146
+ entries = [];
147
+ }
148
+ cache.set(absDir, entries);
149
+ return entries;
150
+ },
151
+ };
152
+ }
153
+
154
+ /**
155
+ * hasTestSibling(projectRoot, relPath, listing) → boolean | null
156
+ *
157
+ * Returns:
158
+ * - true if a test sibling was detected
159
+ * - false if the file is a testable source file and no sibling found
160
+ * - null if the file is not testable (test itself, non-source,
161
+ * convention not understood) — caller should skip in metric
162
+ */
163
+ function hasTestSibling(projectRoot, relPath, listing) {
164
+ if (isTestFile(relPath)) return null;
165
+ if (isNonSourceFile(relPath)) return null;
166
+ if (isIgnoredBasename(relPath)) return null;
167
+
168
+ const ext = path.posix.extname(relPath).toLowerCase();
169
+ const stem = stemOf(relPath);
170
+ const dir = path.posix.dirname(relPath);
171
+ const parent = dir === '.' ? '' : path.posix.dirname(dir);
172
+
173
+ const sameDir = listing.list(path.join(projectRoot, dir));
174
+ const parentDir = listing.list(parent ? path.join(projectRoot, parent) : projectRoot);
175
+
176
+ // Helper: is there a file matching `predicate` in `entries`?
177
+ const any = (entries, predicate) => {
178
+ for (const e of entries) {
179
+ if (predicate(e)) return true;
180
+ }
181
+ return false;
182
+ };
183
+
184
+ switch (ext) {
185
+ case '.js': case '.jsx': case '.mjs': case '.cjs':
186
+ case '.ts': case '.tsx': {
187
+ // sibling: <stem>.test.<ext> | <stem>.spec.<ext>
188
+ const re = new RegExp(`^${escapeRegex(stem)}\\.(test|spec)\\.(js|jsx|mjs|cjs|ts|tsx)$`);
189
+ if (any(sameDir, (e) => re.test(e))) return true;
190
+ // __tests__/<stem>.test.<ext> in same dir
191
+ try {
192
+ const testsDir = listing.list(path.join(projectRoot, dir, '__tests__'));
193
+ if (any(testsDir, (e) => re.test(e))) return true;
194
+ } catch { /* ignore */ }
195
+ return false;
196
+ }
197
+
198
+ case '.py': {
199
+ // test_<stem>.py | <stem>_test.py in same dir
200
+ if (any(sameDir, (e) => e === `test_${stem}.py` || e === `${stem}_test.py`)) return true;
201
+ // tests/test_<stem>.py one level up
202
+ try {
203
+ const testsDir = listing.list(parent ? path.join(projectRoot, parent, 'tests') : path.join(projectRoot, 'tests'));
204
+ if (any(testsDir, (e) => e === `test_${stem}.py`)) return true;
205
+ } catch { /* ignore */ }
206
+ // sibling tests/ in same dir
207
+ try {
208
+ const testsDir = listing.list(path.join(projectRoot, dir, 'tests'));
209
+ if (any(testsDir, (e) => e === `test_${stem}.py`)) return true;
210
+ } catch { /* ignore */ }
211
+ return false;
212
+ }
213
+
214
+ case '.go': {
215
+ // <stem>_test.go in same dir (Go convention)
216
+ return any(sameDir, (e) => e === `${stem}_test.go`);
217
+ }
218
+
219
+ case '.rs': {
220
+ // tests/<stem>.rs one level up (Cargo convention)
221
+ try {
222
+ const testsDir = listing.list(parent ? path.join(projectRoot, parent, 'tests') : path.join(projectRoot, 'tests'));
223
+ if (any(testsDir, (e) => e === `${stem}.rs`)) return true;
224
+ } catch { /* ignore */ }
225
+ // sibling tests/ in same dir
226
+ try {
227
+ const testsDir = listing.list(path.join(projectRoot, dir, 'tests'));
228
+ if (any(testsDir, (e) => e === `${stem}.rs`)) return true;
229
+ } catch { /* ignore */ }
230
+ // Inline #[cfg(test)] is intentionally not detected — would
231
+ // require reading file content. Returning null keeps Rust files
232
+ // out of the metric until we have a content-aware checker.
233
+ return null;
234
+ }
235
+
236
+ default:
237
+ // Convention not modeled (Java, C#, Kotlin, Ruby, C/C++).
238
+ return null;
239
+ }
240
+ }
241
+
242
+ function escapeRegex(s) {
243
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
244
+ }
245
+
246
+ /**
247
+ * filesWithoutTests(projectRoot, files) → { count, files }
248
+ *
249
+ * Public entry point. Pass the union blast radius — every file gets
250
+ * checked once. Returns:
251
+ *
252
+ * {
253
+ * count: number, // size of `files`
254
+ * files: string[], // relative paths with no detectable test
255
+ * considered: number, // total files actually checked (excludes
256
+ * // tests-themselves, non-source, ignored)
257
+ * }
258
+ */
259
+ function filesWithoutTests(projectRoot, files) {
260
+ const listing = buildListingCache();
261
+ const without = [];
262
+ let considered = 0;
263
+ // Dedupe while preserving first-seen order.
264
+ const seen = new Set();
265
+ for (const raw of files || []) {
266
+ const rel = typeof raw === 'string' ? raw : raw && raw.file;
267
+ if (!rel || seen.has(rel)) continue;
268
+ seen.add(rel);
269
+ const has = hasTestSibling(projectRoot, rel, listing);
270
+ if (has === null) continue;
271
+ considered++;
272
+ if (has === false) without.push(rel);
273
+ }
274
+ return { count: without.length, files: without, considered };
275
+ }
276
+
277
+ module.exports = {
278
+ filesWithoutTests,
279
+ // Exported for tests:
280
+ hasTestSibling,
281
+ isTestFile,
282
+ isNonSourceFile,
283
+ isIgnoredBasename,
284
+ stemOf,
285
+ };