preflight-mcp 0.5.0 → 0.5.2
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/README.md +22 -3
- package/dist/analysis/deep.js +159 -2
- package/dist/bundle/ingest.js +36 -7
- package/dist/server.js +105 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -310,20 +310,39 @@ Parameters:
|
|
|
310
310
|
- `min_confidence`: 0-1 (default: 0.85)
|
|
311
311
|
- `limit`: Max suggestions (default: 50)
|
|
312
312
|
|
|
313
|
-
### `preflight_deep_analyze_bundle` *(
|
|
314
|
-
One-call deep analysis aggregating tree, search, deps, and
|
|
313
|
+
### `preflight_deep_analyze_bundle` *(Enhanced v0.5.1)*
|
|
314
|
+
One-call deep analysis aggregating tree, search, deps, traces, **overview content**, and **test detection**.
|
|
315
315
|
- Returns unified evidence pack with LLM-friendly summary
|
|
316
|
+
- **Now includes OVERVIEW.md, START_HERE.md, AGENTS.md, README content** (v0.5.1)
|
|
317
|
+
- **Auto-detects test frameworks** (jest, vitest, pytest, go, mocha) (v0.5.1)
|
|
318
|
+
- **Generates copyable `nextCommands`** for follow-up actions (v0.5.1)
|
|
316
319
|
- Auto-generates **claims** with evidence references
|
|
317
320
|
- Tracks analysis progress via **checklistStatus**
|
|
318
321
|
- Reports unanswered questions as **openQuestions**
|
|
319
|
-
- Triggers: "deep analyze", "comprehensive analysis", "深度分析"
|
|
322
|
+
- Triggers: "deep analyze", "comprehensive analysis", "深度分析", "快速了解项目"
|
|
323
|
+
|
|
324
|
+
**New in v0.5.1 - Aggregated content (reduces round-trips):**
|
|
325
|
+
- `includeOverview` (default: true): Include OVERVIEW.md, START_HERE.md, AGENTS.md
|
|
326
|
+
- `includeReadme` (default: true): Include repo README.md
|
|
327
|
+
- `includeTests` (default: true): Detect test directories and frameworks
|
|
320
328
|
|
|
321
329
|
Output includes:
|
|
330
|
+
- `overviewContent`: `{overview, startHere, agents, readme}` - bundle documentation content
|
|
331
|
+
- `testInfo`: `{detected, framework, testDirs, testFileCount, configFiles, hint}` - test detection result
|
|
332
|
+
- `nextCommands[]`: Copyable tool calls for next steps (can be directly used as arguments)
|
|
322
333
|
- `claims[]`: Auto-generated findings with evidence
|
|
323
334
|
- `checklistStatus`: Analysis progress (repo_tree, deps, entrypoints, etc.)
|
|
324
335
|
- `openQuestions[]`: Questions with `nextEvidenceToFetch` hints
|
|
325
336
|
- `summary`: Markdown summary with checklist and key findings
|
|
326
337
|
|
|
338
|
+
**Example `nextCommands` output:**
|
|
339
|
+
```json
|
|
340
|
+
[
|
|
341
|
+
{ "tool": "preflight_search_bundle", "description": "Search for specific code", "args": { "bundleId": "...", "query": "<填入关键词>" } },
|
|
342
|
+
{ "tool": "preflight_read_file", "description": "Read core module", "args": { "bundleId": "...", "file": "src/server.ts" } }
|
|
343
|
+
]
|
|
344
|
+
```
|
|
345
|
+
|
|
327
346
|
### `preflight_validate_report` *(NEW v0.4.0)*
|
|
328
347
|
Validate claims and evidence chains for auditability.
|
|
329
348
|
- Checks: missing evidence, invalid file references, broken snippet hashes
|
package/dist/analysis/deep.js
CHANGED
|
@@ -8,7 +8,7 @@ import { createEmptyCoverageReport, isCoverageSufficient, } from '../types/evide
|
|
|
8
8
|
* This is called by the server after gathering data from each source.
|
|
9
9
|
*/
|
|
10
10
|
export function buildDeepAnalysis(bundleId, components) {
|
|
11
|
-
const { tree, search, deps, traces, focusPath, focusQuery, errors = [] } = components;
|
|
11
|
+
const { tree, search, deps, traces, overviewContent, testInfo, focusPath, focusQuery, errors = [] } = components;
|
|
12
12
|
// Build coverage report
|
|
13
13
|
const coverageReport = createEmptyCoverageReport();
|
|
14
14
|
if (tree) {
|
|
@@ -75,9 +75,28 @@ export function buildDeepAnalysis(bundleId, components) {
|
|
|
75
75
|
}
|
|
76
76
|
summaryParts.push('');
|
|
77
77
|
}
|
|
78
|
+
// Test detection summary
|
|
79
|
+
if (testInfo) {
|
|
80
|
+
summaryParts.push(`## Test Detection`);
|
|
81
|
+
if (testInfo.detected) {
|
|
82
|
+
summaryParts.push(`- Framework: ${testInfo.framework ?? 'unknown'}`);
|
|
83
|
+
summaryParts.push(`- Test files: ${testInfo.testFileCount}`);
|
|
84
|
+
if (testInfo.testDirs.length > 0) {
|
|
85
|
+
summaryParts.push(`- Test directories: ${testInfo.testDirs.slice(0, 3).join(', ')}`);
|
|
86
|
+
}
|
|
87
|
+
if (testInfo.configFiles.length > 0) {
|
|
88
|
+
summaryParts.push(`- Config files: ${testInfo.configFiles.join(', ')}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
summaryParts.push(`- No tests detected`);
|
|
93
|
+
}
|
|
94
|
+
summaryParts.push(`- 💡 ${testInfo.hint}`);
|
|
95
|
+
summaryParts.push('');
|
|
96
|
+
}
|
|
78
97
|
// Build checklist status
|
|
79
98
|
const checklistStatus = {
|
|
80
|
-
read_overview:
|
|
99
|
+
read_overview: !!(overviewContent?.overview || overviewContent?.startHere),
|
|
81
100
|
repo_tree: !!tree && tree.totalFiles > 0,
|
|
82
101
|
search_focus: !!search && search.totalHits > 0,
|
|
83
102
|
dependency_graph_global: !!deps && deps.totalNodes > 0,
|
|
@@ -271,6 +290,51 @@ export function buildDeepAnalysis(bundleId, components) {
|
|
|
271
290
|
if (isCoverageSufficient(coverageReport) && openQuestions.length === 0 && claims.length > 0) {
|
|
272
291
|
nextSteps.push('🎉 Analysis complete - all key areas covered. Ready for detailed review.');
|
|
273
292
|
}
|
|
293
|
+
// Build nextCommands (copyable JSON for LLM)
|
|
294
|
+
const nextCommands = [];
|
|
295
|
+
// Always suggest search as a useful next step
|
|
296
|
+
nextCommands.push({
|
|
297
|
+
tool: 'preflight_search_bundle',
|
|
298
|
+
description: 'Search for specific code or concepts',
|
|
299
|
+
args: { bundleId, query: '<填入关键词>', scope: 'all', limit: 30 },
|
|
300
|
+
});
|
|
301
|
+
// Suggest reading a specific entry point if identified
|
|
302
|
+
if (deps && deps.topImported.length > 0) {
|
|
303
|
+
const coreFile = deps.topImported[0].file;
|
|
304
|
+
nextCommands.push({
|
|
305
|
+
tool: 'preflight_read_file',
|
|
306
|
+
description: `Read core module: ${coreFile}`,
|
|
307
|
+
args: { bundleId, file: coreFile, withLineNumbers: true },
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
// Suggest dependency analysis for a specific file if entry point identified
|
|
311
|
+
if (deps && deps.topImporters.length > 0) {
|
|
312
|
+
const entryFile = deps.topImporters[0].file;
|
|
313
|
+
nextCommands.push({
|
|
314
|
+
tool: 'preflight_evidence_dependency_graph',
|
|
315
|
+
description: `Analyze dependencies of entry point: ${entryFile}`,
|
|
316
|
+
args: { bundleId, target: { file: entryFile } },
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
// Suggest trace discovery if no traces exist
|
|
320
|
+
if (!traces || traces.totalLinks === 0) {
|
|
321
|
+
nextCommands.push({
|
|
322
|
+
tool: 'preflight_suggest_traces',
|
|
323
|
+
description: 'Auto-discover test↔code relationships',
|
|
324
|
+
args: { bundleId, edge_type: 'tested_by', scope: 'repo' },
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
// Suggest focused tree if large directory detected
|
|
328
|
+
if (tree) {
|
|
329
|
+
const largeDir = tree.topDirs.find(d => d.fileCount > 50);
|
|
330
|
+
if (largeDir) {
|
|
331
|
+
nextCommands.push({
|
|
332
|
+
tool: 'preflight_repo_tree',
|
|
333
|
+
description: `Explore large directory: ${largeDir.path}`,
|
|
334
|
+
args: { bundleId, focusDir: largeDir.path, depth: 6 },
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
274
338
|
// Add checklist and claims to summary
|
|
275
339
|
summaryParts.push(`## Analysis Checklist`);
|
|
276
340
|
const checklistItems = [
|
|
@@ -307,11 +371,104 @@ export function buildDeepAnalysis(bundleId, components) {
|
|
|
307
371
|
search,
|
|
308
372
|
deps,
|
|
309
373
|
traces,
|
|
374
|
+
overviewContent,
|
|
375
|
+
testInfo,
|
|
310
376
|
claims,
|
|
311
377
|
checklistStatus,
|
|
312
378
|
openQuestions,
|
|
313
379
|
coverageReport,
|
|
314
380
|
summary: summaryParts.join('\n'),
|
|
315
381
|
nextSteps,
|
|
382
|
+
nextCommands,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Detect test setup from file tree statistics.
|
|
387
|
+
* Scans for test directories, test files, and framework config files.
|
|
388
|
+
*/
|
|
389
|
+
export function detectTestInfo(stats, filesFound) {
|
|
390
|
+
const testDirs = [];
|
|
391
|
+
let testFileCount = 0;
|
|
392
|
+
const configFiles = [];
|
|
393
|
+
let framework = null;
|
|
394
|
+
// Common test directory patterns
|
|
395
|
+
const testDirPatterns = ['tests', 'test', '__tests__', 'spec', 'specs', 'e2e', 'integration'];
|
|
396
|
+
// Check byTopDir for test directories
|
|
397
|
+
if (stats.byTopDir) {
|
|
398
|
+
for (const [dir, count] of Object.entries(stats.byTopDir)) {
|
|
399
|
+
const dirLower = dir.toLowerCase();
|
|
400
|
+
if (testDirPatterns.some(p => dirLower === p || dirLower.endsWith('/' + p))) {
|
|
401
|
+
testDirs.push(dir);
|
|
402
|
+
testFileCount += count;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
// Framework detection from config files (if filesFound provided)
|
|
407
|
+
const frameworkConfigs = [
|
|
408
|
+
{ pattern: /^jest\.config\.(js|ts|mjs|cjs|json)$/i, framework: 'jest' },
|
|
409
|
+
{ pattern: /^vitest\.config\.(js|ts|mjs|cjs)$/i, framework: 'vitest' },
|
|
410
|
+
{ pattern: /^pytest\.ini$/i, framework: 'pytest' },
|
|
411
|
+
{ pattern: /^pyproject\.toml$/i, framework: 'pytest' }, // May contain pytest config
|
|
412
|
+
{ pattern: /^setup\.cfg$/i, framework: 'pytest' },
|
|
413
|
+
{ pattern: /^\.mocharc\.(js|json|yml|yaml)$/i, framework: 'mocha' },
|
|
414
|
+
{ pattern: /^mocha\.opts$/i, framework: 'mocha' },
|
|
415
|
+
];
|
|
416
|
+
if (filesFound) {
|
|
417
|
+
for (const file of filesFound) {
|
|
418
|
+
for (const cfg of frameworkConfigs) {
|
|
419
|
+
if (cfg.pattern.test(file.name)) {
|
|
420
|
+
configFiles.push(file.path);
|
|
421
|
+
if (!framework) {
|
|
422
|
+
framework = cfg.framework;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
// Infer framework from file extensions if not detected from config
|
|
429
|
+
if (!framework && stats.byExtension) {
|
|
430
|
+
// Check for test file patterns in extensions
|
|
431
|
+
const hasTs = (stats.byExtension['.ts'] ?? 0) > 0 || (stats.byExtension['.tsx'] ?? 0) > 0;
|
|
432
|
+
const hasPy = (stats.byExtension['.py'] ?? 0) > 0;
|
|
433
|
+
const hasGo = (stats.byExtension['.go'] ?? 0) > 0;
|
|
434
|
+
if (testDirs.length > 0 || testFileCount > 0) {
|
|
435
|
+
if (hasGo)
|
|
436
|
+
framework = 'go';
|
|
437
|
+
else if (hasPy)
|
|
438
|
+
framework = 'pytest';
|
|
439
|
+
else if (hasTs)
|
|
440
|
+
framework = 'unknown'; // Could be jest/vitest/mocha
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
// Count test files by pattern (approximate from extensions)
|
|
444
|
+
// This is a heuristic - actual test files may vary
|
|
445
|
+
if (testFileCount === 0 && stats.byExtension) {
|
|
446
|
+
// If no test directories found, estimate based on common patterns
|
|
447
|
+
// This is imprecise but gives a hint
|
|
448
|
+
}
|
|
449
|
+
const detected = testDirs.length > 0 || testFileCount > 0 || configFiles.length > 0;
|
|
450
|
+
// Generate hint based on detection results
|
|
451
|
+
let hint;
|
|
452
|
+
if (detected) {
|
|
453
|
+
if (testFileCount > 0) {
|
|
454
|
+
hint = `Found ${testFileCount} test files. Run preflight_suggest_traces to map code↔test relationships.`;
|
|
455
|
+
}
|
|
456
|
+
else if (configFiles.length > 0) {
|
|
457
|
+
hint = `Test config found (${configFiles[0]}). Run preflight_suggest_traces to discover test files.`;
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
hint = `Test directories found. Run preflight_suggest_traces to map code↔test relationships.`;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
hint = 'No tests detected. Consider adding tests or check if test files use non-standard naming.';
|
|
465
|
+
}
|
|
466
|
+
return {
|
|
467
|
+
detected,
|
|
468
|
+
framework,
|
|
469
|
+
testDirs,
|
|
470
|
+
testFileCount,
|
|
471
|
+
configFiles,
|
|
472
|
+
hint,
|
|
316
473
|
};
|
|
317
474
|
}
|
package/dist/bundle/ingest.js
CHANGED
|
@@ -72,11 +72,21 @@ async function buildIgnore(repoRoot) {
|
|
|
72
72
|
}
|
|
73
73
|
return ig;
|
|
74
74
|
}
|
|
75
|
-
async function* walkFiles(repoRoot, ig) {
|
|
75
|
+
async function* walkFiles(repoRoot, ig, onSkip) {
|
|
76
76
|
const stack = [repoRoot];
|
|
77
77
|
while (stack.length) {
|
|
78
78
|
const dir = stack.pop();
|
|
79
|
-
|
|
79
|
+
let entries;
|
|
80
|
+
try {
|
|
81
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
// Skip directories that cannot be read (broken symlinks, permission issues, etc.)
|
|
85
|
+
const rel = path.relative(repoRoot, dir);
|
|
86
|
+
const relPosix = toPosix(rel);
|
|
87
|
+
onSkip?.(relPosix, `cannot read directory: ${err.code ?? 'unknown'}`);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
80
90
|
for (const ent of entries) {
|
|
81
91
|
const abs = path.join(dir, ent.name);
|
|
82
92
|
const rel = path.relative(repoRoot, abs);
|
|
@@ -85,12 +95,29 @@ async function* walkFiles(repoRoot, ig) {
|
|
|
85
95
|
if (ig.ignores(relPosix)) {
|
|
86
96
|
continue;
|
|
87
97
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
98
|
+
// Handle symlinks and other special entries
|
|
99
|
+
try {
|
|
100
|
+
if (ent.isSymbolicLink()) {
|
|
101
|
+
// Check if symlink target exists
|
|
102
|
+
try {
|
|
103
|
+
await fs.stat(abs);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
onSkip?.(relPosix, 'broken symlink');
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (ent.isDirectory()) {
|
|
111
|
+
stack.push(abs);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (!ent.isFile())
|
|
115
|
+
continue;
|
|
91
116
|
}
|
|
92
|
-
|
|
117
|
+
catch (err) {
|
|
118
|
+
onSkip?.(relPosix, `cannot stat: ${err.code ?? 'unknown'}`);
|
|
93
119
|
continue;
|
|
120
|
+
}
|
|
94
121
|
yield { absPath: abs, relPosix };
|
|
95
122
|
}
|
|
96
123
|
}
|
|
@@ -101,7 +128,9 @@ export async function ingestRepoToBundle(params) {
|
|
|
101
128
|
const files = [];
|
|
102
129
|
const skipped = [];
|
|
103
130
|
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
104
|
-
for await (const f of walkFiles(params.repoRoot, ig)
|
|
131
|
+
for await (const f of walkFiles(params.repoRoot, ig, (relPosix, reason) => {
|
|
132
|
+
skipped.push(`${relPosix} (${reason})`);
|
|
133
|
+
})) {
|
|
105
134
|
// ignore check already done in walkFiles
|
|
106
135
|
const st = await fs.stat(f.absPath);
|
|
107
136
|
if (st.size > params.options.maxFileBytes) {
|
package/dist/server.js
CHANGED
|
@@ -19,7 +19,7 @@ import { DependencyGraphInputSchema, generateDependencyGraph } from './evidence/
|
|
|
19
19
|
import { TraceQueryInputSchema, TraceUpsertInputSchema, traceQuery, traceUpsert } from './trace/service.js';
|
|
20
20
|
import { suggestTestedByTraces } from './trace/suggest.js';
|
|
21
21
|
import { generateRepoTree, formatTreeResult } from './bundle/tree.js';
|
|
22
|
-
import { buildDeepAnalysis } from './analysis/deep.js';
|
|
22
|
+
import { buildDeepAnalysis, detectTestInfo } from './analysis/deep.js';
|
|
23
23
|
import { validateReport } from './analysis/validate.js';
|
|
24
24
|
// RFC v2: New aggregation tools
|
|
25
25
|
import { ReadFilesInputSchema, createReadFilesHandler, readFilesToolDescription } from './tools/readFiles.js';
|
|
@@ -147,7 +147,7 @@ export async function startServer() {
|
|
|
147
147
|
startHttpServer(cfg);
|
|
148
148
|
const server = new McpServer({
|
|
149
149
|
name: 'preflight-mcp',
|
|
150
|
-
version: '0.5.
|
|
150
|
+
version: '0.5.2',
|
|
151
151
|
description: 'Create evidence-based preflight bundles for repositories (docs + code) with SQLite FTS search.',
|
|
152
152
|
}, {
|
|
153
153
|
capabilities: {
|
|
@@ -2071,6 +2071,9 @@ export async function startServer() {
|
|
|
2071
2071
|
includeSearch: z.boolean().optional().default(true).describe('Include search results (requires focus.query).'),
|
|
2072
2072
|
includeDeps: z.boolean().optional().default(true).describe('Include dependency analysis.'),
|
|
2073
2073
|
includeTraces: z.boolean().optional().default(true).describe('Include trace link summary.'),
|
|
2074
|
+
includeOverview: z.boolean().optional().default(true).describe('Include OVERVIEW.md, START_HERE.md, AGENTS.md content.'),
|
|
2075
|
+
includeReadme: z.boolean().optional().default(true).describe('Include README.md content from repos.'),
|
|
2076
|
+
includeTests: z.boolean().optional().default(true).describe('Detect test directories and frameworks.'),
|
|
2074
2077
|
tokenBudget: z.number().int().optional().describe('Soft limit on output tokens (reduces detail if exceeded).'),
|
|
2075
2078
|
maxFiles: z.number().int().min(10).max(1000).optional().default(500).describe('Max files to scan for tree/deps.'),
|
|
2076
2079
|
}).optional().describe('Analysis options.'),
|
|
@@ -2103,6 +2106,20 @@ export async function startServer() {
|
|
|
2103
2106
|
byType: z.record(z.string(), z.number()),
|
|
2104
2107
|
coverageEstimate: z.number(),
|
|
2105
2108
|
}).optional(),
|
|
2109
|
+
overviewContent: z.object({
|
|
2110
|
+
overview: z.string().optional(),
|
|
2111
|
+
startHere: z.string().optional(),
|
|
2112
|
+
agents: z.string().optional(),
|
|
2113
|
+
readme: z.string().optional(),
|
|
2114
|
+
}).optional().describe('Overview content from bundle files (OVERVIEW.md, START_HERE.md, AGENTS.md, README.md).'),
|
|
2115
|
+
testInfo: z.object({
|
|
2116
|
+
detected: z.boolean(),
|
|
2117
|
+
framework: z.enum(['jest', 'vitest', 'pytest', 'go', 'mocha', 'unknown']).nullable(),
|
|
2118
|
+
testDirs: z.array(z.string()),
|
|
2119
|
+
testFileCount: z.number(),
|
|
2120
|
+
configFiles: z.array(z.string()),
|
|
2121
|
+
hint: z.string(),
|
|
2122
|
+
}).optional().describe('Test detection result.'),
|
|
2106
2123
|
claims: z.array(z.object({
|
|
2107
2124
|
id: z.string(),
|
|
2108
2125
|
text: z.string(),
|
|
@@ -2129,7 +2146,12 @@ export async function startServer() {
|
|
|
2129
2146
|
})).describe('Questions that could not be answered.'),
|
|
2130
2147
|
coverageReport: z.any(),
|
|
2131
2148
|
summary: z.string().describe('LLM-formatted analysis summary with checklist and claims.'),
|
|
2132
|
-
nextSteps: z.array(z.string()),
|
|
2149
|
+
nextSteps: z.array(z.string()).describe('Human-readable next step suggestions.'),
|
|
2150
|
+
nextCommands: z.array(z.object({
|
|
2151
|
+
tool: z.string(),
|
|
2152
|
+
description: z.string(),
|
|
2153
|
+
args: z.record(z.string(), z.unknown()),
|
|
2154
|
+
})).describe('Copyable next commands for LLM/automation - can be directly used as tool call arguments.'),
|
|
2133
2155
|
// RFC v2: Top-level evidence aggregation
|
|
2134
2156
|
evidence: z.array(z.object({
|
|
2135
2157
|
path: z.string(),
|
|
@@ -2159,6 +2181,8 @@ export async function startServer() {
|
|
|
2159
2181
|
let search;
|
|
2160
2182
|
let deps;
|
|
2161
2183
|
let traces;
|
|
2184
|
+
let overviewContent;
|
|
2185
|
+
let testInfo;
|
|
2162
2186
|
// 1. Tree
|
|
2163
2187
|
if (opts.includeTree ?? true) {
|
|
2164
2188
|
try {
|
|
@@ -2280,11 +2304,89 @@ export async function startServer() {
|
|
|
2280
2304
|
traces = { totalLinks: 0, byType: {}, coverageEstimate: 0 };
|
|
2281
2305
|
}
|
|
2282
2306
|
}
|
|
2307
|
+
// 5. Overview content (OVERVIEW.md, START_HERE.md, AGENTS.md)
|
|
2308
|
+
if (opts.includeOverview ?? true) {
|
|
2309
|
+
overviewContent = {};
|
|
2310
|
+
const readFile = async (filename) => {
|
|
2311
|
+
try {
|
|
2312
|
+
const absPath = safeJoin(paths.rootDir, filename);
|
|
2313
|
+
return await fs.readFile(absPath, 'utf8');
|
|
2314
|
+
}
|
|
2315
|
+
catch {
|
|
2316
|
+
return undefined;
|
|
2317
|
+
}
|
|
2318
|
+
};
|
|
2319
|
+
overviewContent.overview = await readFile('OVERVIEW.md');
|
|
2320
|
+
overviewContent.startHere = await readFile('START_HERE.md');
|
|
2321
|
+
overviewContent.agents = await readFile('AGENTS.md');
|
|
2322
|
+
}
|
|
2323
|
+
// 6. README content (from repos)
|
|
2324
|
+
if (opts.includeReadme ?? true) {
|
|
2325
|
+
if (!overviewContent)
|
|
2326
|
+
overviewContent = {};
|
|
2327
|
+
try {
|
|
2328
|
+
const manifest = await readManifest(paths.manifestPath);
|
|
2329
|
+
for (const repo of manifest.repos ?? []) {
|
|
2330
|
+
if (!repo.id)
|
|
2331
|
+
continue;
|
|
2332
|
+
const [owner, repoName] = repo.id.split('/');
|
|
2333
|
+
if (!owner || !repoName)
|
|
2334
|
+
continue;
|
|
2335
|
+
const readmeNames = ['README.md', 'readme.md', 'Readme.md'];
|
|
2336
|
+
for (const readmeName of readmeNames) {
|
|
2337
|
+
const readmePath = `repos/${owner}/${repoName}/norm/${readmeName}`;
|
|
2338
|
+
try {
|
|
2339
|
+
const absPath = safeJoin(paths.rootDir, readmePath);
|
|
2340
|
+
overviewContent.readme = await fs.readFile(absPath, 'utf8');
|
|
2341
|
+
break; // Found README, stop searching
|
|
2342
|
+
}
|
|
2343
|
+
catch {
|
|
2344
|
+
// Try next README name
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
if (overviewContent.readme)
|
|
2348
|
+
break; // Only read first repo's README
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
catch {
|
|
2352
|
+
// Ignore manifest read errors
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
// 7. Test detection
|
|
2356
|
+
if ((opts.includeTests ?? true) && tree) {
|
|
2357
|
+
// Collect files for framework detection
|
|
2358
|
+
const filesFound = [];
|
|
2359
|
+
try {
|
|
2360
|
+
// Scan for config files at bundle root
|
|
2361
|
+
const configPatterns = [
|
|
2362
|
+
'jest.config.js', 'jest.config.ts', 'jest.config.mjs', 'jest.config.json',
|
|
2363
|
+
'vitest.config.js', 'vitest.config.ts',
|
|
2364
|
+
'pytest.ini', 'pyproject.toml', 'setup.cfg',
|
|
2365
|
+
'.mocharc.js', '.mocharc.json', '.mocharc.yml',
|
|
2366
|
+
];
|
|
2367
|
+
for (const cfg of configPatterns) {
|
|
2368
|
+
try {
|
|
2369
|
+
const cfgPath = safeJoin(paths.rootDir, cfg);
|
|
2370
|
+
await fs.access(cfgPath);
|
|
2371
|
+
filesFound.push({ path: cfg, name: cfg });
|
|
2372
|
+
}
|
|
2373
|
+
catch {
|
|
2374
|
+
// Config file doesn't exist
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
catch {
|
|
2379
|
+
// Ignore errors during config scanning
|
|
2380
|
+
}
|
|
2381
|
+
testInfo = detectTestInfo({ byExtension: tree.byExtension, byTopDir: tree.topDirs.reduce((acc, d) => ({ ...acc, [d.path]: d.fileCount }), {}) }, filesFound.length > 0 ? filesFound : undefined);
|
|
2382
|
+
}
|
|
2283
2383
|
const result = buildDeepAnalysis(args.bundleId, {
|
|
2284
2384
|
tree,
|
|
2285
2385
|
search,
|
|
2286
2386
|
deps,
|
|
2287
2387
|
traces,
|
|
2388
|
+
overviewContent,
|
|
2389
|
+
testInfo,
|
|
2288
2390
|
focusPath: focus.path,
|
|
2289
2391
|
focusQuery: focus.query,
|
|
2290
2392
|
errors,
|