aibridge-context 1.5.1 → 2.0.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/bin/cli.js +469 -170
- package/core/briefingGenerator.js +368 -0
- package/core/codeDiff.js +304 -0
- package/core/fileSnapshot.js +178 -0
- package/core/init.js +16 -2
- package/core/stateManager.js +802 -1307
- package/core/watcher.js +78 -49
- package/index.js +23 -9
- package/package.json +7 -3
- package/server/routes.js +30 -34
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* briefingGenerator.js
|
|
5
|
+
* --------------------
|
|
6
|
+
* Generates .ai-context/briefing.md — a single markdown file
|
|
7
|
+
* that gives any AI assistant a complete, immediate picture
|
|
8
|
+
* of the project: structure, code, errors, fixes, changes,
|
|
9
|
+
* dependencies, routes, and what to work on next.
|
|
10
|
+
*
|
|
11
|
+
* Auto-regenerated on every startup and every file change.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
// ─────────────────────────────────────────────────────────────────
|
|
17
|
+
// Tree renderer
|
|
18
|
+
// ─────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
function renderFileTree(tree, prefix, lines) {
|
|
21
|
+
const entries = Object.entries(tree || {});
|
|
22
|
+
entries.forEach(([name, children], idx) => {
|
|
23
|
+
const isLast = idx === entries.length - 1;
|
|
24
|
+
const connector = isLast ? '└── ' : '├── ';
|
|
25
|
+
const childPrefix = isLast ? ' ' : '│ ';
|
|
26
|
+
lines.push(prefix + connector + name);
|
|
27
|
+
if (children && typeof children === 'object') {
|
|
28
|
+
renderFileTree(children, prefix + childPrefix, lines);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function fileTreeToString(tree) {
|
|
34
|
+
const lines = [];
|
|
35
|
+
renderFileTree(tree, '', lines);
|
|
36
|
+
return lines.join('\n');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ─────────────────────────────────────────────────────────────────
|
|
40
|
+
// Section builders
|
|
41
|
+
// ─────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
function sectionHeader(title) {
|
|
44
|
+
return `\n## ${title}\n`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function buildOverview(state) {
|
|
48
|
+
const ts = state.tech_stack || {};
|
|
49
|
+
const dbs = (ts.databases || []).join(', ') || 'None detected';
|
|
50
|
+
const lines = [
|
|
51
|
+
sectionHeader('Project Overview'),
|
|
52
|
+
`| Field | Value |`,
|
|
53
|
+
`|---|---|`,
|
|
54
|
+
`| **Project** | ${state.project} v${state.version} |`,
|
|
55
|
+
`| **Stage** | ${state.current_stage} |`,
|
|
56
|
+
`| **Language** | ${ts.language || '—'} |`,
|
|
57
|
+
`| **Framework** | ${ts.framework || '—'} |`,
|
|
58
|
+
`| **Runtime** | ${ts.runtime || '—'} |`,
|
|
59
|
+
`| **Package Manager** | ${ts.package_manager || '—'} |`,
|
|
60
|
+
`| **TypeScript** | ${ts.typescript ? 'Yes' : 'No'} |`,
|
|
61
|
+
`| **Databases** | ${dbs} |`,
|
|
62
|
+
`| **Test Framework** | ${ts.test_framework || 'None detected'} |`,
|
|
63
|
+
`| **Linter** | ${ts.linter || 'None'} |`,
|
|
64
|
+
`| **Bundler** | ${ts.bundler || 'None'} |`,
|
|
65
|
+
`| **Cloud / Deploy** | ${ts.cloud_platform || 'None detected'} |`,
|
|
66
|
+
`| **Last Updated** | ${state.last_updated} |`,
|
|
67
|
+
'',
|
|
68
|
+
`**Summary:** ${state.ai_summary}`,
|
|
69
|
+
'',
|
|
70
|
+
(state.description ? `**Description:** ${state.description}` : '')
|
|
71
|
+
];
|
|
72
|
+
return lines.filter((l) => l !== '').join('\n');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function buildActiveErrors(state) {
|
|
76
|
+
const errors = (state.issue_tracker || {}).active_errors || [];
|
|
77
|
+
if (errors.length === 0) {
|
|
78
|
+
return sectionHeader('🔴 Active Errors') + '\n_No active errors. All clear._\n';
|
|
79
|
+
}
|
|
80
|
+
const lines = [sectionHeader('🔴 Active Errors (FIX THESE FIRST)')];
|
|
81
|
+
lines.push(`> **${errors.length} open error${errors.length !== 1 ? 's' : ''} — address before any new work.**\n`);
|
|
82
|
+
for (const e of errors) {
|
|
83
|
+
lines.push(`### ${e.id}`);
|
|
84
|
+
lines.push(`- **Message:** ${e.message}`);
|
|
85
|
+
if (e.file) lines.push(`- **File:** \`${e.file}\``);
|
|
86
|
+
if (e.stack) lines.push(`- **Stack:**\n\`\`\`\n${e.stack.slice(0, 400)}\n\`\`\``);
|
|
87
|
+
lines.push(`- **Recorded:** ${e.timestamp}`);
|
|
88
|
+
lines.push('');
|
|
89
|
+
}
|
|
90
|
+
return lines.join('\n');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function buildResolvedIssues(state) {
|
|
94
|
+
const resolved = (state.issue_tracker || {}).resolved_issues || [];
|
|
95
|
+
if (resolved.length === 0) return '';
|
|
96
|
+
const lines = [sectionHeader('✅ Resolved Issues (History)')];
|
|
97
|
+
lines.push('_Read this before writing any fix — these bugs were already solved._\n');
|
|
98
|
+
for (const r of resolved.slice(0, 15)) {
|
|
99
|
+
lines.push(`- **${r.message}**`);
|
|
100
|
+
if (r.file) lines.push(` - File: \`${r.file}\``);
|
|
101
|
+
lines.push(` - Fixed: ${r.resolved_at || r.timestamp}`);
|
|
102
|
+
lines.push(` - How: ${r.resolution}`);
|
|
103
|
+
}
|
|
104
|
+
if (resolved.length > 15) lines.push(`\n_...and ${resolved.length - 15} more in state.json_`);
|
|
105
|
+
return lines.join('\n');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function buildRecentChanges(state) {
|
|
109
|
+
const changes = state.code_changes || [];
|
|
110
|
+
if (changes.length === 0) {
|
|
111
|
+
return sectionHeader('📝 Recent Code Changes') + '\n_No changes recorded yet._\n';
|
|
112
|
+
}
|
|
113
|
+
const lines = [sectionHeader('📝 Recent Code Changes')];
|
|
114
|
+
for (const c of changes.slice(0, 10)) {
|
|
115
|
+
lines.push(`### \`${c.file}\` — ${c.action} — ${c.timestamp}`);
|
|
116
|
+
lines.push(`**${c.summary}**`);
|
|
117
|
+
if (c.signals && c.signals.length) {
|
|
118
|
+
lines.push('');
|
|
119
|
+
lines.push('What changed:');
|
|
120
|
+
for (const sig of c.signals) lines.push(`- ${sig}`);
|
|
121
|
+
}
|
|
122
|
+
if (c.patch && c.patch.length < 2000) {
|
|
123
|
+
lines.push('');
|
|
124
|
+
lines.push('```diff');
|
|
125
|
+
lines.push(c.patch);
|
|
126
|
+
lines.push('```');
|
|
127
|
+
}
|
|
128
|
+
lines.push('');
|
|
129
|
+
}
|
|
130
|
+
if (changes.length > 10) lines.push(`_...and ${changes.length - 10} more in state.json → code_changes_`);
|
|
131
|
+
return lines.join('\n');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function buildFileStructure(state) {
|
|
135
|
+
const lines = [sectionHeader('📁 File Structure')];
|
|
136
|
+
lines.push('```');
|
|
137
|
+
lines.push(fileTreeToString(state.file_tree || {}));
|
|
138
|
+
lines.push('```');
|
|
139
|
+
return lines.join('\n');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function buildCodeCatalogue(state) {
|
|
143
|
+
const codeFiles = state.code_files || {};
|
|
144
|
+
const files = Object.keys(codeFiles);
|
|
145
|
+
if (files.length === 0) return '';
|
|
146
|
+
|
|
147
|
+
const lines = [sectionHeader('🔍 Code Catalogue')];
|
|
148
|
+
lines.push('_Every source file with its functions and exports._\n');
|
|
149
|
+
|
|
150
|
+
for (const filePath of files) {
|
|
151
|
+
const info = codeFiles[filePath];
|
|
152
|
+
lines.push(`#### \`${filePath}\` (${info.lines} lines)`);
|
|
153
|
+
|
|
154
|
+
if (info.functions && info.functions.length) {
|
|
155
|
+
lines.push(`- **Functions:** ${info.functions.join(', ')}`);
|
|
156
|
+
}
|
|
157
|
+
if (info.classes && info.classes.length) {
|
|
158
|
+
lines.push(`- **Classes:** ${info.classes.join(', ')}`);
|
|
159
|
+
}
|
|
160
|
+
if (info.exports && info.exports.length) {
|
|
161
|
+
lines.push(`- **Exports:** ${info.exports.join(', ')}`);
|
|
162
|
+
}
|
|
163
|
+
if (info.imports && info.imports.length) {
|
|
164
|
+
lines.push(`- **Imports:** ${info.imports.join(', ')}`);
|
|
165
|
+
}
|
|
166
|
+
lines.push('');
|
|
167
|
+
}
|
|
168
|
+
return lines.join('\n');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function buildApiRoutes(state) {
|
|
172
|
+
const routes = state.api_routes || [];
|
|
173
|
+
if (routes.length === 0) return '';
|
|
174
|
+
|
|
175
|
+
const lines = [sectionHeader('🛤 API Routes')];
|
|
176
|
+
lines.push('| Method | Path | File |');
|
|
177
|
+
lines.push('|---|---|---|');
|
|
178
|
+
for (const r of routes) {
|
|
179
|
+
lines.push(`| \`${r.method}\` | \`${r.path}\` | \`${r.file}\` |`);
|
|
180
|
+
}
|
|
181
|
+
return lines.join('\n');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function buildDependencies(state) {
|
|
185
|
+
const deps = state.dependencies || {};
|
|
186
|
+
const prod = Object.entries(deps.production || {});
|
|
187
|
+
const dev = Object.entries(deps.development || {});
|
|
188
|
+
const scripts = Object.entries(deps.scripts || {});
|
|
189
|
+
|
|
190
|
+
const lines = [sectionHeader('📦 Dependencies & Scripts')];
|
|
191
|
+
|
|
192
|
+
if (prod.length) {
|
|
193
|
+
lines.push('**Production:**');
|
|
194
|
+
for (const [name, ver] of prod) lines.push(`- \`${name}\` ${ver}`);
|
|
195
|
+
lines.push('');
|
|
196
|
+
}
|
|
197
|
+
if (dev.length) {
|
|
198
|
+
lines.push('**Development:**');
|
|
199
|
+
for (const [name, ver] of dev) lines.push(`- \`${name}\` ${ver}`);
|
|
200
|
+
lines.push('');
|
|
201
|
+
}
|
|
202
|
+
if (scripts.length) {
|
|
203
|
+
lines.push('**Scripts:**');
|
|
204
|
+
lines.push('| Name | Command |');
|
|
205
|
+
lines.push('|---|---|');
|
|
206
|
+
for (const [name, cmd] of scripts) lines.push(`| \`${name}\` | \`${cmd}\` |`);
|
|
207
|
+
}
|
|
208
|
+
return lines.join('\n');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function buildEnvVariables(state) {
|
|
212
|
+
const vars = state.env_variables || [];
|
|
213
|
+
if (vars.length === 0) return '';
|
|
214
|
+
|
|
215
|
+
const lines = [sectionHeader('🔑 Environment Variables')];
|
|
216
|
+
lines.push('_All env vars referenced in source or .env files. Never invent undocumented ones._\n');
|
|
217
|
+
for (const v of vars) lines.push(`- \`${v}\``);
|
|
218
|
+
return lines.join('\n');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function buildArchitecture(state) {
|
|
222
|
+
const patterns = state.architecture_patterns || [];
|
|
223
|
+
const details = state.implementation_details || [];
|
|
224
|
+
if (!patterns.length && !details.length) return '';
|
|
225
|
+
|
|
226
|
+
const lines = [sectionHeader('🏗 Architecture & Implementation')];
|
|
227
|
+
|
|
228
|
+
if (patterns.length) {
|
|
229
|
+
lines.push('**Patterns in use:**');
|
|
230
|
+
for (const p of patterns) lines.push(`- ${p}`);
|
|
231
|
+
lines.push('');
|
|
232
|
+
}
|
|
233
|
+
if (details.length) {
|
|
234
|
+
lines.push('**Implementation details:**');
|
|
235
|
+
for (const d of details) lines.push(`- ${d}`);
|
|
236
|
+
}
|
|
237
|
+
return lines.join('\n');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function buildKeyFeatures(state) {
|
|
241
|
+
const features = state.key_features || [];
|
|
242
|
+
if (!features.length) return '';
|
|
243
|
+
|
|
244
|
+
const lines = [sectionHeader('⚡ Key Features')];
|
|
245
|
+
for (const f of features) lines.push(`- ${f}`);
|
|
246
|
+
return lines.join('\n');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function buildWorkingContext(state) {
|
|
250
|
+
const lines = [sectionHeader('🎯 Working Context')];
|
|
251
|
+
let hasContent = false;
|
|
252
|
+
|
|
253
|
+
if (state.current_focus) {
|
|
254
|
+
lines.push(`**Current Focus:** ${state.current_focus}`);
|
|
255
|
+
hasContent = true;
|
|
256
|
+
}
|
|
257
|
+
if (state.working_branch) {
|
|
258
|
+
lines.push(`**Branch:** \`${state.working_branch}\``);
|
|
259
|
+
hasContent = true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const questions = state.open_questions || [];
|
|
263
|
+
if (questions.length) {
|
|
264
|
+
lines.push('');
|
|
265
|
+
lines.push('**Open Questions:**');
|
|
266
|
+
for (const q of questions) {
|
|
267
|
+
lines.push(`- ${typeof q === 'string' ? q : q.question}`);
|
|
268
|
+
}
|
|
269
|
+
hasContent = true;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const decisions = state.decisions_made || [];
|
|
273
|
+
if (decisions.length) {
|
|
274
|
+
lines.push('');
|
|
275
|
+
lines.push('**Decisions Made (do not contradict these):**');
|
|
276
|
+
for (const d of decisions) {
|
|
277
|
+
lines.push(`- ${typeof d === 'string' ? d : d.decision}`);
|
|
278
|
+
}
|
|
279
|
+
hasContent = true;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const notes = state.session_notes || [];
|
|
283
|
+
if (notes.length) {
|
|
284
|
+
lines.push('');
|
|
285
|
+
lines.push('**Session Notes:**');
|
|
286
|
+
for (const n of notes.slice(0, 5)) {
|
|
287
|
+
lines.push(`- ${typeof n === 'string' ? n : n.note}`);
|
|
288
|
+
}
|
|
289
|
+
hasContent = true;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return hasContent ? lines.join('\n') : '';
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function buildNextSteps(state) {
|
|
296
|
+
const steps = state.next_steps || [];
|
|
297
|
+
const issues = state.known_issues || [];
|
|
298
|
+
if (!steps.length && !issues.length) return '';
|
|
299
|
+
|
|
300
|
+
const lines = [sectionHeader('🚀 What To Do Next')];
|
|
301
|
+
|
|
302
|
+
if (issues.length) {
|
|
303
|
+
lines.push('**Known issues to resolve:**');
|
|
304
|
+
for (const i of issues) lines.push(`- ⚠️ ${i}`);
|
|
305
|
+
lines.push('');
|
|
306
|
+
}
|
|
307
|
+
if (steps.length) {
|
|
308
|
+
lines.push('**Recommended next steps:**');
|
|
309
|
+
for (const s of steps) lines.push(`- ${s}`);
|
|
310
|
+
}
|
|
311
|
+
return lines.join('\n');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function buildInstructions() {
|
|
315
|
+
return [
|
|
316
|
+
sectionHeader('📋 Instructions For The AI Reading This'),
|
|
317
|
+
'1. Read **Active Errors** first — fix those before anything else.',
|
|
318
|
+
'2. Read **Resolved Issues** before writing any fix — avoid re-introducing old bugs.',
|
|
319
|
+
'3. Read **Recent Code Changes** to understand what just changed and why.',
|
|
320
|
+
'4. Read **Code Catalogue** to know what each file exports before calling anything.',
|
|
321
|
+
'5. Read **API Routes** before adding or changing any endpoint.',
|
|
322
|
+
'6. Read **Dependencies** before suggesting `npm install` — the package may already exist.',
|
|
323
|
+
'7. Read **Working Context** to understand the current focus and locked-in decisions.',
|
|
324
|
+
'8. Read **What To Do Next** for prioritised next actions.',
|
|
325
|
+
'',
|
|
326
|
+
'> This file is auto-generated by aibridge-context on every startup and file change.',
|
|
327
|
+
'> Do not edit it manually — changes will be overwritten.',
|
|
328
|
+
'> The full machine-readable state is in `.ai-context/state.json`.',
|
|
329
|
+
].join('\n');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ─────────────────────────────────────────────────────────────────
|
|
333
|
+
// Main generator
|
|
334
|
+
// ─────────────────────────────────────────────────────────────────
|
|
335
|
+
|
|
336
|
+
function generateBriefing(state, projectRoot) {
|
|
337
|
+
const now = new Date().toISOString();
|
|
338
|
+
const projName = state.project || path.basename(projectRoot || process.cwd());
|
|
339
|
+
|
|
340
|
+
const sections = [
|
|
341
|
+
`# AI Briefing — ${projName}`,
|
|
342
|
+
'',
|
|
343
|
+
`> **Auto-generated by aibridge-context** | Last updated: ${now}`,
|
|
344
|
+
`> Paste this file into any AI assistant to immediately brief it on the full project state.`,
|
|
345
|
+
'',
|
|
346
|
+
buildInstructions(),
|
|
347
|
+
buildOverview(state),
|
|
348
|
+
buildActiveErrors(state),
|
|
349
|
+
buildResolvedIssues(state),
|
|
350
|
+
buildNextSteps(state),
|
|
351
|
+
buildWorkingContext(state),
|
|
352
|
+
buildRecentChanges(state),
|
|
353
|
+
buildFileStructure(state),
|
|
354
|
+
buildCodeCatalogue(state),
|
|
355
|
+
buildApiRoutes(state),
|
|
356
|
+
buildDependencies(state),
|
|
357
|
+
buildEnvVariables(state),
|
|
358
|
+
buildArchitecture(state),
|
|
359
|
+
buildKeyFeatures(state),
|
|
360
|
+
'',
|
|
361
|
+
'---',
|
|
362
|
+
`_Generated by [aibridge-context](https://github.com/npm/aibridge-context) • ${now}_`,
|
|
363
|
+
];
|
|
364
|
+
|
|
365
|
+
return sections.filter((s) => s !== null && s !== undefined).join('\n');
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
module.exports = { generateBriefing };
|
package/core/codeDiff.js
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* codeDiff.js
|
|
5
|
+
* -----------
|
|
6
|
+
* Pure-JS line-level diff engine. No external deps.
|
|
7
|
+
* Produces a unified-diff-style patch and a human summary
|
|
8
|
+
* of what exactly changed in a file between two snapshots.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const MAX_DIFF_LINES = 120; // cap stored diff size
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Myers diff – returns array of { op: 'eq'|'add'|'del', line }
|
|
15
|
+
*/
|
|
16
|
+
function myersDiff(oldLines, newLines) {
|
|
17
|
+
const N = oldLines.length;
|
|
18
|
+
const M = newLines.length;
|
|
19
|
+
const MAX = N + M;
|
|
20
|
+
if (MAX === 0) return [];
|
|
21
|
+
|
|
22
|
+
const v = new Array(2 * MAX + 1).fill(0);
|
|
23
|
+
const trail = [];
|
|
24
|
+
|
|
25
|
+
for (let d = 0; d <= MAX; d++) {
|
|
26
|
+
const snapshot = v.slice();
|
|
27
|
+
trail.push(snapshot);
|
|
28
|
+
|
|
29
|
+
for (let k = -d; k <= d; k += 2) {
|
|
30
|
+
let x;
|
|
31
|
+
if (k === -d || (k !== d && v[k - 1 + MAX] < v[k + 1 + MAX])) {
|
|
32
|
+
x = v[k + 1 + MAX];
|
|
33
|
+
} else {
|
|
34
|
+
x = v[k - 1 + MAX] + 1;
|
|
35
|
+
}
|
|
36
|
+
let y = x - k;
|
|
37
|
+
while (x < N && y < M && oldLines[x] === newLines[y]) { x++; y++; }
|
|
38
|
+
v[k + MAX] = x;
|
|
39
|
+
if (x >= N && y >= M) {
|
|
40
|
+
return buildOps(trail, d, oldLines, newLines, MAX);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return buildOps(trail, MAX, oldLines, newLines, MAX);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function buildOps(trail, d, oldLines, newLines, MAX) {
|
|
48
|
+
const ops = [];
|
|
49
|
+
let x = oldLines.length;
|
|
50
|
+
let y = newLines.length;
|
|
51
|
+
|
|
52
|
+
for (let step = d; step > 0; step--) {
|
|
53
|
+
const v = trail[step];
|
|
54
|
+
const k = x - y;
|
|
55
|
+
const prevK = (k === -step || (k !== step && v[k - 1 + MAX] < v[k + 1 + MAX]))
|
|
56
|
+
? k + 1 : k - 1;
|
|
57
|
+
const prevX = v[prevK + MAX];
|
|
58
|
+
const prevY = prevX - prevK;
|
|
59
|
+
|
|
60
|
+
while (x > prevX + (x - prevX - (y - prevY)) && y > prevY + (y - prevY - (x - prevX))) {
|
|
61
|
+
ops.unshift({ op: 'eq', line: oldLines[x - 1] });
|
|
62
|
+
x--; y--;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (step > 0) {
|
|
66
|
+
if (x === prevX) {
|
|
67
|
+
ops.unshift({ op: 'add', line: newLines[y - 1] });
|
|
68
|
+
y--;
|
|
69
|
+
} else {
|
|
70
|
+
ops.unshift({ op: 'del', line: oldLines[x - 1] });
|
|
71
|
+
x--;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
while (x > 0) { ops.unshift({ op: 'eq', line: oldLines[x - 1] }); x--; }
|
|
77
|
+
return ops;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Produce a compact unified diff string from old/new text.
|
|
82
|
+
* Returns { patch, linesAdded, linesRemoved, summary }
|
|
83
|
+
*/
|
|
84
|
+
function diffTexts(oldText, newText, filePath) {
|
|
85
|
+
const oldLines = (oldText || '').split('\n');
|
|
86
|
+
const newLines = (newText || '').split('\n');
|
|
87
|
+
|
|
88
|
+
let linesAdded = 0;
|
|
89
|
+
let linesRemoved = 0;
|
|
90
|
+
const patchLines = [`--- ${filePath}`, `+++ ${filePath}`];
|
|
91
|
+
|
|
92
|
+
// Use simpler LCS approach for large files to stay fast
|
|
93
|
+
if (oldLines.length + newLines.length > 2000) {
|
|
94
|
+
// Fast path: just count added/removed, no full diff
|
|
95
|
+
const oldSet = new Set(oldLines);
|
|
96
|
+
const newSet = new Set(newLines);
|
|
97
|
+
linesAdded = newLines.filter((l) => !oldSet.has(l)).length;
|
|
98
|
+
linesRemoved = oldLines.filter((l) => !newSet.has(l)).length;
|
|
99
|
+
return {
|
|
100
|
+
patch: `[diff too large – ${linesAdded} lines added, ${linesRemoved} lines removed]`,
|
|
101
|
+
linesAdded,
|
|
102
|
+
linesRemoved,
|
|
103
|
+
summary: buildSummary(linesAdded, linesRemoved, filePath)
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const ops = myersDiff(oldLines, newLines);
|
|
108
|
+
const CONTEXT = 3;
|
|
109
|
+
const hunks = [];
|
|
110
|
+
let hunk = null;
|
|
111
|
+
let oldLine = 1;
|
|
112
|
+
let newLine = 1;
|
|
113
|
+
let pending = [];
|
|
114
|
+
|
|
115
|
+
for (let i = 0; i < ops.length; i++) {
|
|
116
|
+
const op = ops[i];
|
|
117
|
+
|
|
118
|
+
if (op.op === 'eq') {
|
|
119
|
+
if (hunk) {
|
|
120
|
+
pending.push({ op, oldLine, newLine });
|
|
121
|
+
if (pending.length > CONTEXT * 2) {
|
|
122
|
+
// Flush context
|
|
123
|
+
for (let j = 0; j < CONTEXT; j++) hunk.lines.push(` ${pending[j].op.line}`);
|
|
124
|
+
hunks.push(hunk);
|
|
125
|
+
hunk = null;
|
|
126
|
+
pending = pending.slice(pending.length - CONTEXT);
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
pending.push({ op, oldLine, newLine });
|
|
130
|
+
if (pending.length > CONTEXT) pending.shift();
|
|
131
|
+
}
|
|
132
|
+
oldLine++; newLine++;
|
|
133
|
+
} else {
|
|
134
|
+
if (!hunk) {
|
|
135
|
+
const startOld = Math.max(1, oldLine - pending.length);
|
|
136
|
+
const startNew = Math.max(1, newLine - pending.length);
|
|
137
|
+
hunk = { startOld, startNew, oldCount: 0, newCount: 0, lines: [] };
|
|
138
|
+
for (const p of pending) hunk.lines.push(` ${p.op.line}`);
|
|
139
|
+
pending = [];
|
|
140
|
+
}
|
|
141
|
+
if (op.op === 'add') {
|
|
142
|
+
hunk.lines.push(`+${op.line}`);
|
|
143
|
+
hunk.newCount++;
|
|
144
|
+
linesAdded++;
|
|
145
|
+
newLine++;
|
|
146
|
+
} else {
|
|
147
|
+
hunk.lines.push(`-${op.line}`);
|
|
148
|
+
hunk.oldCount++;
|
|
149
|
+
linesRemoved++;
|
|
150
|
+
oldLine++;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (hunk) {
|
|
156
|
+
for (let j = 0; j < Math.min(CONTEXT, pending.length); j++) {
|
|
157
|
+
hunk.lines.push(` ${pending[j].op.line}`);
|
|
158
|
+
}
|
|
159
|
+
hunks.push(hunk);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let totalLines = 0;
|
|
163
|
+
for (const h of hunks) {
|
|
164
|
+
const oldCount = (h.lines.filter((l) => l[0] !== '+').length);
|
|
165
|
+
const newCount = (h.lines.filter((l) => l[0] !== '-').length);
|
|
166
|
+
patchLines.push(`@@ -${h.startOld},${oldCount} +${h.startNew},${newCount} @@`);
|
|
167
|
+
for (const line of h.lines) {
|
|
168
|
+
patchLines.push(line);
|
|
169
|
+
totalLines++;
|
|
170
|
+
if (totalLines >= MAX_DIFF_LINES) {
|
|
171
|
+
patchLines.push(`[... diff truncated at ${MAX_DIFF_LINES} lines ...]`);
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (totalLines >= MAX_DIFF_LINES) break;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
patch: patchLines.join('\n'),
|
|
180
|
+
linesAdded,
|
|
181
|
+
linesRemoved,
|
|
182
|
+
summary: buildSummary(linesAdded, linesRemoved, filePath)
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function buildSummary(added, removed, filePath) {
|
|
187
|
+
const parts = [];
|
|
188
|
+
if (added > 0) parts.push(`+${added} line${added !== 1 ? 's' : ''}`);
|
|
189
|
+
if (removed > 0) parts.push(`-${removed} line${removed !== 1 ? 's' : ''}`);
|
|
190
|
+
if (parts.length === 0) return `No line changes in ${filePath}`;
|
|
191
|
+
return `${filePath}: ${parts.join(', ')}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Extract meaningful code-level signals from a diff patch.
|
|
196
|
+
* Returns array of human-readable strings like:
|
|
197
|
+
* "Added function buildFileTree"
|
|
198
|
+
* "Removed call to syncContextToGit"
|
|
199
|
+
* "Modified exports block"
|
|
200
|
+
*/
|
|
201
|
+
function extractCodeSignals(patch, filePath) {
|
|
202
|
+
if (!patch || typeof patch !== 'string') return [];
|
|
203
|
+
const signals = [];
|
|
204
|
+
const ext = (filePath || '').split('.').pop().toLowerCase();
|
|
205
|
+
|
|
206
|
+
if (['js', 'ts', 'mjs', 'cjs'].includes(ext)) {
|
|
207
|
+
extractJsSignals(patch, signals);
|
|
208
|
+
} else if (ext === 'json') {
|
|
209
|
+
extractJsonSignals(patch, signals, filePath);
|
|
210
|
+
} else if (ext === 'md') {
|
|
211
|
+
signals.push('Documentation updated');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return uniqueSignals(signals);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function extractJsSignals(patch, signals) {
|
|
218
|
+
const addedLines = patch.split('\n').filter((l) => l.startsWith('+')).map((l) => l.slice(1));
|
|
219
|
+
const removedLines = patch.split('\n').filter((l) => l.startsWith('-')).map((l) => l.slice(1));
|
|
220
|
+
|
|
221
|
+
// Functions added/removed
|
|
222
|
+
const fnPattern = /\bfunction\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/;
|
|
223
|
+
const arrowFn = /\b(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*(?:async\s*)?\(/;
|
|
224
|
+
const methodFn = /\b([A-Za-z_$][A-Za-z0-9_$]*)\s*\(.*\)\s*\{/;
|
|
225
|
+
|
|
226
|
+
for (const line of addedLines) {
|
|
227
|
+
const m = fnPattern.exec(line) || arrowFn.exec(line);
|
|
228
|
+
if (m) signals.push(`Added function \`${m[1]}\``);
|
|
229
|
+
}
|
|
230
|
+
for (const line of removedLines) {
|
|
231
|
+
const m = fnPattern.exec(line) || arrowFn.exec(line);
|
|
232
|
+
if (m) signals.push(`Removed function \`${m[1]}\``);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// require/import changes
|
|
236
|
+
const reqPattern = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/;
|
|
237
|
+
for (const line of addedLines) {
|
|
238
|
+
const m = reqPattern.exec(line);
|
|
239
|
+
if (m) signals.push(`Added dependency on \`${m[1]}\``);
|
|
240
|
+
}
|
|
241
|
+
for (const line of removedLines) {
|
|
242
|
+
const m = reqPattern.exec(line);
|
|
243
|
+
if (m) signals.push(`Removed dependency on \`${m[1]}\``);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// exports changes
|
|
247
|
+
const exportsAdded = addedLines.some((l) => /module\.exports|exports\./.test(l));
|
|
248
|
+
const exportsRemoved = removedLines.some((l) => /module\.exports|exports\./.test(l));
|
|
249
|
+
if (exportsAdded || exportsRemoved) signals.push('Modified module exports');
|
|
250
|
+
|
|
251
|
+
// Const/let additions
|
|
252
|
+
const constPattern = /\b(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=/;
|
|
253
|
+
for (const line of addedLines.slice(0, 20)) {
|
|
254
|
+
const m = constPattern.exec(line);
|
|
255
|
+
if (m && !signals.some((s) => s.includes(m[1]))) {
|
|
256
|
+
signals.push(`Added \`${m[1]}\``);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Error handling
|
|
261
|
+
if (addedLines.some((l) => /try\s*\{|catch\s*\(/.test(l))) signals.push('Added error handling');
|
|
262
|
+
if (addedLines.some((l) => /throw\s+new/.test(l))) signals.push('Added throw statement');
|
|
263
|
+
|
|
264
|
+
// Async/await
|
|
265
|
+
if (addedLines.some((l) => /\basync\b/.test(l))) signals.push('Added async logic');
|
|
266
|
+
|
|
267
|
+
// Route definitions
|
|
268
|
+
const routePattern = /\b(?:router|app)\.(get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/;
|
|
269
|
+
for (const line of addedLines) {
|
|
270
|
+
const m = routePattern.exec(line);
|
|
271
|
+
if (m) signals.push(`Added route ${m[1].toUpperCase()} ${m[2]}`);
|
|
272
|
+
}
|
|
273
|
+
for (const line of removedLines) {
|
|
274
|
+
const m = routePattern.exec(line);
|
|
275
|
+
if (m) signals.push(`Removed route ${m[1].toUpperCase()} ${m[2]}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function extractJsonSignals(patch, signals, filePath) {
|
|
280
|
+
if (filePath && filePath.includes('package.json')) {
|
|
281
|
+
const addedLines = patch.split('\n').filter((l) => l.startsWith('+')).map((l) => l.slice(1));
|
|
282
|
+
const removedLines = patch.split('\n').filter((l) => l.startsWith('-')).map((l) => l.slice(1));
|
|
283
|
+
const pkgPattern = /"([a-z@][a-z0-9@/_-]*)"\s*:/;
|
|
284
|
+
for (const line of addedLines) {
|
|
285
|
+
const m = pkgPattern.exec(line);
|
|
286
|
+
if (m && !m[1].startsWith('name') && !m[1].startsWith('version')) {
|
|
287
|
+
signals.push(`Added package \`${m[1]}\``);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
for (const line of removedLines) {
|
|
291
|
+
const m = pkgPattern.exec(line);
|
|
292
|
+
if (m) signals.push(`Removed package \`${m[1]}\``);
|
|
293
|
+
}
|
|
294
|
+
if (addedLines.some((l) => /"version"/.test(l))) signals.push('Version bumped');
|
|
295
|
+
} else {
|
|
296
|
+
signals.push('Config/data file updated');
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function uniqueSignals(signals) {
|
|
301
|
+
return Array.from(new Set(signals)).slice(0, 10);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
module.exports = { diffTexts, extractCodeSignals };
|