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.
- package/CONTRIBUTING.md +19 -15
- package/README.md +192 -415
- package/docs/api/README.md +99 -0
- package/docs/api/did_we_discuss_this.md +34 -0
- package/docs/api/dismiss_suggestion.md +34 -0
- package/docs/api/explain_change_in_natural_language.md +33 -0
- package/docs/api/find_consumers_of_api.md +34 -0
- package/docs/api/get_action_patterns.md +32 -0
- package/docs/api/get_active_drift.md +32 -0
- package/docs/api/get_active_suggestions.md +25 -0
- package/docs/api/get_ai_cost_attribution.md +32 -0
- package/docs/api/get_arch_events.md +42 -0
- package/docs/api/get_architectural_drift.md +37 -0
- package/docs/api/get_architecture.md +25 -0
- package/docs/api/get_blast_radius.md +34 -0
- package/docs/api/get_canonical_pattern.md +39 -0
- package/docs/api/get_change_plan.md +34 -0
- package/docs/api/get_change_velocity.md +32 -0
- package/docs/api/get_churn_vs_blast_radius.md +32 -0
- package/docs/api/get_complexity_trend.md +39 -0
- package/docs/api/get_context.md +34 -0
- package/docs/api/get_conventions.md +32 -0
- package/docs/api/get_cross_domain.md +25 -0
- package/docs/api/get_cross_language_call_graph.md +25 -0
- package/docs/api/get_cross_repo_blast_radius.md +34 -0
- package/docs/api/get_cross_team_coupling.md +25 -0
- package/docs/api/get_data_flow.md +33 -0
- package/docs/api/get_dead_code_with_confidence.md +32 -0
- package/docs/api/get_decision_log.md +32 -0
- package/docs/api/get_dependency_surface.md +25 -0
- package/docs/api/get_domain.md +34 -0
- package/docs/api/get_domain_evolution.md +39 -0
- package/docs/api/get_domain_health.md +32 -0
- package/docs/api/get_domains_list.md +25 -0
- package/docs/api/get_drift_digest.md +32 -0
- package/docs/api/get_env_vars.md +32 -0
- package/docs/api/get_evolution_delta.md +36 -0
- package/docs/api/get_file_ownership.md +33 -0
- package/docs/api/get_file_summary.md +34 -0
- package/docs/api/get_high_impact_files.md +32 -0
- package/docs/api/get_hot_in_prod_no_tests.md +34 -0
- package/docs/api/get_hotspot_files.md +37 -0
- package/docs/api/get_iac_resources.md +25 -0
- package/docs/api/get_interface_contract.md +33 -0
- package/docs/api/get_intervention_history.md +32 -0
- package/docs/api/get_invariants.md +37 -0
- package/docs/api/get_llm_enrichment.md +33 -0
- package/docs/api/get_microservice_cut_points.md +32 -0
- package/docs/api/get_microservices_migration_cut_points.md +25 -0
- package/docs/api/get_minimal_context_for_intent.md +39 -0
- package/docs/api/get_models.md +32 -0
- package/docs/api/get_neighbors.md +39 -0
- package/docs/api/get_org_architecture.md +25 -0
- package/docs/api/get_org_domain_mapping.md +25 -0
- package/docs/api/get_pending_decisions.md +32 -0
- package/docs/api/get_predictive_risk.md +32 -0
- package/docs/api/get_progressive_disclosure_tree.md +25 -0
- package/docs/api/get_recent_decisions.md +37 -0
- package/docs/api/get_risk_weighted_blast_radius.md +32 -0
- package/docs/api/get_routes.md +25 -0
- package/docs/api/get_safety_checklist.md +33 -0
- package/docs/api/get_semantic_diff.md +34 -0
- package/docs/api/get_service_boundary_violations.md +25 -0
- package/docs/api/get_service_dependency_graph.md +25 -0
- package/docs/api/get_session_context.md +32 -0
- package/docs/api/get_similar_patterns.md +39 -0
- package/docs/api/get_stale_docs.md +25 -0
- package/docs/api/get_structure.md +25 -0
- package/docs/api/get_temporal_context.md +34 -0
- package/docs/api/get_test_coverage_map.md +25 -0
- package/docs/api/get_token_budget_report.md +36 -0
- package/docs/api/get_upgrade_risk.md +25 -0
- package/docs/api/get_working_memory.md +25 -0
- package/docs/api/ingest_otlp_traces.md +34 -0
- package/docs/api/scaffold_for_intent.md +34 -0
- package/docs/api/search_routes.md +34 -0
- package/docs/api/simulate_change_impact.md +37 -0
- package/docs/api/validate_change.md +39 -0
- package/docs/api/validate_diff.md +39 -0
- package/docs/concepts/anci.md +87 -0
- package/docs/concepts/blast-radius.md +66 -0
- package/docs/concepts/domains.md +91 -0
- package/docs/concepts/import-graph.md +102 -0
- package/docs/concepts/mcp-integration.md +148 -0
- package/docs/guides/adding-feature-safely.md +101 -0
- package/docs/guides/ci-integration.md +175 -0
- package/docs/guides/monorepo-setup.md +121 -0
- package/docs/guides/onboarding-new-engineer.md +121 -0
- package/docs/guides/pre-merge-review.md +139 -0
- package/docs/migration/v1-to-v2.md +110 -0
- package/docs/quickstart.md +95 -0
- package/docs/scale.md +2 -2
- package/docs/troubleshooting.md +180 -0
- package/package.json +10 -4
- package/scripts/gen-api-docs.js +170 -0
- package/src/acp/agent.js +83 -11
- package/src/acp/config.js +64 -0
- package/src/acp/persistence.js +146 -0
- package/src/acp/providers/anthropic.js +179 -27
- package/src/acp/providers/index.js +15 -2
- package/src/acp/providers/openai.js +164 -38
- package/src/acp/providers/sse.js +82 -0
- package/src/acp/safety.js +128 -0
- package/src/acp/session.js +73 -0
- package/src/adjacent/call-graph.js +170 -0
- package/src/adjacent/iac.js +167 -0
- package/src/adjacent/llm-enrich.js +35 -0
- package/src/adjacent/runtime.js +216 -0
- package/src/adjacent/semantic-diff.js +143 -0
- package/src/agents/scan-structure.js +1 -1
- package/src/ai/context-builder.js +215 -0
- package/src/ai/retrieval/lexical.js +122 -0
- package/src/ai/retrieval/rrf.js +121 -0
- package/src/ai/retrieval/semantic.js +35 -0
- package/src/ai/retrieval/structural.js +82 -0
- package/src/ai/tools.js +423 -0
- package/src/anci/emit.js +2 -2
- package/src/bitmap/index.js +1 -1
- package/src/bitmap/tools.js +2 -2
- package/src/brain/conventions/index.js +185 -0
- package/src/brain/index.js +31 -0
- package/src/brain/invariants/index.js +252 -0
- package/src/brain/procedural/index.js +181 -0
- package/src/brain/suggestions/index.js +153 -0
- package/src/brain/working/index.js +170 -0
- package/src/cli/check.js +47 -1
- package/src/cli/diff.js +83 -0
- package/src/cli/doctor.js +270 -0
- package/src/cli/explain.js +61 -0
- package/src/cli/index.js +101 -0
- package/src/cli/init.js +143 -5
- package/src/cli/org.js +172 -0
- package/src/cli/pr-impact.js +59 -2
- package/src/cli/serve.js +1 -1
- package/src/cli/status.js +211 -0
- package/src/cli/sync.js +2 -2
- package/src/cli/temporal.js +188 -0
- package/src/cli/validate.js +201 -0
- package/src/cli/watch.js +4 -4
- package/src/cli/why.js +101 -0
- package/src/extractors/frameworks.js +236 -0
- package/src/extractors/languages/dart.js +138 -0
- package/src/extractors/languages/go.js +11 -1
- package/src/extractors/languages/javascript.js +13 -3
- package/src/extractors/languages/kotlin.js +169 -0
- package/src/extractors/languages/php.js +195 -0
- package/src/extractors/languages/python.js +12 -1
- package/src/extractors/languages/swift.js +140 -0
- package/src/extractors/languages/typescript.js +15 -2
- package/src/extractors/plugin-api.js +102 -0
- package/src/mcp/files-without-tests.js +285 -0
- package/src/mcp/middleware/index.js +451 -0
- package/src/mcp/server.js +2292 -0
- package/src/mcp/validate.js +1 -1
- package/src/org/detect.js +262 -0
- package/src/org/queries.js +144 -0
- package/src/org/store.js +173 -0
- package/src/org/sync.js +106 -0
- package/src/predictive/cut-points.js +83 -0
- package/src/predictive/drift-digest.js +88 -0
- package/src/predictive/ownership.js +145 -0
- package/src/predictive/risk-score.js +121 -0
- package/src/predictive/validate-change.js +55 -0
- package/src/store/store-adapter.js +3 -3
- package/src/store/{sync-v2.js → sync.js} +53 -13
- package/src/temporal/backfill.js +211 -0
- package/src/temporal/delta.js +85 -0
- package/src/temporal/events.js +180 -0
- package/src/temporal/queries.js +358 -0
- package/src/temporal/snapshot.js +151 -0
- package/src/temporal/store.js +400 -0
- package/src/mcp/server-v2.js +0 -986
|
@@ -0,0 +1,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
|
+
};
|