preflight-mcp 0.1.2 → 0.1.4
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 +49 -142
- package/README.zh-CN.md +141 -124
- package/dist/ast/treeSitter.js +588 -0
- package/dist/bundle/analysis.js +47 -0
- package/dist/bundle/context7.js +65 -36
- package/dist/bundle/facts.js +829 -0
- package/dist/bundle/github.js +34 -3
- package/dist/bundle/githubArchive.js +102 -29
- package/dist/bundle/overview.js +226 -48
- package/dist/bundle/service.js +250 -130
- package/dist/config.js +30 -3
- package/dist/context7/client.js +5 -2
- package/dist/evidence/dependencyGraph.js +1136 -0
- package/dist/http/server.js +109 -0
- package/dist/jobs/progressTracker.js +191 -0
- package/dist/search/sqliteFts.js +150 -10
- package/dist/server.js +340 -326
- package/dist/trace/service.js +108 -0
- package/dist/trace/store.js +170 -0
- package/package.json +4 -2
- package/dist/bundle/deepwiki.js +0 -206
package/dist/bundle/github.js
CHANGED
|
@@ -13,8 +13,26 @@ export function parseOwnerRepo(input) {
|
|
|
13
13
|
export function toCloneUrl(ref) {
|
|
14
14
|
return `https://github.com/${ref.owner}/${ref.repo}.git`;
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Parse git clone progress from stderr.
|
|
18
|
+
* Git outputs progress like:
|
|
19
|
+
* - "Receiving objects: 45% (1234/2741)"
|
|
20
|
+
* - "Resolving deltas: 60% (100/167)"
|
|
21
|
+
*/
|
|
22
|
+
function parseGitProgress(line) {
|
|
23
|
+
// Match patterns like "Receiving objects: 45% (1234/2741)"
|
|
24
|
+
const match = line.match(/(Receiving objects|Resolving deltas|Counting objects|Compressing objects):\s+(\d+)%/);
|
|
25
|
+
if (match) {
|
|
26
|
+
return {
|
|
27
|
+
phase: match[1],
|
|
28
|
+
percent: parseInt(match[2], 10),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
16
33
|
async function runGit(args, opts) {
|
|
17
34
|
const timeoutMs = opts?.timeoutMs ?? 5 * 60_000;
|
|
35
|
+
const onProgress = opts?.onProgress;
|
|
18
36
|
return new Promise((resolve, reject) => {
|
|
19
37
|
const child = spawn('git', args, {
|
|
20
38
|
cwd: opts?.cwd,
|
|
@@ -65,7 +83,19 @@ async function runGit(args, opts) {
|
|
|
65
83
|
stdout += data.toString('utf8');
|
|
66
84
|
});
|
|
67
85
|
child.stderr?.on('data', (data) => {
|
|
68
|
-
|
|
86
|
+
const chunk = data.toString('utf8');
|
|
87
|
+
stderr += chunk;
|
|
88
|
+
// Parse and report progress
|
|
89
|
+
if (onProgress) {
|
|
90
|
+
// Git progress can come in chunks, split by lines
|
|
91
|
+
const lines = chunk.split(/[\r\n]+/);
|
|
92
|
+
for (const line of lines) {
|
|
93
|
+
const progress = parseGitProgress(line);
|
|
94
|
+
if (progress) {
|
|
95
|
+
onProgress(progress.phase, progress.percent, `${progress.phase}: ${progress.percent}%`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
69
99
|
});
|
|
70
100
|
child.on('error', (err) => {
|
|
71
101
|
cleanup();
|
|
@@ -125,14 +155,15 @@ export async function shallowClone(cloneUrl, destDir, opts) {
|
|
|
125
155
|
await fs.mkdir(path.dirname(destDir), { recursive: true });
|
|
126
156
|
// Clean dest if exists.
|
|
127
157
|
await fs.rm(destDir, { recursive: true, force: true });
|
|
128
|
-
|
|
158
|
+
// Use --progress to force progress output even when not attached to a terminal
|
|
159
|
+
const args = ['-c', 'core.autocrlf=false', 'clone', '--depth', '1', '--no-tags', '--single-branch', '--progress'];
|
|
129
160
|
if (opts?.ref) {
|
|
130
161
|
// Validate ref before using it in git command
|
|
131
162
|
validateGitRef(opts.ref);
|
|
132
163
|
args.push('--branch', opts.ref);
|
|
133
164
|
}
|
|
134
165
|
args.push(cloneUrl, destDir);
|
|
135
|
-
await runGit(args, { timeoutMs: opts?.timeoutMs ?? 15 * 60_000 });
|
|
166
|
+
await runGit(args, { timeoutMs: opts?.timeoutMs ?? 15 * 60_000, onProgress: opts?.onProgress });
|
|
136
167
|
}
|
|
137
168
|
export async function getLocalHeadSha(repoDir) {
|
|
138
169
|
const { stdout } = await runGit(['-C', repoDir, 'rev-parse', 'HEAD']);
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import AdmZip from 'adm-zip';
|
|
4
|
+
import { logger } from '../logging/logger.js';
|
|
4
5
|
function nowIso() {
|
|
5
6
|
return new Date().toISOString();
|
|
6
7
|
}
|
|
7
8
|
function githubHeaders(cfg) {
|
|
8
9
|
const headers = {
|
|
9
|
-
'User-Agent': 'preflight-mcp/0.1.
|
|
10
|
+
'User-Agent': 'preflight-mcp/0.1.3',
|
|
10
11
|
Accept: 'application/vnd.github+json',
|
|
11
12
|
};
|
|
12
13
|
if (cfg.githubToken) {
|
|
@@ -17,36 +18,106 @@ function githubHeaders(cfg) {
|
|
|
17
18
|
async function ensureDir(p) {
|
|
18
19
|
await fs.mkdir(p, { recursive: true });
|
|
19
20
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
/** Default timeout for GitHub API requests (30 seconds). */
|
|
22
|
+
const DEFAULT_API_TIMEOUT_MS = 30_000;
|
|
23
|
+
/** Default timeout for file downloads (5 minutes). */
|
|
24
|
+
const DEFAULT_DOWNLOAD_TIMEOUT_MS = 5 * 60_000;
|
|
25
|
+
async function fetchJson(url, headers, timeoutMs = DEFAULT_API_TIMEOUT_MS) {
|
|
26
|
+
const controller = new AbortController();
|
|
27
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
28
|
+
try {
|
|
29
|
+
const res = await fetch(url, { headers, signal: controller.signal });
|
|
30
|
+
if (!res.ok) {
|
|
31
|
+
throw new Error(`GitHub API error ${res.status}: ${res.statusText}`);
|
|
32
|
+
}
|
|
33
|
+
return (await res.json());
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
clearTimeout(timeoutId);
|
|
24
37
|
}
|
|
25
|
-
return (await res.json());
|
|
26
38
|
}
|
|
27
|
-
async function downloadToFile(url, headers, destPath) {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
39
|
+
async function downloadToFile(url, headers, destPath, timeoutMs = DEFAULT_DOWNLOAD_TIMEOUT_MS, onProgress) {
|
|
40
|
+
const controller = new AbortController();
|
|
41
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
42
|
+
try {
|
|
43
|
+
const res = await fetch(url, { headers, redirect: 'follow', signal: controller.signal });
|
|
44
|
+
if (!res.ok) {
|
|
45
|
+
throw new Error(`Download error ${res.status}: ${res.statusText}`);
|
|
46
|
+
}
|
|
47
|
+
// Get content length for progress reporting
|
|
48
|
+
const contentLengthHeader = res.headers.get('content-length');
|
|
49
|
+
const totalBytes = contentLengthHeader ? parseInt(contentLengthHeader, 10) : undefined;
|
|
50
|
+
await ensureDir(path.dirname(destPath));
|
|
51
|
+
// Use streaming to report progress
|
|
52
|
+
const anyRes = res;
|
|
53
|
+
const body = anyRes.body;
|
|
54
|
+
if (body && typeof body[Symbol.asyncIterator] === 'function') {
|
|
55
|
+
// Async iterator for progress tracking
|
|
56
|
+
const fsModule = await import('node:fs');
|
|
57
|
+
const ws = fsModule.createWriteStream(destPath);
|
|
58
|
+
let downloadedBytes = 0;
|
|
59
|
+
let lastReportTime = Date.now();
|
|
60
|
+
const reportIntervalMs = 500; // Report at most every 500ms
|
|
61
|
+
try {
|
|
62
|
+
for await (const chunk of body) {
|
|
63
|
+
ws.write(chunk);
|
|
64
|
+
downloadedBytes += chunk.length;
|
|
65
|
+
// Throttle progress reports
|
|
66
|
+
const now = Date.now();
|
|
67
|
+
if (onProgress && (now - lastReportTime > reportIntervalMs)) {
|
|
68
|
+
lastReportTime = now;
|
|
69
|
+
const percent = totalBytes ? Math.round((downloadedBytes / totalBytes) * 100) : 0;
|
|
70
|
+
const msg = totalBytes
|
|
71
|
+
? `Downloaded ${formatBytes(downloadedBytes)} / ${formatBytes(totalBytes)} (${percent}%)`
|
|
72
|
+
: `Downloaded ${formatBytes(downloadedBytes)}`;
|
|
73
|
+
onProgress(downloadedBytes, totalBytes, msg);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
ws.end();
|
|
79
|
+
await new Promise((resolve) => ws.on('finish', () => resolve()));
|
|
80
|
+
}
|
|
81
|
+
// Final progress report
|
|
82
|
+
if (onProgress) {
|
|
83
|
+
const msg = totalBytes
|
|
84
|
+
? `Downloaded ${formatBytes(downloadedBytes)} / ${formatBytes(totalBytes)} (100%)`
|
|
85
|
+
: `Downloaded ${formatBytes(downloadedBytes)}`;
|
|
86
|
+
onProgress(downloadedBytes, totalBytes, msg);
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (body && typeof body.pipe === 'function') {
|
|
91
|
+
// Node.js stream (fallback without progress)
|
|
92
|
+
const ws = (await import('node:fs')).createWriteStream(destPath);
|
|
93
|
+
await new Promise((resolve, reject) => {
|
|
94
|
+
body.pipe(ws);
|
|
95
|
+
body.on('error', reject);
|
|
96
|
+
ws.on('error', reject);
|
|
97
|
+
ws.on('finish', () => resolve());
|
|
98
|
+
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
// Web stream or no stream support (fallback without progress)
|
|
102
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
103
|
+
await fs.writeFile(destPath, buf);
|
|
104
|
+
if (onProgress) {
|
|
105
|
+
onProgress(buf.length, buf.length, `Downloaded ${formatBytes(buf.length)}`);
|
|
106
|
+
}
|
|
31
107
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const body = anyRes.body;
|
|
35
|
-
await ensureDir(path.dirname(destPath));
|
|
36
|
-
if (body && typeof body.pipe === 'function') {
|
|
37
|
-
// Node.js stream
|
|
38
|
-
const ws = (await import('node:fs')).createWriteStream(destPath);
|
|
39
|
-
await new Promise((resolve, reject) => {
|
|
40
|
-
body.pipe(ws);
|
|
41
|
-
body.on('error', reject);
|
|
42
|
-
ws.on('error', reject);
|
|
43
|
-
ws.on('finish', () => resolve());
|
|
44
|
-
});
|
|
45
|
-
return;
|
|
108
|
+
finally {
|
|
109
|
+
clearTimeout(timeoutId);
|
|
46
110
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
111
|
+
}
|
|
112
|
+
/** Format bytes for display */
|
|
113
|
+
function formatBytes(bytes) {
|
|
114
|
+
if (bytes < 1024)
|
|
115
|
+
return `${bytes}B`;
|
|
116
|
+
if (bytes < 1024 * 1024)
|
|
117
|
+
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
118
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
119
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
120
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)}GB`;
|
|
50
121
|
}
|
|
51
122
|
async function extractZip(zipPath, destDir) {
|
|
52
123
|
await ensureDir(destDir);
|
|
@@ -72,11 +143,13 @@ export async function downloadAndExtractGitHubArchive(params) {
|
|
|
72
143
|
// Use the API zipball endpoint so ref can be branch/tag/SHA (including slashes via URL-encoding).
|
|
73
144
|
const zipballUrl = `https://api.github.com/repos/${params.owner}/${params.repo}/zipball/${encodeURIComponent(refUsed)}`;
|
|
74
145
|
await ensureDir(params.destDir);
|
|
75
|
-
await downloadToFile(zipballUrl, headers, zipPath);
|
|
146
|
+
await downloadToFile(zipballUrl, headers, zipPath, DEFAULT_DOWNLOAD_TIMEOUT_MS, params.onProgress);
|
|
76
147
|
const extractDir = path.join(params.destDir, `extracted-${Date.now()}`);
|
|
77
148
|
await extractZip(zipPath, extractDir);
|
|
78
149
|
const repoRoot = await findSingleTopLevelDir(extractDir);
|
|
79
150
|
// Best-effort cleanup: remove zip file (keep extracted for caller to consume).
|
|
80
|
-
await fs.rm(zipPath, { force: true }).catch(() =>
|
|
151
|
+
await fs.rm(zipPath, { force: true }).catch((err) => {
|
|
152
|
+
logger.debug(`Failed to cleanup zip file ${zipPath} (non-critical)`, err instanceof Error ? err : undefined);
|
|
153
|
+
});
|
|
81
154
|
return { repoRoot, refUsed, fetchedAt: nowIso() };
|
|
82
155
|
}
|
package/dist/bundle/overview.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { readFacts } from './facts.js';
|
|
3
4
|
function evidence(p, start, end) {
|
|
4
5
|
return `(evidence: ${p}:${start}-${end})`;
|
|
5
6
|
}
|
|
@@ -156,64 +157,241 @@ async function renderContext7LibraryFacts(bundleRootDir, lib) {
|
|
|
156
157
|
}
|
|
157
158
|
return out;
|
|
158
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* Phase 3: Extract project purpose from README.md
|
|
162
|
+
*/
|
|
163
|
+
async function extractProjectPurpose(files) {
|
|
164
|
+
const readme = files.find(f => f.repoRelativePath.toLowerCase() === 'readme.md');
|
|
165
|
+
if (!readme)
|
|
166
|
+
return null;
|
|
167
|
+
try {
|
|
168
|
+
const content = await fs.readFile(readme.bundleNormAbsPath, 'utf8');
|
|
169
|
+
const lines = content.split('\n');
|
|
170
|
+
// Skip title (first h1)
|
|
171
|
+
let startIdx = 0;
|
|
172
|
+
for (let i = 0; i < lines.length; i++) {
|
|
173
|
+
if (lines[i]?.startsWith('# ')) {
|
|
174
|
+
startIdx = i + 1;
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// Extract first paragraph (non-empty lines until empty line or next heading)
|
|
179
|
+
const paragraph = [];
|
|
180
|
+
for (let i = startIdx; i < Math.min(lines.length, startIdx + 20); i++) {
|
|
181
|
+
const line = lines[i]?.trim() || '';
|
|
182
|
+
if (!line || line.startsWith('#'))
|
|
183
|
+
break;
|
|
184
|
+
paragraph.push(line);
|
|
185
|
+
}
|
|
186
|
+
return paragraph.join(' ').trim() || null;
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Phase 3: Format module list for display
|
|
194
|
+
*/
|
|
195
|
+
function formatCoreModules(facts) {
|
|
196
|
+
if (!facts.modules || facts.modules.length === 0)
|
|
197
|
+
return [];
|
|
198
|
+
const coreModules = facts.modules
|
|
199
|
+
.filter(m => m.role === 'core')
|
|
200
|
+
.sort((a, b) => b.exports.length - a.exports.length)
|
|
201
|
+
.slice(0, 10);
|
|
202
|
+
if (coreModules.length === 0)
|
|
203
|
+
return [];
|
|
204
|
+
const lines = [];
|
|
205
|
+
for (const mod of coreModules) {
|
|
206
|
+
const shortPath = mod.path.replace(/^repos\/[^\/]+\/[^\/]+\/norm\//, '');
|
|
207
|
+
lines.push(`- **${shortPath}**`);
|
|
208
|
+
lines.push(` - Exports: ${mod.exports.slice(0, 5).join(', ')}${mod.exports.length > 5 ? ` (+${mod.exports.length - 5} more)` : ''}`);
|
|
209
|
+
lines.push(` - Complexity: ${mod.complexity}, LOC: ${mod.loc}`);
|
|
210
|
+
lines.push(` - Evidence: ${mod.path}:1`);
|
|
211
|
+
}
|
|
212
|
+
return lines;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Phase 3: Format standalone modules for reuse guidance
|
|
216
|
+
*/
|
|
217
|
+
function formatStandaloneModules(facts) {
|
|
218
|
+
if (!facts.modules || facts.modules.length === 0)
|
|
219
|
+
return [];
|
|
220
|
+
const standalone = facts.modules
|
|
221
|
+
.filter(m => m.standalone && (m.role === 'core' || m.role === 'utility'))
|
|
222
|
+
.filter(m => m.exports.length > 0)
|
|
223
|
+
.slice(0, 5);
|
|
224
|
+
if (standalone.length === 0)
|
|
225
|
+
return [];
|
|
226
|
+
const lines = [];
|
|
227
|
+
for (const mod of standalone) {
|
|
228
|
+
const shortPath = mod.path.replace(/^repos\/[^\/]+\/[^\/]+\/norm\//, '');
|
|
229
|
+
lines.push(`- **${shortPath}**`);
|
|
230
|
+
lines.push(` - Can be used independently`);
|
|
231
|
+
lines.push(` - Exports: ${mod.exports.slice(0, 3).join(', ')}`);
|
|
232
|
+
lines.push(` - External deps: ${mod.imports.filter(i => !i.startsWith('.')).slice(0, 3).join(', ') || 'None'}`);
|
|
233
|
+
}
|
|
234
|
+
return lines;
|
|
235
|
+
}
|
|
159
236
|
export async function generateOverviewMarkdown(params) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
//
|
|
186
|
-
|
|
187
|
-
.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
.
|
|
191
|
-
|
|
192
|
-
sections.push('### Code paths spotted (sample)');
|
|
193
|
-
for (const p of codeSamples) {
|
|
194
|
-
const file = r.files.find((f) => f.repoRelativePath === p);
|
|
195
|
-
if (!file)
|
|
196
|
-
continue;
|
|
197
|
-
sections.push(`- ${file.bundleNormRelativePath}. ${evidence(file.bundleNormRelativePath, 1, 1)}`);
|
|
237
|
+
// Load FACTS.json if available
|
|
238
|
+
const factsPath = path.join(params.bundleRootDir, 'analysis', 'FACTS.json');
|
|
239
|
+
const facts = await readFacts(factsPath);
|
|
240
|
+
const sections = [];
|
|
241
|
+
// Header
|
|
242
|
+
sections.push(`# ${params.repos[0]?.repoId || 'Project'} - Overview\r\n`);
|
|
243
|
+
// Phase 3: What is this?
|
|
244
|
+
if (facts) {
|
|
245
|
+
sections.push('## What is this?\r\n');
|
|
246
|
+
// Try to get project purpose from README
|
|
247
|
+
const allFiles = params.repos.flatMap(r => r.files);
|
|
248
|
+
const purpose = await extractProjectPurpose(allFiles);
|
|
249
|
+
if (purpose) {
|
|
250
|
+
sections.push(`**Purpose**: ${purpose}\r\n`);
|
|
251
|
+
}
|
|
252
|
+
// Primary language and frameworks
|
|
253
|
+
if (facts.languages && facts.languages.length > 0) {
|
|
254
|
+
const primaryLang = facts.languages[0];
|
|
255
|
+
if (primaryLang) {
|
|
256
|
+
sections.push(`**Language**: ${primaryLang.language} (${primaryLang.fileCount} files)\r\n`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (facts.frameworks && facts.frameworks.length > 0) {
|
|
260
|
+
sections.push(`**Frameworks**: ${facts.frameworks.join(', ')}\r\n`);
|
|
261
|
+
}
|
|
262
|
+
// Tech stack (Phase 2)
|
|
263
|
+
if (facts.techStack) {
|
|
264
|
+
if (facts.techStack.runtime) {
|
|
265
|
+
sections.push(`**Runtime**: ${facts.techStack.runtime}\r\n`);
|
|
266
|
+
}
|
|
267
|
+
if (facts.techStack.packageManager) {
|
|
268
|
+
sections.push(`**Package Manager**: ${facts.techStack.packageManager}\r\n`);
|
|
198
269
|
}
|
|
199
270
|
}
|
|
200
271
|
sections.push('');
|
|
201
272
|
}
|
|
202
|
-
|
|
203
|
-
if (
|
|
204
|
-
sections.push('##
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
sections.push(
|
|
208
|
-
|
|
209
|
-
|
|
273
|
+
// Phase 3: Architecture
|
|
274
|
+
if (facts) {
|
|
275
|
+
sections.push('## Architecture\r\n');
|
|
276
|
+
// Entry points
|
|
277
|
+
if (facts.entryPoints && facts.entryPoints.length > 0) {
|
|
278
|
+
sections.push('### Entry Points\r\n');
|
|
279
|
+
for (const ep of facts.entryPoints.slice(0, 5)) {
|
|
280
|
+
const shortPath = ep.file.replace(/^repos\/[^\/]+\/[^\/]+\/norm\//, '');
|
|
281
|
+
sections.push(`- \`${shortPath}\` (${ep.type}). ${evidence(ep.evidence, 1, 1)}\r\n`);
|
|
210
282
|
}
|
|
211
|
-
|
|
212
|
-
|
|
283
|
+
sections.push('');
|
|
284
|
+
}
|
|
285
|
+
// Phase 2: Architecture patterns
|
|
286
|
+
if (facts.patterns && facts.patterns.length > 0) {
|
|
287
|
+
sections.push('### Design Patterns\r\n');
|
|
288
|
+
for (const pattern of facts.patterns) {
|
|
289
|
+
sections.push(`- ${pattern}\r\n`);
|
|
290
|
+
}
|
|
291
|
+
sections.push('');
|
|
292
|
+
}
|
|
293
|
+
// Phase 2: Core modules
|
|
294
|
+
const coreModuleLines = formatCoreModules(facts);
|
|
295
|
+
if (coreModuleLines.length > 0) {
|
|
296
|
+
sections.push('### Core Modules\r\n');
|
|
297
|
+
sections.push(...coreModuleLines.map(l => l + '\r\n'));
|
|
298
|
+
sections.push('');
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
// Dependencies
|
|
302
|
+
if (facts && (facts.dependencies.runtime.length > 0 || facts.dependencies.dev.length > 0)) {
|
|
303
|
+
sections.push('## Dependencies\r\n');
|
|
304
|
+
if (facts.dependencies.runtime.length > 0) {
|
|
305
|
+
sections.push(`### Production (${facts.dependencies.runtime.length})\r\n`);
|
|
306
|
+
for (const dep of facts.dependencies.runtime.slice(0, 15)) {
|
|
307
|
+
sections.push(`- ${dep.name}${dep.version ? ` ${dep.version}` : ''}\r\n`);
|
|
308
|
+
}
|
|
309
|
+
if (facts.dependencies.runtime.length > 15) {
|
|
310
|
+
sections.push(`- ... and ${facts.dependencies.runtime.length - 15} more\r\n`);
|
|
311
|
+
}
|
|
312
|
+
sections.push('');
|
|
313
|
+
}
|
|
314
|
+
if (facts.dependencies.dev.length > 0) {
|
|
315
|
+
sections.push(`### Development (${facts.dependencies.dev.length})\r\n`);
|
|
316
|
+
for (const dep of facts.dependencies.dev.slice(0, 10)) {
|
|
317
|
+
sections.push(`- ${dep.name}${dep.version ? ` ${dep.version}` : ''}\r\n`);
|
|
318
|
+
}
|
|
319
|
+
if (facts.dependencies.dev.length > 10) {
|
|
320
|
+
sections.push(`- ... and ${facts.dependencies.dev.length - 10} more\r\n`);
|
|
213
321
|
}
|
|
214
322
|
sections.push('');
|
|
215
323
|
}
|
|
216
324
|
}
|
|
325
|
+
// Phase 3: How to Reuse
|
|
326
|
+
if (facts) {
|
|
327
|
+
const standaloneLines = formatStandaloneModules(facts);
|
|
328
|
+
if (standaloneLines.length > 0) {
|
|
329
|
+
sections.push('## How to Reuse\r\n');
|
|
330
|
+
sections.push('### Standalone Modules\r\n');
|
|
331
|
+
sections.push('These modules can be extracted and used independently:\r\n\r\n');
|
|
332
|
+
sections.push(...standaloneLines.map(l => l + '\r\n'));
|
|
333
|
+
sections.push('');
|
|
334
|
+
}
|
|
335
|
+
// Return Phase 3 format directly
|
|
336
|
+
return sections.join('\n') + '\n';
|
|
337
|
+
}
|
|
338
|
+
// Fallback to legacy format if no FACTS
|
|
339
|
+
{
|
|
340
|
+
const header = `# OVERVIEW.md - Preflight Bundle ${params.bundleId}\r\n\r\nThis file is generated. It contains **only factual statements** with evidence pointers into bundle files.\r\n\r\n`;
|
|
341
|
+
sections.splice(0, sections.length); // Clear Phase 3 sections
|
|
342
|
+
sections.push(header);
|
|
343
|
+
for (const r of params.repos) {
|
|
344
|
+
sections.push(`## Repo: ${r.repoId}`);
|
|
345
|
+
const metaFacts = await renderRepoMetaFacts(params.bundleRootDir, r.repoId);
|
|
346
|
+
if (metaFacts.length) {
|
|
347
|
+
sections.push('### Snapshot facts');
|
|
348
|
+
sections.push(...metaFacts);
|
|
349
|
+
}
|
|
350
|
+
const nodeFacts = await renderNodePackageFacts(r.files);
|
|
351
|
+
if (nodeFacts.length) {
|
|
352
|
+
sections.push('### Node/JS facts');
|
|
353
|
+
sections.push(...nodeFacts);
|
|
354
|
+
}
|
|
355
|
+
const docs = getRepoDocFiles(r.files).slice(0, 50);
|
|
356
|
+
if (docs.length) {
|
|
357
|
+
sections.push('### Documentation files (first 50)');
|
|
358
|
+
for (const d of docs) {
|
|
359
|
+
sections.push(`- ${d.bundleNormRelativePath}. ${evidence(d.bundleNormRelativePath, 1, 1)}`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
// Give a small hint about where code lives, without guessing entry points.
|
|
363
|
+
const codeSamples = r.files
|
|
364
|
+
.filter((f) => f.kind === 'code')
|
|
365
|
+
.map((f) => f.repoRelativePath)
|
|
366
|
+
.filter((p) => p.startsWith('src/') || p.startsWith('lib/'))
|
|
367
|
+
.slice(0, 10);
|
|
368
|
+
if (codeSamples.length) {
|
|
369
|
+
sections.push('### Code paths spotted (sample)');
|
|
370
|
+
for (const p of codeSamples) {
|
|
371
|
+
const file = r.files.find((f) => f.repoRelativePath === p);
|
|
372
|
+
if (!file)
|
|
373
|
+
continue;
|
|
374
|
+
sections.push(`- ${file.bundleNormRelativePath}. ${evidence(file.bundleNormRelativePath, 1, 1)}`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
sections.push('');
|
|
378
|
+
}
|
|
379
|
+
const libs = params.libraries ?? [];
|
|
380
|
+
if (libs.length) {
|
|
381
|
+
sections.push('## Context7 libraries');
|
|
382
|
+
for (const lib of libs) {
|
|
383
|
+
const facts = await renderContext7LibraryFacts(params.bundleRootDir, lib);
|
|
384
|
+
sections.push(`### ${lib.input}`);
|
|
385
|
+
if (facts.length) {
|
|
386
|
+
sections.push(...facts);
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
sections.push('- No library facts available.');
|
|
390
|
+
}
|
|
391
|
+
sections.push('');
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
217
395
|
return sections.join('\n') + '\n';
|
|
218
396
|
}
|
|
219
397
|
export async function writeOverviewFile(targetPath, markdown) {
|