@vibecheck-ai/mcp 24.6.6 → 24.6.7
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/dist/index.js +1695 -1695
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -18048,1850 +18048,1850 @@ rules:
|
|
|
18048
18048
|
target:
|
|
18049
18049
|
match: "src/repositories/**"
|
|
18050
18050
|
`;
|
|
18051
|
-
|
|
18051
|
+
|
|
18052
|
+
// ../context-engine/dist/chunk-HMKLYBWJ.js
|
|
18053
|
+
var import_better_sqlite3 = __toESM(require_lib(), 1);
|
|
18054
|
+
var import_fast_glob2 = __toESM(require_out4(), 1);
|
|
18055
|
+
var SCHEMA_VERSION = 1;
|
|
18056
|
+
var PersistentIndex = class {
|
|
18057
|
+
db;
|
|
18058
|
+
config;
|
|
18052
18059
|
rootPath;
|
|
18053
|
-
|
|
18054
|
-
|
|
18055
|
-
|
|
18056
|
-
|
|
18057
|
-
|
|
18058
|
-
|
|
18059
|
-
|
|
18060
|
-
|
|
18061
|
-
|
|
18062
|
-
|
|
18063
|
-
|
|
18064
|
-
|
|
18065
|
-
|
|
18066
|
-
|
|
18067
|
-
|
|
18068
|
-
|
|
18069
|
-
|
|
18070
|
-
|
|
18071
|
-
|
|
18072
|
-
|
|
18073
|
-
|
|
18074
|
-
const verificationSteps = this.buildVerificationSteps();
|
|
18075
|
-
const riskBriefing = this.buildRiskBriefing();
|
|
18076
|
-
return {
|
|
18077
|
-
version: "2.0.0",
|
|
18078
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
18079
|
-
projectIdentity,
|
|
18080
|
-
architecturalRules: archRules,
|
|
18081
|
-
activeViolations,
|
|
18082
|
-
codebaseDNA,
|
|
18083
|
-
fileContexts,
|
|
18084
|
-
taskPlaybooks,
|
|
18085
|
-
verificationSteps,
|
|
18086
|
-
riskBriefing
|
|
18087
|
-
};
|
|
18088
|
-
}
|
|
18089
|
-
/**
|
|
18090
|
-
* Generate context for a specific file — what an AI agent needs to know
|
|
18091
|
-
* before editing this file.
|
|
18092
|
-
*/
|
|
18093
|
-
synthesizeForFile(filePath) {
|
|
18094
|
-
const rel = this.toRelative(filePath);
|
|
18095
|
-
const file = this.data.files.find((f) => f.relativePath === rel || f.path === filePath);
|
|
18096
|
-
if (!file) return null;
|
|
18097
|
-
return this.buildSingleFileContext(file);
|
|
18098
|
-
}
|
|
18099
|
-
/**
|
|
18100
|
-
* Generate a compact markdown context document for IDE rules.
|
|
18101
|
-
* This replaces the old static rule generation with intelligence-driven context.
|
|
18102
|
-
*/
|
|
18103
|
-
generateContextDocument() {
|
|
18104
|
-
const ctx = this.synthesize();
|
|
18105
|
-
const lines = [];
|
|
18106
|
-
lines.push(`# ${ctx.projectIdentity.name} \u2014 AI Context`);
|
|
18107
|
-
lines.push(`<!-- Generated by @repo/context-engine at ${ctx.generatedAt} -->`);
|
|
18108
|
-
lines.push("");
|
|
18109
|
-
lines.push("## Project Identity");
|
|
18110
|
-
lines.push(`- **Stack**: ${ctx.projectIdentity.stack}`);
|
|
18111
|
-
lines.push(`- **Architecture**: ${ctx.projectIdentity.architecture}`);
|
|
18112
|
-
if (ctx.projectIdentity.keyPatterns.length > 0) {
|
|
18113
|
-
lines.push(`- **Key Patterns**: ${ctx.projectIdentity.keyPatterns.join(", ")}`);
|
|
18114
|
-
}
|
|
18115
|
-
lines.push("");
|
|
18116
|
-
if (ctx.codebaseDNA.conventions.length > 0) {
|
|
18117
|
-
lines.push("## Conventions (Auto-Discovered)");
|
|
18118
|
-
for (const conv of ctx.codebaseDNA.conventions) {
|
|
18119
|
-
lines.push(`- ${conv}`);
|
|
18120
|
-
}
|
|
18121
|
-
lines.push("");
|
|
18122
|
-
}
|
|
18123
|
-
if (ctx.architecturalRules.length > 0) {
|
|
18124
|
-
lines.push("## Architecture Rules");
|
|
18125
|
-
for (const rule of ctx.architecturalRules) {
|
|
18126
|
-
const icon = rule.severity === "error" ? "MUST" : rule.severity === "warning" ? "SHOULD" : "MAY";
|
|
18127
|
-
lines.push(`- **[${icon}]** ${rule.name}: ${rule.description}`);
|
|
18128
|
-
if (rule.violationCount > 0) {
|
|
18129
|
-
lines.push(` - ${rule.violationCount} active violations`);
|
|
18130
|
-
}
|
|
18131
|
-
}
|
|
18132
|
-
lines.push("");
|
|
18133
|
-
}
|
|
18134
|
-
if (ctx.codebaseDNA.boundaries.length > 0) {
|
|
18135
|
-
lines.push("## Module Boundaries");
|
|
18136
|
-
for (const boundary of ctx.codebaseDNA.boundaries) {
|
|
18137
|
-
lines.push(`- ${boundary}`);
|
|
18138
|
-
}
|
|
18139
|
-
lines.push("");
|
|
18140
|
-
}
|
|
18141
|
-
if (ctx.codebaseDNA.hotFiles.length > 0) {
|
|
18142
|
-
lines.push("## High-Impact Files (Edit With Care)");
|
|
18143
|
-
for (const file of ctx.codebaseDNA.hotFiles.slice(0, 10)) {
|
|
18144
|
-
lines.push(`- \`${file}\``);
|
|
18145
|
-
}
|
|
18146
|
-
lines.push("");
|
|
18147
|
-
}
|
|
18148
|
-
if (ctx.riskBriefing.securityConcerns.length > 0 || ctx.riskBriefing.testGaps.length > 0) {
|
|
18149
|
-
lines.push("## Risk Areas");
|
|
18150
|
-
for (const concern of ctx.riskBriefing.securityConcerns) {
|
|
18151
|
-
lines.push(`- ${concern}`);
|
|
18152
|
-
}
|
|
18153
|
-
for (const gap of ctx.riskBriefing.testGaps.slice(0, 5)) {
|
|
18154
|
-
lines.push(`- ${gap}`);
|
|
18155
|
-
}
|
|
18156
|
-
lines.push("");
|
|
18157
|
-
}
|
|
18158
|
-
if (ctx.projectIdentity.noGoZones.length > 0) {
|
|
18159
|
-
lines.push("## No-Go Zones");
|
|
18160
|
-
for (const zone of ctx.projectIdentity.noGoZones) {
|
|
18161
|
-
lines.push(`- ${zone}`);
|
|
18162
|
-
}
|
|
18163
|
-
lines.push("");
|
|
18164
|
-
}
|
|
18165
|
-
if (ctx.taskPlaybooks.length > 0) {
|
|
18166
|
-
lines.push("## Task Playbooks");
|
|
18167
|
-
for (const playbook of ctx.taskPlaybooks) {
|
|
18168
|
-
lines.push(`### ${playbook.taskType}`);
|
|
18169
|
-
for (const step of playbook.steps) {
|
|
18170
|
-
lines.push(`1. ${step}`);
|
|
18171
|
-
}
|
|
18172
|
-
if (playbook.mustVerify.length > 0) {
|
|
18173
|
-
lines.push(`**Verify**: ${playbook.mustVerify.join(", ")}`);
|
|
18174
|
-
}
|
|
18175
|
-
lines.push("");
|
|
18176
|
-
}
|
|
18177
|
-
}
|
|
18178
|
-
if (ctx.verificationSteps.length > 0) {
|
|
18179
|
-
lines.push("## Verification Protocol");
|
|
18180
|
-
for (const step of ctx.verificationSteps) {
|
|
18181
|
-
lines.push(`### On ${step.trigger}`);
|
|
18182
|
-
for (const check of step.checks) {
|
|
18183
|
-
lines.push(`- ${check}`);
|
|
18184
|
-
}
|
|
18185
|
-
if (step.commands.length > 0) {
|
|
18186
|
-
lines.push(`**Run**: \`${step.commands.join(" && ")}\``);
|
|
18187
|
-
}
|
|
18188
|
-
lines.push("");
|
|
18189
|
-
}
|
|
18190
|
-
}
|
|
18191
|
-
lines.push("## Codebase Health");
|
|
18192
|
-
lines.push(`- **Overall**: ${this.dna.healthScore.overall}/100`);
|
|
18193
|
-
const dims = this.dna.healthScore.dimensions;
|
|
18194
|
-
lines.push(`- Architecture: ${dims.architecture} | Tests: ${dims.testCoverage} | Conventions: ${dims.conventions} | Dependencies: ${dims.dependencies}`);
|
|
18195
|
-
lines.push("");
|
|
18196
|
-
lines.push("---");
|
|
18197
|
-
lines.push("<!-- context-engine:v2 -->");
|
|
18198
|
-
return lines.join("\n");
|
|
18199
|
-
}
|
|
18200
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
18201
|
-
// IDENTITY
|
|
18202
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
18203
|
-
buildProjectIdentity() {
|
|
18204
|
-
const fp = this.dna.fingerprint;
|
|
18205
|
-
const stack = [fp.framework, fp.language, fp.orm, fp.validator, fp.authLib, fp.router].filter(Boolean).join(" | ");
|
|
18206
|
-
const keyPatterns = this.dna.patterns.map((p) => p.name);
|
|
18207
|
-
const criticalPaths = this.dna.hotspots.slice(0, 5).map((h) => h.file);
|
|
18208
|
-
const noGoZones = [];
|
|
18209
|
-
if (this.ruleResult) {
|
|
18210
|
-
const errorRules = this.ruleResult.violations.filter((v) => v.severity === "error");
|
|
18211
|
-
const uniqueMessages = [...new Set(errorRules.map((v) => v.message))];
|
|
18212
|
-
noGoZones.push(...uniqueMessages.slice(0, 5));
|
|
18213
|
-
}
|
|
18214
|
-
for (const cycle of this.graph.cycles) {
|
|
18215
|
-
noGoZones.push(`Circular dependency: ${cycle.nodes.slice(0, 3).join(" \u2192 ")}...`);
|
|
18216
|
-
}
|
|
18217
|
-
const architecture = this.dna.conventions.filter((c) => c.area === "structure").map((c) => c.description).join("; ") || fp.framework;
|
|
18218
|
-
return {
|
|
18219
|
-
name: fp.name,
|
|
18220
|
-
stack,
|
|
18221
|
-
architecture,
|
|
18222
|
-
keyPatterns,
|
|
18223
|
-
criticalPaths,
|
|
18224
|
-
noGoZones: noGoZones.slice(0, 10)
|
|
18225
|
-
};
|
|
18226
|
-
}
|
|
18227
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
18228
|
-
// RULES SUMMARY
|
|
18229
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
18230
|
-
buildArchRuleSummary() {
|
|
18231
|
-
if (!this.ruleResult) return [];
|
|
18232
|
-
const breakdown = this.ruleResult.ruleBreakdown;
|
|
18233
|
-
return Object.entries(breakdown).map(([ruleId, count]) => {
|
|
18234
|
-
const violation = this.ruleResult.violations.find((v) => v.ruleId === ruleId);
|
|
18235
|
-
return {
|
|
18236
|
-
id: ruleId,
|
|
18237
|
-
name: violation?.ruleName || ruleId,
|
|
18238
|
-
type: "import_forbidden",
|
|
18239
|
-
severity: violation?.severity || "warning",
|
|
18240
|
-
scope: violation?.sourceSymbol.filePath || "",
|
|
18241
|
-
description: violation?.message || "",
|
|
18242
|
-
violationCount: count
|
|
18243
|
-
};
|
|
18244
|
-
});
|
|
18245
|
-
}
|
|
18246
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
18247
|
-
// DNA SUMMARY
|
|
18248
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
18249
|
-
buildDNASummary() {
|
|
18250
|
-
return {
|
|
18251
|
-
conventions: this.dna.conventions.filter((c) => c.confidence > 0.5).map((c) => c.description),
|
|
18252
|
-
patterns: this.dna.patterns.map((p) => `${p.name}: ${p.description}`),
|
|
18253
|
-
boundaries: this.dna.boundaries.filter((b) => b.importCount > 3).map((b) => `${b.from} \u2192 ${b.to} (${b.importCount} imports${b.isCircular ? ", CIRCULAR" : ""})`),
|
|
18254
|
-
hotFiles: this.dna.hotspots.slice(0, 10).map((h) => h.file),
|
|
18255
|
-
riskAreas: this.dna.riskMap.filter((r) => r.riskLevel === "critical" || r.riskLevel === "high").map((r) => `${r.file}: ${r.factors[0]}`)
|
|
18060
|
+
constructor(config) {
|
|
18061
|
+
this.rootPath = config.rootPath;
|
|
18062
|
+
this.config = {
|
|
18063
|
+
rootPath: config.rootPath,
|
|
18064
|
+
dbPath: config.dbPath ?? join(config.rootPath, ".vibecheck", "index.db"),
|
|
18065
|
+
includePatterns: config.includePatterns ?? ["**/*.{ts,tsx,js,jsx,py,rs,go,java,c,cpp,h,hpp,rb,swift,kt,lua,zig}"],
|
|
18066
|
+
excludePatterns: config.excludePatterns ?? [
|
|
18067
|
+
"**/node_modules/**",
|
|
18068
|
+
"**/dist/**",
|
|
18069
|
+
"**/build/**",
|
|
18070
|
+
"**/.next/**",
|
|
18071
|
+
"**/.git/**",
|
|
18072
|
+
"**/coverage/**",
|
|
18073
|
+
"**/.turbo/**",
|
|
18074
|
+
"**/__pycache__/**",
|
|
18075
|
+
"**/target/**",
|
|
18076
|
+
"**/.mcp_data/**"
|
|
18077
|
+
],
|
|
18078
|
+
maxFileSize: config.maxFileSize ?? 5e5,
|
|
18079
|
+
maxFiles: config.maxFiles ?? 1e4,
|
|
18080
|
+
storeContent: config.storeContent ?? true
|
|
18256
18081
|
};
|
|
18082
|
+
const dbDir = join(this.config.dbPath, "..");
|
|
18083
|
+
mkdirSync(dbDir, { recursive: true });
|
|
18084
|
+
this.db = new import_better_sqlite3.default(this.config.dbPath);
|
|
18085
|
+
this.db.pragma("journal_mode = WAL");
|
|
18086
|
+
this.db.pragma("synchronous = NORMAL");
|
|
18087
|
+
this.db.pragma("cache_size = -64000");
|
|
18088
|
+
this.initSchema();
|
|
18257
18089
|
}
|
|
18258
18090
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
18259
|
-
//
|
|
18091
|
+
// SCHEMA
|
|
18260
18092
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
18261
|
-
|
|
18262
|
-
const
|
|
18263
|
-
|
|
18264
|
-
|
|
18265
|
-
|
|
18266
|
-
|
|
18267
|
-
|
|
18268
|
-
|
|
18269
|
-
|
|
18270
|
-
|
|
18271
|
-
|
|
18272
|
-
|
|
18273
|
-
|
|
18274
|
-
|
|
18275
|
-
|
|
18276
|
-
|
|
18277
|
-
|
|
18278
|
-
|
|
18279
|
-
|
|
18280
|
-
|
|
18281
|
-
|
|
18282
|
-
|
|
18283
|
-
|
|
18284
|
-
|
|
18285
|
-
|
|
18286
|
-
|
|
18287
|
-
|
|
18288
|
-
|
|
18289
|
-
|
|
18290
|
-
|
|
18291
|
-
|
|
18292
|
-
|
|
18293
|
-
|
|
18294
|
-
|
|
18295
|
-
|
|
18296
|
-
|
|
18297
|
-
|
|
18298
|
-
|
|
18299
|
-
|
|
18300
|
-
|
|
18301
|
-
|
|
18302
|
-
|
|
18303
|
-
|
|
18304
|
-
|
|
18305
|
-
|
|
18306
|
-
|
|
18307
|
-
|
|
18308
|
-
|
|
18309
|
-
|
|
18310
|
-
|
|
18311
|
-
|
|
18312
|
-
|
|
18313
|
-
|
|
18314
|
-
|
|
18315
|
-
|
|
18316
|
-
|
|
18317
|
-
|
|
18318
|
-
|
|
18319
|
-
|
|
18320
|
-
|
|
18321
|
-
|
|
18322
|
-
|
|
18323
|
-
|
|
18324
|
-
|
|
18325
|
-
|
|
18326
|
-
|
|
18327
|
-
|
|
18328
|
-
|
|
18329
|
-
|
|
18330
|
-
|
|
18331
|
-
|
|
18332
|
-
|
|
18333
|
-
|
|
18334
|
-
|
|
18335
|
-
|
|
18336
|
-
|
|
18337
|
-
|
|
18338
|
-
|
|
18339
|
-
|
|
18340
|
-
|
|
18341
|
-
|
|
18342
|
-
|
|
18343
|
-
|
|
18344
|
-
|
|
18345
|
-
|
|
18346
|
-
|
|
18347
|
-
|
|
18348
|
-
|
|
18349
|
-
|
|
18350
|
-
|
|
18351
|
-
|
|
18352
|
-
|
|
18353
|
-
|
|
18354
|
-
|
|
18355
|
-
|
|
18356
|
-
|
|
18357
|
-
|
|
18358
|
-
|
|
18359
|
-
|
|
18360
|
-
|
|
18361
|
-
|
|
18362
|
-
|
|
18363
|
-
|
|
18364
|
-
|
|
18365
|
-
|
|
18366
|
-
|
|
18367
|
-
|
|
18368
|
-
|
|
18369
|
-
|
|
18370
|
-
|
|
18371
|
-
|
|
18372
|
-
|
|
18373
|
-
|
|
18374
|
-
if (layer) guidance.push(`This is in the ${layer} layer \u2014 only import from lower layers.`);
|
|
18375
|
-
break;
|
|
18376
|
-
case "repository":
|
|
18377
|
-
guidance.push("Only data access logic belongs here \u2014 no business rules.");
|
|
18378
|
-
guidance.push("Return domain objects, not raw database rows.");
|
|
18379
|
-
break;
|
|
18380
|
-
case "component":
|
|
18381
|
-
guidance.push("Keep components focused and composable.");
|
|
18382
|
-
guidance.push("Extract complex logic to custom hooks.");
|
|
18383
|
-
break;
|
|
18384
|
-
case "middleware":
|
|
18385
|
-
guidance.push("Middleware must call next() or return a response \u2014 never leave the request hanging.");
|
|
18386
|
-
guidance.push("Keep middleware focused on a single concern.");
|
|
18387
|
-
break;
|
|
18388
|
-
case "test":
|
|
18389
|
-
guidance.push("Follow Arrange-Act-Assert pattern.");
|
|
18390
|
-
guidance.push("Test edge cases and error conditions, not just happy path.");
|
|
18391
|
-
break;
|
|
18392
|
-
}
|
|
18393
|
-
for (const conv of conventions.slice(0, 3)) {
|
|
18394
|
-
guidance.push(`Convention: ${conv}`);
|
|
18395
|
-
}
|
|
18396
|
-
return guidance;
|
|
18397
|
-
}
|
|
18398
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
18399
|
-
// TASK PLAYBOOKS
|
|
18400
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
18401
|
-
buildTaskPlaybooks() {
|
|
18402
|
-
const fp = this.dna.fingerprint;
|
|
18403
|
-
const playbooks = [];
|
|
18404
|
-
playbooks.push({
|
|
18405
|
-
taskType: "Bug Fix",
|
|
18406
|
-
steps: [
|
|
18407
|
-
"Reproduce the bug and understand the expected vs actual behavior",
|
|
18408
|
-
"Identify the root cause file(s) using the dependency graph",
|
|
18409
|
-
"Write a failing test that reproduces the bug",
|
|
18410
|
-
"Apply the minimal fix at the root cause",
|
|
18411
|
-
"Verify the fix passes the test and does not break existing tests",
|
|
18412
|
-
"Check that the fix does not violate any architecture rules"
|
|
18413
|
-
],
|
|
18414
|
-
mustRead: this.dna.hotspots.slice(0, 3).map((h) => h.file),
|
|
18415
|
-
mustUpdate: ["The buggy file", "Related test file"],
|
|
18416
|
-
mustVerify: ["All existing tests pass", "New regression test passes", "No new arch rule violations"],
|
|
18417
|
-
stopConditions: ["Never modify tests to make them pass \u2014 fix the code", "Do not change public API signatures without discussion"]
|
|
18418
|
-
});
|
|
18419
|
-
if (fp.router) {
|
|
18420
|
-
playbooks.push({
|
|
18421
|
-
taskType: "Add API Endpoint",
|
|
18422
|
-
steps: [
|
|
18423
|
-
"Check existing routes in truthpack to avoid duplicates",
|
|
18424
|
-
"Create the route handler following existing patterns",
|
|
18425
|
-
"Add input validation using the project validator",
|
|
18426
|
-
"Add authentication middleware if the route is protected",
|
|
18427
|
-
"Write tests for success, validation failure, and auth failure",
|
|
18428
|
-
"Update the truthpack (run vibecheck scan)"
|
|
18429
|
-
],
|
|
18430
|
-
mustRead: ["truthpack/routes.json", ...this.dna.patterns.filter((p) => p.category === "api").map((p) => p.exemplar)],
|
|
18431
|
-
mustUpdate: ["Route file", "Test file", "Truthpack"],
|
|
18432
|
-
mustVerify: ["Route responds correctly", "Input validation works", "Auth is enforced", "Test passes"],
|
|
18433
|
-
stopConditions: ["Do not create duplicate routes", "Do not hardcode mock data in handlers"]
|
|
18434
|
-
});
|
|
18435
|
-
}
|
|
18436
|
-
if (fp.framework.includes("Next") || fp.framework.includes("React")) {
|
|
18437
|
-
playbooks.push({
|
|
18438
|
-
taskType: "Add UI Component",
|
|
18439
|
-
steps: [
|
|
18440
|
-
"Check if a similar component already exists",
|
|
18441
|
-
"Create the component following existing naming and structure patterns",
|
|
18442
|
-
"Add TypeScript props interface",
|
|
18443
|
-
"Add unit test for the component",
|
|
18444
|
-
"If using state, determine if it should be a client component"
|
|
18445
|
-
],
|
|
18446
|
-
mustRead: this.dna.patterns.filter((p) => p.category === "ui" || p.category === "state").map((p) => p.exemplar),
|
|
18447
|
-
mustUpdate: ["Component file", "Test file", "Parent component that uses it"],
|
|
18448
|
-
mustVerify: ["Component renders correctly", "Props are typed", "Test passes"],
|
|
18449
|
-
stopConditions: ['Do not use "any" type for props', 'Do not add useState in server components without "use client"']
|
|
18450
|
-
});
|
|
18451
|
-
}
|
|
18452
|
-
playbooks.push({
|
|
18453
|
-
taskType: "Refactor",
|
|
18454
|
-
steps: [
|
|
18455
|
-
"Identify all callers/dependents of the code being refactored",
|
|
18456
|
-
"Ensure comprehensive tests exist before refactoring",
|
|
18457
|
-
"Apply changes incrementally, testing after each step",
|
|
18458
|
-
"Update all dependents to use the new API",
|
|
18459
|
-
"Remove old code only after all dependents are migrated",
|
|
18460
|
-
"Verify no architecture rules are violated"
|
|
18461
|
-
],
|
|
18462
|
-
mustRead: ["Dependency graph for affected files"],
|
|
18463
|
-
mustUpdate: ["Refactored file", "All dependent files", "Tests"],
|
|
18464
|
-
mustVerify: ["All tests pass", "No new violations", "No regressions"],
|
|
18465
|
-
stopConditions: ["Never break existing public APIs without migration path", "Do not refactor and add features in the same change"]
|
|
18466
|
-
});
|
|
18467
|
-
return playbooks;
|
|
18468
|
-
}
|
|
18469
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
18470
|
-
// VERIFICATION STEPS
|
|
18471
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
18472
|
-
buildVerificationSteps() {
|
|
18473
|
-
const fp = this.dna.fingerprint;
|
|
18474
|
-
const steps = [];
|
|
18475
|
-
if (fp.language === "TypeScript") {
|
|
18476
|
-
steps.push({
|
|
18477
|
-
trigger: "Any TypeScript file change",
|
|
18478
|
-
checks: ["TypeScript compilation succeeds", "No new type errors introduced"],
|
|
18479
|
-
commands: [fp.packageManager === "pnpm" ? "pnpm run check-types" : "npm run check-types"],
|
|
18480
|
-
artifacts: []
|
|
18481
|
-
});
|
|
18482
|
-
}
|
|
18483
|
-
if (fp.testRunner) {
|
|
18484
|
-
steps.push({
|
|
18485
|
-
trigger: "Any source file change",
|
|
18486
|
-
checks: ["Related tests pass", "No test regressions"],
|
|
18487
|
-
commands: [`${fp.packageManager} run test`],
|
|
18488
|
-
artifacts: ["test-results.json"]
|
|
18489
|
-
});
|
|
18490
|
-
}
|
|
18491
|
-
if (fp.router) {
|
|
18492
|
-
steps.push({
|
|
18493
|
-
trigger: "Route handler added or modified",
|
|
18494
|
-
checks: ["Route responds with correct status", "Auth middleware is applied", "Input validation works"],
|
|
18495
|
-
commands: ["vibecheck scan"],
|
|
18496
|
-
artifacts: ["truthpack/routes.json"]
|
|
18497
|
-
});
|
|
18498
|
-
}
|
|
18499
|
-
steps.push({
|
|
18500
|
-
trigger: "Any source file change",
|
|
18501
|
-
checks: ["No new architecture rule violations", "No new circular dependencies"],
|
|
18502
|
-
commands: ["vibecheck arch-rules"],
|
|
18503
|
-
artifacts: []
|
|
18504
|
-
});
|
|
18505
|
-
return steps;
|
|
18093
|
+
initSchema() {
|
|
18094
|
+
const version = this.getSchemaVersion();
|
|
18095
|
+
if (version === SCHEMA_VERSION) return;
|
|
18096
|
+
this.db.exec(`
|
|
18097
|
+
DROP TABLE IF EXISTS file_hashes;
|
|
18098
|
+
DROP TABLE IF EXISTS files;
|
|
18099
|
+
DROP TABLE IF EXISTS symbols;
|
|
18100
|
+
DROP TABLE IF EXISTS imports;
|
|
18101
|
+
DROP TABLE IF EXISTS call_edges;
|
|
18102
|
+
DROP TABLE IF EXISTS routes;
|
|
18103
|
+
DROP TABLE IF EXISTS services;
|
|
18104
|
+
DROP TABLE IF EXISTS embeddings;
|
|
18105
|
+
DROP TABLE IF EXISTS meta;
|
|
18106
|
+
|
|
18107
|
+
CREATE TABLE meta (
|
|
18108
|
+
key TEXT PRIMARY KEY,
|
|
18109
|
+
value TEXT NOT NULL
|
|
18110
|
+
);
|
|
18111
|
+
|
|
18112
|
+
CREATE TABLE file_hashes (
|
|
18113
|
+
relative_path TEXT PRIMARY KEY,
|
|
18114
|
+
content_hash TEXT NOT NULL,
|
|
18115
|
+
size_bytes INTEGER NOT NULL,
|
|
18116
|
+
modified_ms INTEGER NOT NULL,
|
|
18117
|
+
indexed_at INTEGER NOT NULL DEFAULT (unixepoch('now'))
|
|
18118
|
+
);
|
|
18119
|
+
|
|
18120
|
+
CREATE TABLE files (
|
|
18121
|
+
id TEXT PRIMARY KEY,
|
|
18122
|
+
path TEXT NOT NULL,
|
|
18123
|
+
relative_path TEXT NOT NULL UNIQUE,
|
|
18124
|
+
language TEXT NOT NULL,
|
|
18125
|
+
line_count INTEGER NOT NULL,
|
|
18126
|
+
exports TEXT NOT NULL DEFAULT '[]',
|
|
18127
|
+
content TEXT
|
|
18128
|
+
);
|
|
18129
|
+
|
|
18130
|
+
CREATE TABLE symbols (
|
|
18131
|
+
id TEXT PRIMARY KEY,
|
|
18132
|
+
name TEXT NOT NULL,
|
|
18133
|
+
kind TEXT NOT NULL,
|
|
18134
|
+
file_path TEXT NOT NULL,
|
|
18135
|
+
start_line INTEGER NOT NULL,
|
|
18136
|
+
end_line INTEGER NOT NULL,
|
|
18137
|
+
exported INTEGER NOT NULL DEFAULT 0,
|
|
18138
|
+
async INTEGER NOT NULL DEFAULT 0,
|
|
18139
|
+
params INTEGER,
|
|
18140
|
+
branches INTEGER,
|
|
18141
|
+
signature TEXT
|
|
18142
|
+
);
|
|
18143
|
+
|
|
18144
|
+
CREATE TABLE imports (
|
|
18145
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
18146
|
+
file_id TEXT NOT NULL,
|
|
18147
|
+
file_path TEXT NOT NULL,
|
|
18148
|
+
source_path TEXT NOT NULL,
|
|
18149
|
+
resolved_path TEXT NOT NULL DEFAULT '',
|
|
18150
|
+
imported_symbols TEXT NOT NULL DEFAULT '[]',
|
|
18151
|
+
is_type_only INTEGER NOT NULL DEFAULT 0,
|
|
18152
|
+
is_dynamic INTEGER NOT NULL DEFAULT 0,
|
|
18153
|
+
line INTEGER NOT NULL DEFAULT 0
|
|
18154
|
+
);
|
|
18155
|
+
|
|
18156
|
+
CREATE TABLE call_edges (
|
|
18157
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
18158
|
+
caller_id TEXT NOT NULL,
|
|
18159
|
+
callee_id TEXT NOT NULL,
|
|
18160
|
+
caller_name TEXT NOT NULL,
|
|
18161
|
+
callee_name TEXT NOT NULL,
|
|
18162
|
+
caller_file TEXT NOT NULL,
|
|
18163
|
+
callee_file TEXT NOT NULL
|
|
18164
|
+
);
|
|
18165
|
+
|
|
18166
|
+
CREATE TABLE routes (
|
|
18167
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
18168
|
+
path TEXT NOT NULL,
|
|
18169
|
+
method TEXT NOT NULL,
|
|
18170
|
+
handler TEXT NOT NULL,
|
|
18171
|
+
file TEXT NOT NULL,
|
|
18172
|
+
line INTEGER NOT NULL DEFAULT 0,
|
|
18173
|
+
middleware TEXT NOT NULL DEFAULT '[]',
|
|
18174
|
+
auth INTEGER
|
|
18175
|
+
);
|
|
18176
|
+
|
|
18177
|
+
CREATE TABLE services (
|
|
18178
|
+
id TEXT PRIMARY KEY,
|
|
18179
|
+
name TEXT NOT NULL,
|
|
18180
|
+
root_path TEXT NOT NULL DEFAULT ''
|
|
18181
|
+
);
|
|
18182
|
+
|
|
18183
|
+
CREATE TABLE embeddings (
|
|
18184
|
+
path TEXT NOT NULL,
|
|
18185
|
+
chunk_id TEXT NOT NULL,
|
|
18186
|
+
chunk_type TEXT NOT NULL DEFAULT 'file',
|
|
18187
|
+
content_hash TEXT NOT NULL,
|
|
18188
|
+
vector BLOB NOT NULL,
|
|
18189
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
18190
|
+
PRIMARY KEY (path, chunk_id)
|
|
18191
|
+
);
|
|
18192
|
+
|
|
18193
|
+
-- Indexes for fast lookups
|
|
18194
|
+
CREATE INDEX idx_symbols_file ON symbols(file_path);
|
|
18195
|
+
CREATE INDEX idx_symbols_name ON symbols(name);
|
|
18196
|
+
CREATE INDEX idx_symbols_kind ON symbols(kind);
|
|
18197
|
+
CREATE INDEX idx_imports_file ON imports(file_path);
|
|
18198
|
+
CREATE INDEX idx_imports_source ON imports(source_path);
|
|
18199
|
+
CREATE INDEX idx_imports_resolved ON imports(resolved_path);
|
|
18200
|
+
CREATE INDEX idx_call_edges_caller ON call_edges(caller_file);
|
|
18201
|
+
CREATE INDEX idx_call_edges_callee ON call_edges(callee_file);
|
|
18202
|
+
CREATE INDEX idx_embeddings_type ON embeddings(chunk_type);
|
|
18203
|
+
`);
|
|
18204
|
+
this.setMeta("schema_version", String(SCHEMA_VERSION));
|
|
18205
|
+
this.setMeta("created_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
18506
18206
|
}
|
|
18507
|
-
|
|
18508
|
-
|
|
18509
|
-
|
|
18510
|
-
|
|
18511
|
-
|
|
18512
|
-
|
|
18513
|
-
const securityConcerns = [];
|
|
18514
|
-
for (const risk of this.dna.riskMap) {
|
|
18515
|
-
if (risk.riskLevel === "critical") {
|
|
18516
|
-
securityConcerns.push(`${risk.file}: ${risk.factors.join(", ")}`);
|
|
18517
|
-
}
|
|
18518
|
-
}
|
|
18519
|
-
const testGaps = [];
|
|
18520
|
-
const sourceFiles = this.data.files.filter(
|
|
18521
|
-
(f) => !f.relativePath.includes(".test.") && !f.relativePath.includes(".spec.") && (f.relativePath.endsWith(".ts") || f.relativePath.endsWith(".tsx")) && !f.relativePath.includes("config") && !f.relativePath.includes(".d.ts")
|
|
18522
|
-
);
|
|
18523
|
-
const testFiles = this.data.files.filter(
|
|
18524
|
-
(f) => f.relativePath.includes(".test.") || f.relativePath.includes(".spec.")
|
|
18525
|
-
);
|
|
18526
|
-
const testedBases = new Set(testFiles.map(
|
|
18527
|
-
(f) => path2.basename(f.relativePath).replace(/\.(test|spec)\.(ts|tsx|js|jsx)$/, "")
|
|
18528
|
-
));
|
|
18529
|
-
for (const file of sourceFiles) {
|
|
18530
|
-
const baseName = path2.basename(file.relativePath).replace(/\.(ts|tsx|js|jsx)$/, "");
|
|
18531
|
-
if (!testedBases.has(baseName) && file.exports.length > 0) {
|
|
18532
|
-
testGaps.push(`${file.relativePath} has exports but no test file`);
|
|
18533
|
-
}
|
|
18534
|
-
}
|
|
18535
|
-
const driftWarnings = [];
|
|
18536
|
-
for (const cycle of this.graph.cycles) {
|
|
18537
|
-
driftWarnings.push(`Circular dependency: ${cycle.nodes.slice(0, 3).join(" \u2192 ")}${cycle.nodes.length > 3 ? "..." : ""}`);
|
|
18207
|
+
getSchemaVersion() {
|
|
18208
|
+
try {
|
|
18209
|
+
const row = this.db.prepare("SELECT value FROM meta WHERE key = ?").get("schema_version");
|
|
18210
|
+
return row ? Number.parseInt(row.value, 10) : 0;
|
|
18211
|
+
} catch {
|
|
18212
|
+
return 0;
|
|
18538
18213
|
}
|
|
18539
|
-
|
|
18540
|
-
|
|
18541
|
-
|
|
18542
|
-
|
|
18543
|
-
|
|
18544
|
-
|
|
18545
|
-
|
|
18214
|
+
}
|
|
18215
|
+
setMeta(key, value) {
|
|
18216
|
+
this.db.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)").run(key, value);
|
|
18217
|
+
}
|
|
18218
|
+
getMeta(key) {
|
|
18219
|
+
const row = this.db.prepare("SELECT value FROM meta WHERE key = ?").get(key);
|
|
18220
|
+
return row?.value ?? null;
|
|
18546
18221
|
}
|
|
18547
18222
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
18548
|
-
//
|
|
18223
|
+
// INCREMENTAL INDEXING
|
|
18549
18224
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
18550
|
-
toRelative(filePath) {
|
|
18551
|
-
if (filePath.startsWith(this.rootPath)) {
|
|
18552
|
-
return filePath.slice(this.rootPath.length + 1).replace(/\\/g, "/");
|
|
18553
|
-
}
|
|
18554
|
-
return filePath.replace(/\\/g, "/");
|
|
18555
|
-
}
|
|
18556
|
-
};
|
|
18557
|
-
var ContextExplainer = class {
|
|
18558
18225
|
/**
|
|
18559
|
-
*
|
|
18226
|
+
* Discover files, diff against stored hashes, return only changed files.
|
|
18560
18227
|
*/
|
|
18561
|
-
|
|
18562
|
-
const
|
|
18563
|
-
const
|
|
18564
|
-
const
|
|
18565
|
-
const
|
|
18566
|
-
const
|
|
18567
|
-
const
|
|
18568
|
-
|
|
18569
|
-
|
|
18570
|
-
|
|
18571
|
-
|
|
18572
|
-
|
|
18573
|
-
|
|
18574
|
-
const parts = [];
|
|
18575
|
-
if (depCount > 0) {
|
|
18576
|
-
const critical = depCount > 10 ? " \u2014 changes here have wide blast radius" : "";
|
|
18577
|
-
parts.push(`${depCount} file${depCount > 1 ? "s" : ""} depend on this${critical}`);
|
|
18578
|
-
}
|
|
18579
|
-
if (depsOnCount > 0) {
|
|
18580
|
-
parts.push(`it imports from ${depsOnCount} file${depsOnCount > 1 ? "s" : ""}`);
|
|
18581
|
-
}
|
|
18582
|
-
paragraphs.push({
|
|
18583
|
-
heading: "Dependencies",
|
|
18584
|
-
text: parts.join(". ") + ".",
|
|
18585
|
-
importance: depCount > 10 ? "critical" : depCount > 5 ? "high" : "medium"
|
|
18586
|
-
});
|
|
18587
|
-
}
|
|
18588
|
-
quickFacts.push({ label: "Role", value: role, icon: "layer" });
|
|
18589
|
-
if (fc.layer) quickFacts.push({ label: "Layer", value: fc.layer, icon: "layer" });
|
|
18590
|
-
quickFacts.push({ label: "Dependents", value: String(depCount), icon: "dependency" });
|
|
18591
|
-
quickFacts.push({ label: "Dependencies", value: String(depsOnCount), icon: "dependency" });
|
|
18592
|
-
if (fc.riskLevel === "critical" || fc.riskLevel === "high") {
|
|
18593
|
-
quickFacts.push({ label: "Risk", value: fc.riskLevel.toUpperCase(), icon: "warning" });
|
|
18594
|
-
warnings.push({
|
|
18595
|
-
message: `This file is classified as ${fc.riskLevel} risk`,
|
|
18596
|
-
severity: fc.riskLevel === "critical" ? "error" : "warning",
|
|
18597
|
-
action: "Add comprehensive tests and review carefully before merging changes"
|
|
18598
|
-
});
|
|
18599
|
-
}
|
|
18600
|
-
for (const dep of fc.dependedOnBy.slice(0, 5)) {
|
|
18601
|
-
relatedFiles.push({
|
|
18602
|
-
filePath: dep,
|
|
18603
|
-
reason: `Imports from this file`,
|
|
18604
|
-
relationship: "depended-by",
|
|
18605
|
-
confidence: 0.9
|
|
18606
|
-
});
|
|
18607
|
-
}
|
|
18608
|
-
overlays.push({
|
|
18609
|
-
type: "code-lens",
|
|
18610
|
-
line: 1,
|
|
18611
|
-
text: `${depCount} dependent${depCount !== 1 ? "s" : ""} \xB7 ${depsOnCount} import${depsOnCount !== 1 ? "s" : ""} \xB7 ${role}`,
|
|
18612
|
-
tooltip: `This ${role} file has ${depCount} files that depend on it and imports from ${depsOnCount} files`
|
|
18613
|
-
});
|
|
18614
|
-
}
|
|
18615
|
-
if (fc && fc.conventions.length > 0) {
|
|
18616
|
-
paragraphs.push({
|
|
18617
|
-
heading: "Conventions",
|
|
18618
|
-
text: `Follow these discovered conventions: ${fc.conventions.slice(0, 3).join("; ")}.`,
|
|
18619
|
-
importance: "medium"
|
|
18620
|
-
});
|
|
18621
|
-
}
|
|
18622
|
-
if (ctx.rules) {
|
|
18623
|
-
const fileViolations = ctx.rules.violations.filter(
|
|
18624
|
-
(v) => v.sourceSymbol.filePath.includes(shortName(filePath)) || v.targetSymbol && v.targetSymbol.filePath.includes(shortName(filePath))
|
|
18625
|
-
);
|
|
18626
|
-
if (fileViolations.length > 0) {
|
|
18627
|
-
const errors = fileViolations.filter((v) => v.severity === "error");
|
|
18628
|
-
const warns = fileViolations.filter((v) => v.severity === "warning");
|
|
18629
|
-
paragraphs.push({
|
|
18630
|
-
heading: "Architecture Violations",
|
|
18631
|
-
text: `${errors.length} error${errors.length !== 1 ? "s" : ""} and ${warns.length} warning${warns.length !== 1 ? "s" : ""} from architecture rules.`,
|
|
18632
|
-
importance: errors.length > 0 ? "critical" : "high"
|
|
18633
|
-
});
|
|
18634
|
-
for (const v of fileViolations.slice(0, 5)) {
|
|
18635
|
-
warnings.push({
|
|
18636
|
-
message: v.message,
|
|
18637
|
-
severity: v.severity === "error" ? "error" : "warning",
|
|
18638
|
-
action: v.suggestedFix ?? "Review and fix the violation",
|
|
18639
|
-
ruleId: v.ruleId
|
|
18640
|
-
});
|
|
18641
|
-
overlays.push({
|
|
18642
|
-
type: "diagnostic",
|
|
18643
|
-
line: v.sourceSymbol.line,
|
|
18644
|
-
text: v.message,
|
|
18645
|
-
severity: v.severity === "error" ? "error" : "warning",
|
|
18646
|
-
tooltip: v.suggestedFix
|
|
18647
|
-
});
|
|
18648
|
-
}
|
|
18649
|
-
}
|
|
18650
|
-
}
|
|
18651
|
-
if (ctx.callGraph) {
|
|
18652
|
-
const fileNodes = ctx.callGraph.nodes.filter((n) => n.filePath.includes(shortName(filePath)));
|
|
18653
|
-
const hotFunctions = fileNodes.filter((n) => n.callerCount > 5);
|
|
18654
|
-
if (hotFunctions.length > 0) {
|
|
18655
|
-
paragraphs.push({
|
|
18656
|
-
heading: "Hot Functions",
|
|
18657
|
-
text: hotFunctions.map(
|
|
18658
|
-
(f) => `\`${f.name}\` is called by ${f.callerCount} function${f.callerCount !== 1 ? "s" : ""}${f.calleeCount > 0 ? ` and calls ${f.calleeCount}` : ""}`
|
|
18659
|
-
).join(". ") + ".",
|
|
18660
|
-
importance: "high"
|
|
18661
|
-
});
|
|
18662
|
-
for (const fn of hotFunctions) {
|
|
18663
|
-
overlays.push({
|
|
18664
|
-
type: "code-lens",
|
|
18665
|
-
line: 0,
|
|
18666
|
-
// Would need symbol line mapping
|
|
18667
|
-
text: `${fn.callerCount} callers \xB7 ${fn.calleeCount} callees`,
|
|
18668
|
-
tooltip: `Function ${fn.name} has ${fn.callerCount} callers and ${fn.calleeCount} callees`
|
|
18669
|
-
});
|
|
18670
|
-
}
|
|
18671
|
-
}
|
|
18672
|
-
const deadInFile = ctx.callGraph.stats.deadFunctions.filter(
|
|
18673
|
-
(d) => d.filePath.includes(shortName(filePath))
|
|
18674
|
-
);
|
|
18675
|
-
if (deadInFile.length > 0) {
|
|
18676
|
-
for (const dead of deadInFile) {
|
|
18677
|
-
warnings.push({
|
|
18678
|
-
message: `\`${dead.name}\` appears to be dead code (exported but never called)`,
|
|
18679
|
-
severity: "info",
|
|
18680
|
-
action: "Verify this function is not called via dynamic dispatch or external consumers, then consider removing it"
|
|
18681
|
-
});
|
|
18682
|
-
}
|
|
18683
|
-
}
|
|
18684
|
-
}
|
|
18685
|
-
if (ctx.temporal) {
|
|
18686
|
-
const hotspot = ctx.temporal.changeHotspots.find((h) => filePath.includes(h.file) || h.file.includes(shortName(filePath)));
|
|
18687
|
-
if (hotspot) {
|
|
18688
|
-
const daysSince = Math.round((Date.now() - new Date(hotspot.lastChanged).getTime()) / 864e5);
|
|
18689
|
-
paragraphs.push({
|
|
18690
|
-
heading: "Recent Activity",
|
|
18691
|
-
text: `Changed ${hotspot.commits} times in the last ${ctx.temporal.stats.analysisWindowDays} days by ${hotspot.authors} author${hotspot.authors !== 1 ? "s" : ""}. Last modified ${daysSince} day${daysSince !== 1 ? "s" : ""} ago.`,
|
|
18692
|
-
importance: hotspot.commits > 10 ? "high" : "medium"
|
|
18693
|
-
});
|
|
18694
|
-
quickFacts.push({ label: "Recent commits", value: String(hotspot.commits), icon: "git" });
|
|
18695
|
-
quickFacts.push({ label: "Last changed", value: `${daysSince}d ago`, icon: "git" });
|
|
18696
|
-
quickFacts.push({ label: "Authors", value: String(hotspot.authors), icon: "git" });
|
|
18697
|
-
}
|
|
18698
|
-
const churn = ctx.temporal.churnFiles.find((c) => filePath.includes(c.file) || c.file.includes(shortName(filePath)));
|
|
18699
|
-
if (churn && churn.severity !== "low") {
|
|
18700
|
-
warnings.push({
|
|
18701
|
-
message: churn.reason,
|
|
18702
|
-
severity: churn.severity === "high" ? "warning" : "info",
|
|
18703
|
-
action: "Consider whether this file needs refactoring to reduce change frequency"
|
|
18704
|
-
});
|
|
18705
|
-
}
|
|
18706
|
-
const expertise = ctx.temporal.authorExpertise.find(
|
|
18707
|
-
(e) => filePath.startsWith(e.area) || filePath.includes(e.area)
|
|
18708
|
-
);
|
|
18709
|
-
if (expertise && expertise.busFactor === 1) {
|
|
18710
|
-
warnings.push({
|
|
18711
|
-
message: `Bus factor of 1 \u2014 ${expertise.primaryAuthor} has made ${expertise.authors[0]?.percentage}% of changes to this area`,
|
|
18712
|
-
severity: "info",
|
|
18713
|
-
action: "Consider knowledge sharing or pair programming for this area"
|
|
18714
|
-
});
|
|
18228
|
+
async diffFiles() {
|
|
18229
|
+
const discoveredFiles = await this.discoverFiles();
|
|
18230
|
+
const storedHashes = this.getStoredHashes();
|
|
18231
|
+
const changed = [];
|
|
18232
|
+
const unchanged = [];
|
|
18233
|
+
const currentPaths = /* @__PURE__ */ new Set();
|
|
18234
|
+
for (const file of discoveredFiles) {
|
|
18235
|
+
currentPaths.add(file.relativePath);
|
|
18236
|
+
const stored = storedHashes.get(file.relativePath);
|
|
18237
|
+
if (!stored || stored.contentHash !== file.contentHash) {
|
|
18238
|
+
changed.push(file.relativePath);
|
|
18239
|
+
} else {
|
|
18240
|
+
unchanged.push(file.relativePath);
|
|
18715
18241
|
}
|
|
18716
18242
|
}
|
|
18717
|
-
|
|
18718
|
-
|
|
18719
|
-
if (
|
|
18720
|
-
|
|
18721
|
-
const other = pair.files[0].includes(shortName(filePath)) ? pair.files[1] : pair.files[0];
|
|
18722
|
-
relatedFiles.push({
|
|
18723
|
-
filePath: other,
|
|
18724
|
-
reason: `Often edited together (${pair.count} times)`,
|
|
18725
|
-
relationship: "co-edited",
|
|
18726
|
-
confidence: pair.weight
|
|
18727
|
-
});
|
|
18728
|
-
}
|
|
18243
|
+
const deleted = [];
|
|
18244
|
+
for (const storedPath of storedHashes.keys()) {
|
|
18245
|
+
if (!currentPaths.has(storedPath)) {
|
|
18246
|
+
deleted.push(storedPath);
|
|
18729
18247
|
}
|
|
18730
18248
|
}
|
|
18731
|
-
|
|
18732
|
-
|
|
18733
|
-
|
|
18734
|
-
|
|
18735
|
-
|
|
18736
|
-
|
|
18249
|
+
return { changed, deleted, unchanged };
|
|
18250
|
+
}
|
|
18251
|
+
/**
|
|
18252
|
+
* Full reindex — scan all files and store data.
|
|
18253
|
+
* Returns parsed CodebaseData + stats.
|
|
18254
|
+
*/
|
|
18255
|
+
async reindex(parser4) {
|
|
18256
|
+
const startMs = Date.now();
|
|
18257
|
+
const { changed, deleted, unchanged } = await this.diffFiles();
|
|
18258
|
+
if (deleted.length > 0) {
|
|
18259
|
+
this.removeFiles(deleted);
|
|
18260
|
+
}
|
|
18261
|
+
const parsedFiles = [];
|
|
18262
|
+
for (const relPath of changed) {
|
|
18263
|
+
const absPath = join(this.rootPath, relPath);
|
|
18264
|
+
try {
|
|
18265
|
+
const parsed = await parser4.parseFile(absPath, relPath);
|
|
18266
|
+
parsedFiles.push(parsed);
|
|
18267
|
+
} catch {
|
|
18268
|
+
}
|
|
18269
|
+
}
|
|
18270
|
+
this.storeFiles(parsedFiles);
|
|
18271
|
+
const data = this.loadCodebaseData();
|
|
18272
|
+
const stats = {
|
|
18273
|
+
totalFiles: data.files.length,
|
|
18274
|
+
totalSymbols: data.symbols.length,
|
|
18275
|
+
totalImports: data.imports.length,
|
|
18276
|
+
indexedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
18277
|
+
reindexedFiles: changed.length,
|
|
18278
|
+
skippedFiles: unchanged.length,
|
|
18279
|
+
durationMs: Date.now() - startMs
|
|
18280
|
+
};
|
|
18281
|
+
this.setMeta("last_index_at", stats.indexedAt);
|
|
18282
|
+
this.setMeta("last_index_stats", JSON.stringify(stats));
|
|
18283
|
+
return { data, stats };
|
|
18284
|
+
}
|
|
18285
|
+
/**
|
|
18286
|
+
* Incremental update — only reindex specific files.
|
|
18287
|
+
*/
|
|
18288
|
+
async reindexFiles(relativePaths, parser4) {
|
|
18289
|
+
const startMs = Date.now();
|
|
18290
|
+
this.removeFiles(relativePaths);
|
|
18291
|
+
const parsedFiles = [];
|
|
18292
|
+
for (const relPath of relativePaths) {
|
|
18293
|
+
const absPath = join(this.rootPath, relPath);
|
|
18294
|
+
try {
|
|
18295
|
+
const parsed = await parser4.parseFile(absPath, relPath);
|
|
18296
|
+
parsedFiles.push(parsed);
|
|
18297
|
+
} catch {
|
|
18298
|
+
}
|
|
18737
18299
|
}
|
|
18300
|
+
this.storeFiles(parsedFiles);
|
|
18301
|
+
const totalFiles = this.db.prepare("SELECT COUNT(*) as cnt FROM files").get();
|
|
18302
|
+
const totalSymbols = this.db.prepare("SELECT COUNT(*) as cnt FROM symbols").get();
|
|
18303
|
+
const totalImports = this.db.prepare("SELECT COUNT(*) as cnt FROM imports").get();
|
|
18738
18304
|
return {
|
|
18739
|
-
|
|
18740
|
-
|
|
18741
|
-
|
|
18742
|
-
|
|
18743
|
-
|
|
18744
|
-
|
|
18745
|
-
|
|
18305
|
+
totalFiles: totalFiles.cnt,
|
|
18306
|
+
totalSymbols: totalSymbols.cnt,
|
|
18307
|
+
totalImports: totalImports.cnt,
|
|
18308
|
+
indexedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
18309
|
+
reindexedFiles: parsedFiles.length,
|
|
18310
|
+
skippedFiles: 0,
|
|
18311
|
+
durationMs: Date.now() - startMs
|
|
18746
18312
|
};
|
|
18747
18313
|
}
|
|
18314
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
18315
|
+
// DATA LOADING (warm start)
|
|
18316
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
18317
|
+
/**
|
|
18318
|
+
* Load full CodebaseData from the persistent store.
|
|
18319
|
+
* This is the warm-start path — sub-second for indexed repos.
|
|
18320
|
+
*/
|
|
18321
|
+
loadCodebaseData() {
|
|
18322
|
+
const files = this.loadFiles();
|
|
18323
|
+
const symbols = this.loadSymbols();
|
|
18324
|
+
const imports = this.loadImports();
|
|
18325
|
+
const callEdges = this.loadCallEdges();
|
|
18326
|
+
const routes = this.loadRoutes();
|
|
18327
|
+
const services = this.loadServices();
|
|
18328
|
+
return { files, symbols, imports, callEdges, routes, services };
|
|
18329
|
+
}
|
|
18748
18330
|
/**
|
|
18749
|
-
*
|
|
18331
|
+
* Check if the index exists and has data.
|
|
18750
18332
|
*/
|
|
18751
|
-
|
|
18752
|
-
|
|
18753
|
-
|
|
18754
|
-
|
|
18755
|
-
|
|
18756
|
-
|
|
18757
|
-
lines.push(explanation.quickFacts.map((f) => `**${f.label}**: ${f.value}`).join(" \xB7 "));
|
|
18758
|
-
lines.push("");
|
|
18333
|
+
isPopulated() {
|
|
18334
|
+
try {
|
|
18335
|
+
const row = this.db.prepare("SELECT COUNT(*) as cnt FROM files").get();
|
|
18336
|
+
return row.cnt > 0;
|
|
18337
|
+
} catch {
|
|
18338
|
+
return false;
|
|
18759
18339
|
}
|
|
18760
|
-
|
|
18761
|
-
|
|
18762
|
-
|
|
18763
|
-
|
|
18340
|
+
}
|
|
18341
|
+
/**
|
|
18342
|
+
* Get the last index timestamp.
|
|
18343
|
+
*/
|
|
18344
|
+
getLastIndexedAt() {
|
|
18345
|
+
return this.getMeta("last_index_at");
|
|
18346
|
+
}
|
|
18347
|
+
/**
|
|
18348
|
+
* Get the last index stats.
|
|
18349
|
+
*/
|
|
18350
|
+
getLastIndexStats() {
|
|
18351
|
+
const raw = this.getMeta("last_index_stats");
|
|
18352
|
+
if (!raw) return null;
|
|
18353
|
+
try {
|
|
18354
|
+
return JSON.parse(raw);
|
|
18355
|
+
} catch {
|
|
18356
|
+
return null;
|
|
18764
18357
|
}
|
|
18765
|
-
|
|
18766
|
-
|
|
18767
|
-
|
|
18768
|
-
|
|
18769
|
-
|
|
18358
|
+
}
|
|
18359
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
18360
|
+
// EMBEDDING STORAGE
|
|
18361
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
18362
|
+
/**
|
|
18363
|
+
* Store a chunk embedding (file-level, function-level, etc.)
|
|
18364
|
+
*/
|
|
18365
|
+
storeEmbedding(path10, chunkId, chunkType, contentHash, vector, metadata) {
|
|
18366
|
+
const vectorBuf = Buffer.from(new Float32Array(vector).buffer);
|
|
18367
|
+
this.db.prepare(`
|
|
18368
|
+
INSERT OR REPLACE INTO embeddings (path, chunk_id, chunk_type, content_hash, vector, metadata)
|
|
18369
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
18370
|
+
`).run(path10, chunkId, chunkType, contentHash, vectorBuf, JSON.stringify(metadata ?? {}));
|
|
18371
|
+
}
|
|
18372
|
+
/**
|
|
18373
|
+
* Load embedding for a specific chunk.
|
|
18374
|
+
*/
|
|
18375
|
+
loadEmbedding(path10, chunkId) {
|
|
18376
|
+
const row = this.db.prepare("SELECT vector, content_hash, metadata FROM embeddings WHERE path = ? AND chunk_id = ?").get(path10, chunkId);
|
|
18377
|
+
if (!row) return null;
|
|
18378
|
+
return {
|
|
18379
|
+
vector: Array.from(new Float32Array(row.vector.buffer, row.vector.byteOffset, row.vector.byteLength / 4)),
|
|
18380
|
+
contentHash: row.content_hash,
|
|
18381
|
+
metadata: JSON.parse(row.metadata)
|
|
18382
|
+
};
|
|
18383
|
+
}
|
|
18384
|
+
/**
|
|
18385
|
+
* Load all embeddings of a given type for vector search.
|
|
18386
|
+
*/
|
|
18387
|
+
loadEmbeddingsByType(chunkType) {
|
|
18388
|
+
const rows = this.db.prepare("SELECT path, chunk_id, vector, content_hash, metadata FROM embeddings WHERE chunk_type = ?").all(chunkType);
|
|
18389
|
+
return rows.map((row) => ({
|
|
18390
|
+
path: row.path,
|
|
18391
|
+
chunkId: row.chunk_id,
|
|
18392
|
+
vector: Array.from(new Float32Array(row.vector.buffer, row.vector.byteOffset, row.vector.byteLength / 4)),
|
|
18393
|
+
contentHash: row.content_hash,
|
|
18394
|
+
metadata: JSON.parse(row.metadata)
|
|
18395
|
+
}));
|
|
18396
|
+
}
|
|
18397
|
+
/**
|
|
18398
|
+
* Remove stale embeddings for files no longer in the index.
|
|
18399
|
+
*/
|
|
18400
|
+
pruneStaleEmbeddings() {
|
|
18401
|
+
const result = this.db.prepare(`
|
|
18402
|
+
DELETE FROM embeddings WHERE path NOT IN (SELECT relative_path FROM files)
|
|
18403
|
+
`).run();
|
|
18404
|
+
return result.changes;
|
|
18405
|
+
}
|
|
18406
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
18407
|
+
// QUERYING
|
|
18408
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
18409
|
+
/**
|
|
18410
|
+
* Get all symbols in a specific file.
|
|
18411
|
+
*/
|
|
18412
|
+
getSymbolsForFile(filePath) {
|
|
18413
|
+
return this.loadSymbolsWhere("file_path = ?", [filePath]);
|
|
18414
|
+
}
|
|
18415
|
+
/**
|
|
18416
|
+
* Search symbols by name pattern.
|
|
18417
|
+
*/
|
|
18418
|
+
searchSymbols(namePattern, limit = 50) {
|
|
18419
|
+
return this.loadSymbolsWhere("name LIKE ?", [`%${namePattern}%`]).slice(0, limit);
|
|
18420
|
+
}
|
|
18421
|
+
/**
|
|
18422
|
+
* Get files that import a given file.
|
|
18423
|
+
*/
|
|
18424
|
+
getDependents(filePath) {
|
|
18425
|
+
const rows = this.db.prepare("SELECT DISTINCT file_path FROM imports WHERE resolved_path = ?").all(filePath);
|
|
18426
|
+
return rows.map((r) => r.file_path);
|
|
18427
|
+
}
|
|
18428
|
+
/**
|
|
18429
|
+
* Get files that a given file imports.
|
|
18430
|
+
*/
|
|
18431
|
+
getDependencies(filePath) {
|
|
18432
|
+
const rows = this.db.prepare('SELECT DISTINCT resolved_path FROM imports WHERE file_path = ? AND resolved_path != ""').all(filePath);
|
|
18433
|
+
return rows.map((r) => r.resolved_path);
|
|
18434
|
+
}
|
|
18435
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
18436
|
+
// CLEANUP
|
|
18437
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
18438
|
+
/**
|
|
18439
|
+
* Close the database connection.
|
|
18440
|
+
*/
|
|
18441
|
+
close() {
|
|
18442
|
+
this.db.close();
|
|
18443
|
+
}
|
|
18444
|
+
/**
|
|
18445
|
+
* Wipe all data and rebuild schema.
|
|
18446
|
+
*/
|
|
18447
|
+
reset() {
|
|
18448
|
+
this.db.exec("DROP TABLE IF EXISTS file_hashes");
|
|
18449
|
+
this.db.exec("DROP TABLE IF EXISTS files");
|
|
18450
|
+
this.db.exec("DROP TABLE IF EXISTS symbols");
|
|
18451
|
+
this.db.exec("DROP TABLE IF EXISTS imports");
|
|
18452
|
+
this.db.exec("DROP TABLE IF EXISTS call_edges");
|
|
18453
|
+
this.db.exec("DROP TABLE IF EXISTS routes");
|
|
18454
|
+
this.db.exec("DROP TABLE IF EXISTS services");
|
|
18455
|
+
this.db.exec("DROP TABLE IF EXISTS embeddings");
|
|
18456
|
+
this.db.exec("DROP TABLE IF EXISTS meta");
|
|
18457
|
+
this.initSchema();
|
|
18458
|
+
}
|
|
18459
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
18460
|
+
// PRIVATE — File Discovery
|
|
18461
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
18462
|
+
async discoverFiles() {
|
|
18463
|
+
const files = await (0, import_fast_glob2.glob)(this.config.includePatterns, {
|
|
18464
|
+
cwd: this.rootPath,
|
|
18465
|
+
ignore: this.config.excludePatterns,
|
|
18466
|
+
absolute: false,
|
|
18467
|
+
dot: false,
|
|
18468
|
+
onlyFiles: true
|
|
18469
|
+
});
|
|
18470
|
+
const hashes = [];
|
|
18471
|
+
const limit = this.config.maxFiles;
|
|
18472
|
+
for (const relPath of files.slice(0, limit)) {
|
|
18473
|
+
const absPath = join(this.rootPath, relPath);
|
|
18474
|
+
try {
|
|
18475
|
+
const fileStat = await stat(absPath);
|
|
18476
|
+
if (fileStat.size > this.config.maxFileSize) continue;
|
|
18477
|
+
const content = await readFile(absPath, "utf-8");
|
|
18478
|
+
hashes.push({
|
|
18479
|
+
relativePath: relPath.replace(/\\/g, "/"),
|
|
18480
|
+
contentHash: hashContent2(content),
|
|
18481
|
+
sizeBytes: fileStat.size,
|
|
18482
|
+
modifiedMs: Math.floor(fileStat.mtimeMs)
|
|
18483
|
+
});
|
|
18484
|
+
} catch {
|
|
18770
18485
|
}
|
|
18771
|
-
lines.push("");
|
|
18772
18486
|
}
|
|
18773
|
-
|
|
18774
|
-
|
|
18775
|
-
|
|
18776
|
-
|
|
18777
|
-
|
|
18778
|
-
|
|
18487
|
+
return hashes;
|
|
18488
|
+
}
|
|
18489
|
+
getStoredHashes() {
|
|
18490
|
+
const rows = this.db.prepare("SELECT relative_path, content_hash, size_bytes, modified_ms FROM file_hashes").all();
|
|
18491
|
+
const map = /* @__PURE__ */ new Map();
|
|
18492
|
+
for (const row of rows) {
|
|
18493
|
+
map.set(row.relative_path, {
|
|
18494
|
+
relativePath: row.relative_path,
|
|
18495
|
+
contentHash: row.content_hash,
|
|
18496
|
+
sizeBytes: row.size_bytes,
|
|
18497
|
+
modifiedMs: row.modified_ms
|
|
18498
|
+
});
|
|
18779
18499
|
}
|
|
18780
|
-
return
|
|
18500
|
+
return map;
|
|
18781
18501
|
}
|
|
18782
|
-
|
|
18783
|
-
|
|
18784
|
-
|
|
18785
|
-
|
|
18786
|
-
|
|
18787
|
-
|
|
18502
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
18503
|
+
// PRIVATE — Storage
|
|
18504
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
18505
|
+
storeFiles(parsedFiles) {
|
|
18506
|
+
if (parsedFiles.length === 0) return;
|
|
18507
|
+
const insertHash = this.db.prepare(
|
|
18508
|
+
"INSERT OR REPLACE INTO file_hashes (relative_path, content_hash, size_bytes, modified_ms) VALUES (?, ?, ?, ?)"
|
|
18509
|
+
);
|
|
18510
|
+
const insertFile = this.db.prepare(
|
|
18511
|
+
"INSERT OR REPLACE INTO files (id, path, relative_path, language, line_count, exports, content) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
|
18512
|
+
);
|
|
18513
|
+
const insertSymbol = this.db.prepare(
|
|
18514
|
+
"INSERT OR REPLACE INTO symbols (id, name, kind, file_path, start_line, end_line, exported, async, params, branches, signature) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
18515
|
+
);
|
|
18516
|
+
const insertImport = this.db.prepare(
|
|
18517
|
+
"INSERT INTO imports (file_id, file_path, source_path, resolved_path, imported_symbols, is_type_only, is_dynamic, line) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
|
18518
|
+
);
|
|
18519
|
+
const insertCallEdge = this.db.prepare(
|
|
18520
|
+
"INSERT INTO call_edges (caller_id, callee_id, caller_name, callee_name, caller_file, callee_file) VALUES (?, ?, ?, ?, ?, ?)"
|
|
18521
|
+
);
|
|
18522
|
+
const txn = this.db.transaction(() => {
|
|
18523
|
+
for (const parsed of parsedFiles) {
|
|
18524
|
+
const { file, symbols, imports, callEdges, contentHash } = parsed;
|
|
18525
|
+
insertHash.run(file.relativePath, contentHash, 0, Date.now());
|
|
18526
|
+
insertFile.run(
|
|
18527
|
+
file.id,
|
|
18528
|
+
file.path,
|
|
18529
|
+
file.relativePath,
|
|
18530
|
+
file.language,
|
|
18531
|
+
file.lineCount,
|
|
18532
|
+
JSON.stringify(file.exports),
|
|
18533
|
+
this.config.storeContent ? file.content ?? null : null
|
|
18534
|
+
);
|
|
18535
|
+
for (const sym of symbols) {
|
|
18536
|
+
insertSymbol.run(
|
|
18537
|
+
sym.id,
|
|
18538
|
+
sym.name,
|
|
18539
|
+
sym.kind,
|
|
18540
|
+
sym.filePath,
|
|
18541
|
+
sym.startLine,
|
|
18542
|
+
sym.endLine,
|
|
18543
|
+
sym.exported ? 1 : 0,
|
|
18544
|
+
sym.async ? 1 : 0,
|
|
18545
|
+
sym.params ?? null,
|
|
18546
|
+
sym.branches ?? null,
|
|
18547
|
+
sym.signature ?? null
|
|
18548
|
+
);
|
|
18549
|
+
}
|
|
18550
|
+
for (const imp of imports) {
|
|
18551
|
+
insertImport.run(
|
|
18552
|
+
imp.fileId,
|
|
18553
|
+
imp.filePath,
|
|
18554
|
+
imp.sourcePath,
|
|
18555
|
+
imp.resolvedPath,
|
|
18556
|
+
JSON.stringify(imp.importedSymbols),
|
|
18557
|
+
imp.isTypeOnly ? 1 : 0,
|
|
18558
|
+
imp.isDynamic ? 1 : 0,
|
|
18559
|
+
imp.line
|
|
18560
|
+
);
|
|
18561
|
+
}
|
|
18562
|
+
for (const edge of callEdges) {
|
|
18563
|
+
insertCallEdge.run(
|
|
18564
|
+
edge.callerId,
|
|
18565
|
+
edge.calleeId,
|
|
18566
|
+
edge.callerName,
|
|
18567
|
+
edge.calleeName,
|
|
18568
|
+
edge.callerFile,
|
|
18569
|
+
edge.calleeFile
|
|
18570
|
+
);
|
|
18571
|
+
}
|
|
18572
|
+
}
|
|
18573
|
+
});
|
|
18574
|
+
txn();
|
|
18788
18575
|
}
|
|
18789
|
-
|
|
18790
|
-
|
|
18791
|
-
|
|
18792
|
-
|
|
18793
|
-
|
|
18794
|
-
|
|
18795
|
-
|
|
18796
|
-
|
|
18797
|
-
|
|
18798
|
-
|
|
18799
|
-
|
|
18800
|
-
|
|
18801
|
-
|
|
18802
|
-
|
|
18803
|
-
{ name: "Complexity", score: dims.complexity, explain: dims.complexity >= 80 ? "Complexity is well-managed" : "High-complexity hotspots detected" }
|
|
18804
|
-
];
|
|
18805
|
-
for (const entry of entries) {
|
|
18806
|
-
const bar = this.renderBar(entry.score);
|
|
18807
|
-
lines.push(`${bar} **${entry.name}**: ${entry.score}/100 \u2014 ${entry.explain}`);
|
|
18808
|
-
}
|
|
18809
|
-
return lines.join("\n");
|
|
18576
|
+
removeFiles(relativePaths) {
|
|
18577
|
+
if (relativePaths.length === 0) return;
|
|
18578
|
+
const txn = this.db.transaction(() => {
|
|
18579
|
+
for (const relPath of relativePaths) {
|
|
18580
|
+
const absPath = join(this.rootPath, relPath);
|
|
18581
|
+
this.db.prepare("DELETE FROM file_hashes WHERE relative_path = ?").run(relPath);
|
|
18582
|
+
this.db.prepare("DELETE FROM files WHERE relative_path = ?").run(relPath);
|
|
18583
|
+
this.db.prepare("DELETE FROM symbols WHERE file_path = ? OR file_path = ?").run(absPath, relPath);
|
|
18584
|
+
this.db.prepare("DELETE FROM imports WHERE file_path = ? OR file_path = ?").run(absPath, relPath);
|
|
18585
|
+
this.db.prepare("DELETE FROM call_edges WHERE caller_file = ? OR callee_file = ? OR caller_file = ? OR callee_file = ?").run(absPath, absPath, relPath, relPath);
|
|
18586
|
+
this.db.prepare("DELETE FROM embeddings WHERE path = ?").run(relPath);
|
|
18587
|
+
}
|
|
18588
|
+
});
|
|
18589
|
+
txn();
|
|
18810
18590
|
}
|
|
18811
18591
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
18812
|
-
// PRIVATE
|
|
18592
|
+
// PRIVATE — Loading
|
|
18813
18593
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
18814
|
-
|
|
18815
|
-
const
|
|
18816
|
-
|
|
18817
|
-
|
|
18818
|
-
|
|
18819
|
-
|
|
18820
|
-
|
|
18821
|
-
|
|
18822
|
-
|
|
18823
|
-
|
|
18824
|
-
|
|
18825
|
-
break;
|
|
18826
|
-
case "repository":
|
|
18827
|
-
parts.push("Data access layer");
|
|
18828
|
-
break;
|
|
18829
|
-
case "middleware":
|
|
18830
|
-
parts.push("Request middleware");
|
|
18831
|
-
break;
|
|
18832
|
-
case "test":
|
|
18833
|
-
parts.push("Test file");
|
|
18834
|
-
break;
|
|
18835
|
-
case "config":
|
|
18836
|
-
parts.push("Configuration");
|
|
18837
|
-
break;
|
|
18838
|
-
case "type":
|
|
18839
|
-
parts.push("Type definitions");
|
|
18840
|
-
break;
|
|
18841
|
-
case "util":
|
|
18842
|
-
parts.push("Utility module");
|
|
18843
|
-
break;
|
|
18844
|
-
case "entry":
|
|
18845
|
-
parts.push("Entry point");
|
|
18846
|
-
break;
|
|
18847
|
-
default:
|
|
18848
|
-
parts.push("Source file");
|
|
18849
|
-
}
|
|
18850
|
-
if (ctx.fileContext) {
|
|
18851
|
-
if (ctx.fileContext.layer) parts.push(`in ${ctx.fileContext.layer} layer`);
|
|
18852
|
-
if (ctx.fileContext.dependedOnBy.length > 10) parts.push("(high-impact)");
|
|
18853
|
-
}
|
|
18854
|
-
return parts.join(" ");
|
|
18594
|
+
loadFiles() {
|
|
18595
|
+
const rows = this.db.prepare("SELECT id, path, relative_path, language, line_count, exports, content FROM files").all();
|
|
18596
|
+
return rows.map((row) => ({
|
|
18597
|
+
id: row.id,
|
|
18598
|
+
path: row.path,
|
|
18599
|
+
relativePath: row.relative_path,
|
|
18600
|
+
language: row.language,
|
|
18601
|
+
lineCount: row.line_count,
|
|
18602
|
+
exports: JSON.parse(row.exports),
|
|
18603
|
+
content: row.content ?? void 0
|
|
18604
|
+
}));
|
|
18855
18605
|
}
|
|
18856
|
-
|
|
18857
|
-
|
|
18858
|
-
|
|
18859
|
-
|
|
18606
|
+
loadSymbols() {
|
|
18607
|
+
const rows = this.db.prepare("SELECT id, name, kind, file_path, start_line, end_line, exported, async, params, branches FROM symbols").all();
|
|
18608
|
+
return rows.map((row) => ({
|
|
18609
|
+
id: row.id,
|
|
18610
|
+
name: row.name,
|
|
18611
|
+
kind: row.kind,
|
|
18612
|
+
filePath: row.file_path,
|
|
18613
|
+
startLine: row.start_line,
|
|
18614
|
+
endLine: row.end_line,
|
|
18615
|
+
exported: row.exported === 1,
|
|
18616
|
+
async: row.async === 1,
|
|
18617
|
+
params: row.params ?? void 0,
|
|
18618
|
+
branches: row.branches ?? void 0
|
|
18619
|
+
}));
|
|
18860
18620
|
}
|
|
18861
|
-
|
|
18862
|
-
|
|
18863
|
-
|
|
18864
|
-
|
|
18621
|
+
loadSymbolsWhere(where, params) {
|
|
18622
|
+
const rows = this.db.prepare(`SELECT id, name, kind, file_path, start_line, end_line, exported, async, params, branches FROM symbols WHERE ${where}`).all(...params);
|
|
18623
|
+
return rows.map((row) => ({
|
|
18624
|
+
id: row.id,
|
|
18625
|
+
name: row.name,
|
|
18626
|
+
kind: row.kind,
|
|
18627
|
+
filePath: row.file_path,
|
|
18628
|
+
startLine: row.start_line,
|
|
18629
|
+
endLine: row.end_line,
|
|
18630
|
+
exported: row.exported === 1,
|
|
18631
|
+
async: row.async === 1,
|
|
18632
|
+
params: row.params ?? void 0,
|
|
18633
|
+
branches: row.branches ?? void 0
|
|
18634
|
+
}));
|
|
18635
|
+
}
|
|
18636
|
+
loadImports() {
|
|
18637
|
+
const rows = this.db.prepare("SELECT file_id, file_path, source_path, resolved_path, imported_symbols, is_type_only, is_dynamic, line FROM imports").all();
|
|
18638
|
+
return rows.map((row) => ({
|
|
18639
|
+
fileId: row.file_id,
|
|
18640
|
+
filePath: row.file_path,
|
|
18641
|
+
sourcePath: row.source_path,
|
|
18642
|
+
resolvedPath: row.resolved_path,
|
|
18643
|
+
importedSymbols: JSON.parse(row.imported_symbols),
|
|
18644
|
+
isTypeOnly: row.is_type_only === 1,
|
|
18645
|
+
isDynamic: row.is_dynamic === 1,
|
|
18646
|
+
line: row.line
|
|
18647
|
+
}));
|
|
18865
18648
|
}
|
|
18866
|
-
|
|
18867
|
-
const
|
|
18868
|
-
|
|
18869
|
-
|
|
18870
|
-
|
|
18649
|
+
loadCallEdges() {
|
|
18650
|
+
const rows = this.db.prepare("SELECT caller_id, callee_id, caller_name, callee_name, caller_file, callee_file FROM call_edges").all();
|
|
18651
|
+
return rows.map((row) => ({
|
|
18652
|
+
callerId: row.caller_id,
|
|
18653
|
+
calleeId: row.callee_id,
|
|
18654
|
+
callerName: row.caller_name,
|
|
18655
|
+
calleeName: row.callee_name,
|
|
18656
|
+
callerFile: row.caller_file,
|
|
18657
|
+
calleeFile: row.callee_file
|
|
18658
|
+
}));
|
|
18871
18659
|
}
|
|
18872
|
-
|
|
18873
|
-
const
|
|
18874
|
-
|
|
18875
|
-
|
|
18876
|
-
|
|
18660
|
+
loadRoutes() {
|
|
18661
|
+
const rows = this.db.prepare("SELECT path, method, handler, file, line, middleware, auth FROM routes").all();
|
|
18662
|
+
return rows.map((row) => ({
|
|
18663
|
+
path: row.path,
|
|
18664
|
+
method: row.method,
|
|
18665
|
+
handler: row.handler,
|
|
18666
|
+
file: row.file,
|
|
18667
|
+
line: row.line,
|
|
18668
|
+
middleware: JSON.parse(row.middleware),
|
|
18669
|
+
auth: row.auth === null ? void 0 : row.auth === 1
|
|
18670
|
+
}));
|
|
18877
18671
|
}
|
|
18878
|
-
|
|
18879
|
-
const
|
|
18880
|
-
return
|
|
18672
|
+
loadServices() {
|
|
18673
|
+
const rows = this.db.prepare("SELECT id, name, root_path FROM services").all();
|
|
18674
|
+
return rows.map((row) => ({
|
|
18675
|
+
id: row.id,
|
|
18676
|
+
name: row.name,
|
|
18677
|
+
rootPath: row.root_path
|
|
18678
|
+
}));
|
|
18881
18679
|
}
|
|
18882
18680
|
};
|
|
18883
|
-
function
|
|
18884
|
-
|
|
18885
|
-
return parts[parts.length - 1] ?? filePath;
|
|
18681
|
+
function hashContent2(content) {
|
|
18682
|
+
return createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
18886
18683
|
}
|
|
18887
|
-
|
|
18888
|
-
|
|
18889
|
-
|
|
18890
|
-
|
|
18891
|
-
|
|
18892
|
-
|
|
18893
|
-
|
|
18894
|
-
|
|
18684
|
+
var EXT_LANG = {
|
|
18685
|
+
".ts": "typescript",
|
|
18686
|
+
".tsx": "typescript",
|
|
18687
|
+
".js": "javascript",
|
|
18688
|
+
".jsx": "javascript",
|
|
18689
|
+
".mjs": "javascript",
|
|
18690
|
+
".cjs": "javascript",
|
|
18691
|
+
".py": "python",
|
|
18692
|
+
".rs": "rust",
|
|
18693
|
+
".go": "go",
|
|
18694
|
+
".java": "java",
|
|
18695
|
+
".c": "c",
|
|
18696
|
+
".h": "c",
|
|
18697
|
+
".cpp": "cpp",
|
|
18698
|
+
".hpp": "cpp",
|
|
18699
|
+
".rb": "ruby",
|
|
18700
|
+
".swift": "swift",
|
|
18701
|
+
".kt": "kotlin",
|
|
18702
|
+
".lua": "lua",
|
|
18703
|
+
".zig": "zig",
|
|
18704
|
+
".cs": "csharp"
|
|
18705
|
+
};
|
|
18706
|
+
var TS_IMPORT_RE = /^import\s+(?:type\s+)?(?:\{[^}]*\}|[\w*]+(?:\s*,\s*\{[^}]*\})?)\s+from\s+['"]([^'"]+)['"]/gm;
|
|
18707
|
+
var TS_IMPORT_TYPE_RE = /^import\s+type\s+/;
|
|
18708
|
+
var TS_DYNAMIC_IMPORT_RE = /(?:import|require)\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
18709
|
+
var PY_IMPORT_RE = /^(?:from\s+([\w.]+)\s+import|import\s+([\w.]+))/gm;
|
|
18710
|
+
var GO_IMPORT_RE = /import\s+(?:\(\s*([\s\S]*?)\s*\)|"([^"]+)")/g;
|
|
18711
|
+
var DefaultFileParser = class {
|
|
18895
18712
|
rootPath;
|
|
18896
|
-
|
|
18897
|
-
|
|
18898
|
-
|
|
18899
|
-
|
|
18900
|
-
|
|
18901
|
-
|
|
18902
|
-
|
|
18903
|
-
|
|
18904
|
-
|
|
18905
|
-
|
|
18906
|
-
|
|
18907
|
-
|
|
18908
|
-
|
|
18909
|
-
|
|
18910
|
-
|
|
18911
|
-
|
|
18912
|
-
|
|
18913
|
-
|
|
18914
|
-
|
|
18915
|
-
|
|
18916
|
-
|
|
18713
|
+
treeSitterParser = null;
|
|
18714
|
+
treeSitterLoaded = false;
|
|
18715
|
+
constructor(rootPath) {
|
|
18716
|
+
this.rootPath = rootPath;
|
|
18717
|
+
}
|
|
18718
|
+
async parseFile(absolutePath, relativePath2) {
|
|
18719
|
+
const content = await readFile(absolutePath, "utf-8");
|
|
18720
|
+
const lines = content.split("\n");
|
|
18721
|
+
const ext2 = extname(absolutePath).toLowerCase();
|
|
18722
|
+
const language = EXT_LANG[ext2] ?? "unknown";
|
|
18723
|
+
const contentHash = createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
18724
|
+
const exports$1 = this.extractExports(content, language);
|
|
18725
|
+
const symbols = await this.extractSymbols(content, lines, absolutePath, relativePath2, language);
|
|
18726
|
+
const imports = this.extractImports(content, absolutePath, relativePath2, language);
|
|
18727
|
+
const callEdges = this.extractCallEdges(content, symbols, absolutePath);
|
|
18728
|
+
const fileId = `file:${relativePath2}`;
|
|
18729
|
+
const file = {
|
|
18730
|
+
id: fileId,
|
|
18731
|
+
path: absolutePath,
|
|
18732
|
+
relativePath: relativePath2,
|
|
18733
|
+
language,
|
|
18734
|
+
lineCount: lines.length,
|
|
18735
|
+
exports: exports$1,
|
|
18736
|
+
content
|
|
18917
18737
|
};
|
|
18918
|
-
|
|
18919
|
-
mkdirSync(dbDir, { recursive: true });
|
|
18920
|
-
this.db = new import_better_sqlite3.default(this.config.dbPath);
|
|
18921
|
-
this.db.pragma("journal_mode = WAL");
|
|
18922
|
-
this.db.pragma("synchronous = NORMAL");
|
|
18923
|
-
this.db.pragma("cache_size = -64000");
|
|
18924
|
-
this.initSchema();
|
|
18738
|
+
return { file, symbols, imports, callEdges, contentHash };
|
|
18925
18739
|
}
|
|
18926
18740
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
18927
|
-
//
|
|
18741
|
+
// EXPORTS
|
|
18928
18742
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
18929
|
-
|
|
18930
|
-
|
|
18931
|
-
|
|
18932
|
-
|
|
18933
|
-
|
|
18934
|
-
|
|
18935
|
-
|
|
18936
|
-
|
|
18937
|
-
|
|
18938
|
-
|
|
18939
|
-
|
|
18940
|
-
|
|
18941
|
-
|
|
18942
|
-
|
|
18943
|
-
|
|
18944
|
-
|
|
18945
|
-
|
|
18946
|
-
|
|
18947
|
-
|
|
18948
|
-
CREATE TABLE file_hashes (
|
|
18949
|
-
relative_path TEXT PRIMARY KEY,
|
|
18950
|
-
content_hash TEXT NOT NULL,
|
|
18951
|
-
size_bytes INTEGER NOT NULL,
|
|
18952
|
-
modified_ms INTEGER NOT NULL,
|
|
18953
|
-
indexed_at INTEGER NOT NULL DEFAULT (unixepoch('now'))
|
|
18954
|
-
);
|
|
18955
|
-
|
|
18956
|
-
CREATE TABLE files (
|
|
18957
|
-
id TEXT PRIMARY KEY,
|
|
18958
|
-
path TEXT NOT NULL,
|
|
18959
|
-
relative_path TEXT NOT NULL UNIQUE,
|
|
18960
|
-
language TEXT NOT NULL,
|
|
18961
|
-
line_count INTEGER NOT NULL,
|
|
18962
|
-
exports TEXT NOT NULL DEFAULT '[]',
|
|
18963
|
-
content TEXT
|
|
18964
|
-
);
|
|
18965
|
-
|
|
18966
|
-
CREATE TABLE symbols (
|
|
18967
|
-
id TEXT PRIMARY KEY,
|
|
18968
|
-
name TEXT NOT NULL,
|
|
18969
|
-
kind TEXT NOT NULL,
|
|
18970
|
-
file_path TEXT NOT NULL,
|
|
18971
|
-
start_line INTEGER NOT NULL,
|
|
18972
|
-
end_line INTEGER NOT NULL,
|
|
18973
|
-
exported INTEGER NOT NULL DEFAULT 0,
|
|
18974
|
-
async INTEGER NOT NULL DEFAULT 0,
|
|
18975
|
-
params INTEGER,
|
|
18976
|
-
branches INTEGER,
|
|
18977
|
-
signature TEXT
|
|
18978
|
-
);
|
|
18979
|
-
|
|
18980
|
-
CREATE TABLE imports (
|
|
18981
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
18982
|
-
file_id TEXT NOT NULL,
|
|
18983
|
-
file_path TEXT NOT NULL,
|
|
18984
|
-
source_path TEXT NOT NULL,
|
|
18985
|
-
resolved_path TEXT NOT NULL DEFAULT '',
|
|
18986
|
-
imported_symbols TEXT NOT NULL DEFAULT '[]',
|
|
18987
|
-
is_type_only INTEGER NOT NULL DEFAULT 0,
|
|
18988
|
-
is_dynamic INTEGER NOT NULL DEFAULT 0,
|
|
18989
|
-
line INTEGER NOT NULL DEFAULT 0
|
|
18990
|
-
);
|
|
18991
|
-
|
|
18992
|
-
CREATE TABLE call_edges (
|
|
18993
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
18994
|
-
caller_id TEXT NOT NULL,
|
|
18995
|
-
callee_id TEXT NOT NULL,
|
|
18996
|
-
caller_name TEXT NOT NULL,
|
|
18997
|
-
callee_name TEXT NOT NULL,
|
|
18998
|
-
caller_file TEXT NOT NULL,
|
|
18999
|
-
callee_file TEXT NOT NULL
|
|
19000
|
-
);
|
|
19001
|
-
|
|
19002
|
-
CREATE TABLE routes (
|
|
19003
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
19004
|
-
path TEXT NOT NULL,
|
|
19005
|
-
method TEXT NOT NULL,
|
|
19006
|
-
handler TEXT NOT NULL,
|
|
19007
|
-
file TEXT NOT NULL,
|
|
19008
|
-
line INTEGER NOT NULL DEFAULT 0,
|
|
19009
|
-
middleware TEXT NOT NULL DEFAULT '[]',
|
|
19010
|
-
auth INTEGER
|
|
19011
|
-
);
|
|
19012
|
-
|
|
19013
|
-
CREATE TABLE services (
|
|
19014
|
-
id TEXT PRIMARY KEY,
|
|
19015
|
-
name TEXT NOT NULL,
|
|
19016
|
-
root_path TEXT NOT NULL DEFAULT ''
|
|
19017
|
-
);
|
|
19018
|
-
|
|
19019
|
-
CREATE TABLE embeddings (
|
|
19020
|
-
path TEXT NOT NULL,
|
|
19021
|
-
chunk_id TEXT NOT NULL,
|
|
19022
|
-
chunk_type TEXT NOT NULL DEFAULT 'file',
|
|
19023
|
-
content_hash TEXT NOT NULL,
|
|
19024
|
-
vector BLOB NOT NULL,
|
|
19025
|
-
metadata TEXT NOT NULL DEFAULT '{}',
|
|
19026
|
-
PRIMARY KEY (path, chunk_id)
|
|
19027
|
-
);
|
|
19028
|
-
|
|
19029
|
-
-- Indexes for fast lookups
|
|
19030
|
-
CREATE INDEX idx_symbols_file ON symbols(file_path);
|
|
19031
|
-
CREATE INDEX idx_symbols_name ON symbols(name);
|
|
19032
|
-
CREATE INDEX idx_symbols_kind ON symbols(kind);
|
|
19033
|
-
CREATE INDEX idx_imports_file ON imports(file_path);
|
|
19034
|
-
CREATE INDEX idx_imports_source ON imports(source_path);
|
|
19035
|
-
CREATE INDEX idx_imports_resolved ON imports(resolved_path);
|
|
19036
|
-
CREATE INDEX idx_call_edges_caller ON call_edges(caller_file);
|
|
19037
|
-
CREATE INDEX idx_call_edges_callee ON call_edges(callee_file);
|
|
19038
|
-
CREATE INDEX idx_embeddings_type ON embeddings(chunk_type);
|
|
19039
|
-
`);
|
|
19040
|
-
this.setMeta("schema_version", String(SCHEMA_VERSION));
|
|
19041
|
-
this.setMeta("created_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
18743
|
+
extractExports(content, language) {
|
|
18744
|
+
if (language !== "typescript" && language !== "javascript") return [];
|
|
18745
|
+
const exports$1 = [];
|
|
18746
|
+
const exportRe = /export\s+(?:default\s+)?(?:async\s+)?(?:function|class|const|let|var|type|interface|enum)\s+(\w+)/g;
|
|
18747
|
+
for (const match2 of content.matchAll(exportRe)) {
|
|
18748
|
+
if (match2[1]) exports$1.push(match2[1]);
|
|
18749
|
+
}
|
|
18750
|
+
if (/export\s+default\s/.test(content) && !exports$1.includes("default")) {
|
|
18751
|
+
exports$1.push("default");
|
|
18752
|
+
}
|
|
18753
|
+
const reExportRe = /export\s+\{([^}]+)\}\s+from/g;
|
|
18754
|
+
for (const match2 of content.matchAll(reExportRe)) {
|
|
18755
|
+
for (const sym of match2[1].split(",")) {
|
|
18756
|
+
const name = sym.trim().split(/\s+as\s+/).pop()?.trim();
|
|
18757
|
+
if (name) exports$1.push(name);
|
|
18758
|
+
}
|
|
18759
|
+
}
|
|
18760
|
+
return [...new Set(exports$1)];
|
|
19042
18761
|
}
|
|
19043
|
-
|
|
18762
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
18763
|
+
// SYMBOLS
|
|
18764
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
18765
|
+
async extractSymbols(content, lines, absolutePath, relativePath2, language) {
|
|
18766
|
+
const tsSymbols = await this.tryTreeSitter(content, absolutePath);
|
|
18767
|
+
if (tsSymbols) {
|
|
18768
|
+
return tsSymbols.map((sym, i) => ({
|
|
18769
|
+
id: `sym:${relativePath2}:${sym.name}:${sym.line}`,
|
|
18770
|
+
name: sym.name,
|
|
18771
|
+
kind: mapTreeSitterKind(sym.kind),
|
|
18772
|
+
filePath: absolutePath,
|
|
18773
|
+
startLine: sym.line,
|
|
18774
|
+
endLine: sym.endLine,
|
|
18775
|
+
exported: this.isExported(content, sym.name, language),
|
|
18776
|
+
async: sym.signature.includes("async "),
|
|
18777
|
+
params: this.countParams(sym.signature),
|
|
18778
|
+
branches: void 0,
|
|
18779
|
+
signature: sym.signature
|
|
18780
|
+
}));
|
|
18781
|
+
}
|
|
18782
|
+
return this.extractSymbolsRegex(content, lines, absolutePath, relativePath2, language);
|
|
18783
|
+
}
|
|
18784
|
+
async tryTreeSitter(content, filePath) {
|
|
18785
|
+
if (!this.treeSitterLoaded) {
|
|
18786
|
+
this.treeSitterLoaded = true;
|
|
18787
|
+
try {
|
|
18788
|
+
const mod = await import('./tree-sitter-H5E7LKR4-MKO3NNLJ.js');
|
|
18789
|
+
this.treeSitterParser = mod.parseWithTreeSitter;
|
|
18790
|
+
} catch {
|
|
18791
|
+
this.treeSitterParser = null;
|
|
18792
|
+
}
|
|
18793
|
+
}
|
|
18794
|
+
if (!this.treeSitterParser) return null;
|
|
19044
18795
|
try {
|
|
19045
|
-
const
|
|
19046
|
-
|
|
18796
|
+
const ext2 = extname(filePath).toLowerCase();
|
|
18797
|
+
const symbols = await this.treeSitterParser(content, ext2);
|
|
18798
|
+
if (!symbols || symbols.length === 0) return null;
|
|
18799
|
+
return flattenCodeSymbols(symbols);
|
|
19047
18800
|
} catch {
|
|
19048
|
-
return
|
|
18801
|
+
return null;
|
|
19049
18802
|
}
|
|
19050
18803
|
}
|
|
19051
|
-
|
|
19052
|
-
|
|
19053
|
-
|
|
19054
|
-
|
|
19055
|
-
|
|
19056
|
-
|
|
18804
|
+
extractSymbolsRegex(content, lines, absolutePath, relativePath2, language) {
|
|
18805
|
+
const symbols = [];
|
|
18806
|
+
if (language === "typescript" || language === "javascript") {
|
|
18807
|
+
const patterns = [
|
|
18808
|
+
{ re: /^(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*(<[^>]*>)?\s*\(([^)]*)\)/gm, kind: "function" },
|
|
18809
|
+
{ re: /^(?:export\s+)?(?:abstract\s+)?class\s+(\w+)/gm, kind: "class" },
|
|
18810
|
+
{ re: /^(?:export\s+)?interface\s+(\w+)/gm, kind: "interface" },
|
|
18811
|
+
{ re: /^(?:export\s+)?type\s+(\w+)\s*(?:<[^>]*>)?\s*=/gm, kind: "type" },
|
|
18812
|
+
{ re: /^(?:export\s+)?(?:const\s+)?enum\s+(\w+)/gm, kind: "enum" },
|
|
18813
|
+
{ re: /^(?:export\s+)?const\s+(\w+)\s*(?::\s*[^=]+)?\s*=\s*(?:async\s+)?\(/gm, kind: "function" }
|
|
18814
|
+
];
|
|
18815
|
+
for (const { re, kind } of patterns) {
|
|
18816
|
+
for (const match2 of content.matchAll(re)) {
|
|
18817
|
+
const name = match2[1];
|
|
18818
|
+
if (!name) continue;
|
|
18819
|
+
const line = content.slice(0, match2.index).split("\n").length;
|
|
18820
|
+
const endLine = this.findBlockEnd(lines, line - 1);
|
|
18821
|
+
symbols.push({
|
|
18822
|
+
id: `sym:${relativePath2}:${name}:${line}`,
|
|
18823
|
+
name,
|
|
18824
|
+
kind,
|
|
18825
|
+
filePath: absolutePath,
|
|
18826
|
+
startLine: line,
|
|
18827
|
+
endLine,
|
|
18828
|
+
exported: this.isExported(content, name, language),
|
|
18829
|
+
async: match2[0].includes("async"),
|
|
18830
|
+
params: this.countParams(match2[0]),
|
|
18831
|
+
branches: this.countBranches(lines, line - 1, endLine - 1),
|
|
18832
|
+
signature: match2[0].trim().replace(/\s*\{?\s*$/, "")
|
|
18833
|
+
});
|
|
18834
|
+
}
|
|
18835
|
+
}
|
|
18836
|
+
}
|
|
18837
|
+
if (language === "python") {
|
|
18838
|
+
const re = /^(?:async\s+)?(?:def|class)\s+(\w+)/gm;
|
|
18839
|
+
for (const match2 of content.matchAll(re)) {
|
|
18840
|
+
const name = match2[1];
|
|
18841
|
+
const kind = match2[0].includes("class") ? "class" : "function";
|
|
18842
|
+
const line = content.slice(0, match2.index).split("\n").length;
|
|
18843
|
+
symbols.push({
|
|
18844
|
+
id: `sym:${relativePath2}:${name}:${line}`,
|
|
18845
|
+
name,
|
|
18846
|
+
kind,
|
|
18847
|
+
filePath: absolutePath,
|
|
18848
|
+
startLine: line,
|
|
18849
|
+
endLine: line + 10,
|
|
18850
|
+
exported: true,
|
|
18851
|
+
async: match2[0].includes("async"),
|
|
18852
|
+
params: this.countParams(match2[0]),
|
|
18853
|
+
branches: void 0,
|
|
18854
|
+
signature: match2[0].trim()
|
|
18855
|
+
});
|
|
18856
|
+
}
|
|
18857
|
+
}
|
|
18858
|
+
return symbols;
|
|
19057
18859
|
}
|
|
19058
18860
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
19059
|
-
//
|
|
18861
|
+
// IMPORTS
|
|
19060
18862
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
19061
|
-
|
|
19062
|
-
|
|
19063
|
-
|
|
19064
|
-
|
|
19065
|
-
|
|
19066
|
-
|
|
19067
|
-
|
|
19068
|
-
|
|
19069
|
-
|
|
19070
|
-
|
|
19071
|
-
|
|
19072
|
-
|
|
19073
|
-
|
|
19074
|
-
|
|
19075
|
-
|
|
19076
|
-
|
|
18863
|
+
extractImports(content, absolutePath, relativePath2, language) {
|
|
18864
|
+
const imports = [];
|
|
18865
|
+
const fileId = `file:${relativePath2}`;
|
|
18866
|
+
if (language === "typescript" || language === "javascript") {
|
|
18867
|
+
for (const match2 of content.matchAll(TS_IMPORT_RE)) {
|
|
18868
|
+
const sourcePath = match2[1];
|
|
18869
|
+
const line = content.slice(0, match2.index).split("\n").length;
|
|
18870
|
+
const isTypeOnly = TS_IMPORT_TYPE_RE.test(match2[0]);
|
|
18871
|
+
const importedSymbols = this.extractImportedSymbols(match2[0]);
|
|
18872
|
+
const resolvedPath = this.resolveImportPath(sourcePath, absolutePath);
|
|
18873
|
+
imports.push({
|
|
18874
|
+
fileId,
|
|
18875
|
+
filePath: absolutePath,
|
|
18876
|
+
sourcePath,
|
|
18877
|
+
resolvedPath,
|
|
18878
|
+
importedSymbols,
|
|
18879
|
+
isTypeOnly,
|
|
18880
|
+
isDynamic: false,
|
|
18881
|
+
line
|
|
18882
|
+
});
|
|
19077
18883
|
}
|
|
19078
|
-
|
|
19079
|
-
|
|
19080
|
-
|
|
19081
|
-
|
|
19082
|
-
|
|
18884
|
+
for (const match2 of content.matchAll(TS_DYNAMIC_IMPORT_RE)) {
|
|
18885
|
+
const sourcePath = match2[1];
|
|
18886
|
+
const line = content.slice(0, match2.index).split("\n").length;
|
|
18887
|
+
imports.push({
|
|
18888
|
+
fileId,
|
|
18889
|
+
filePath: absolutePath,
|
|
18890
|
+
sourcePath,
|
|
18891
|
+
resolvedPath: this.resolveImportPath(sourcePath, absolutePath),
|
|
18892
|
+
importedSymbols: [],
|
|
18893
|
+
isTypeOnly: false,
|
|
18894
|
+
isDynamic: true,
|
|
18895
|
+
line
|
|
18896
|
+
});
|
|
19083
18897
|
}
|
|
19084
18898
|
}
|
|
19085
|
-
|
|
19086
|
-
|
|
19087
|
-
|
|
19088
|
-
|
|
19089
|
-
|
|
19090
|
-
|
|
19091
|
-
|
|
19092
|
-
|
|
19093
|
-
|
|
19094
|
-
|
|
19095
|
-
|
|
18899
|
+
if (language === "python") {
|
|
18900
|
+
for (const match2 of content.matchAll(PY_IMPORT_RE)) {
|
|
18901
|
+
const sourcePath = match2[1] || match2[2];
|
|
18902
|
+
if (!sourcePath) continue;
|
|
18903
|
+
const line = content.slice(0, match2.index).split("\n").length;
|
|
18904
|
+
imports.push({
|
|
18905
|
+
fileId,
|
|
18906
|
+
filePath: absolutePath,
|
|
18907
|
+
sourcePath,
|
|
18908
|
+
resolvedPath: "",
|
|
18909
|
+
importedSymbols: [],
|
|
18910
|
+
isTypeOnly: false,
|
|
18911
|
+
isDynamic: false,
|
|
18912
|
+
line
|
|
18913
|
+
});
|
|
18914
|
+
}
|
|
19096
18915
|
}
|
|
19097
|
-
|
|
19098
|
-
|
|
19099
|
-
|
|
19100
|
-
|
|
19101
|
-
const
|
|
19102
|
-
|
|
19103
|
-
|
|
18916
|
+
if (language === "go") {
|
|
18917
|
+
for (const match2 of content.matchAll(GO_IMPORT_RE)) {
|
|
18918
|
+
const block = match2[1] || match2[2];
|
|
18919
|
+
if (!block) continue;
|
|
18920
|
+
const paths = block.match(/"([^"]+)"/g) ?? [];
|
|
18921
|
+
for (const p of paths) {
|
|
18922
|
+
const sourcePath = p.replace(/"/g, "");
|
|
18923
|
+
imports.push({
|
|
18924
|
+
fileId,
|
|
18925
|
+
filePath: absolutePath,
|
|
18926
|
+
sourcePath,
|
|
18927
|
+
resolvedPath: "",
|
|
18928
|
+
importedSymbols: [],
|
|
18929
|
+
isTypeOnly: false,
|
|
18930
|
+
isDynamic: false,
|
|
18931
|
+
line: 0
|
|
18932
|
+
});
|
|
18933
|
+
}
|
|
19104
18934
|
}
|
|
19105
18935
|
}
|
|
19106
|
-
|
|
19107
|
-
const data = this.loadCodebaseData();
|
|
19108
|
-
const stats = {
|
|
19109
|
-
totalFiles: data.files.length,
|
|
19110
|
-
totalSymbols: data.symbols.length,
|
|
19111
|
-
totalImports: data.imports.length,
|
|
19112
|
-
indexedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19113
|
-
reindexedFiles: changed.length,
|
|
19114
|
-
skippedFiles: unchanged.length,
|
|
19115
|
-
durationMs: Date.now() - startMs
|
|
19116
|
-
};
|
|
19117
|
-
this.setMeta("last_index_at", stats.indexedAt);
|
|
19118
|
-
this.setMeta("last_index_stats", JSON.stringify(stats));
|
|
19119
|
-
return { data, stats };
|
|
18936
|
+
return imports;
|
|
19120
18937
|
}
|
|
19121
|
-
|
|
19122
|
-
|
|
19123
|
-
|
|
19124
|
-
|
|
19125
|
-
|
|
19126
|
-
|
|
19127
|
-
|
|
19128
|
-
|
|
19129
|
-
|
|
18938
|
+
extractImportedSymbols(importLine) {
|
|
18939
|
+
const braceMatch = importLine.match(/\{([^}]+)\}/);
|
|
18940
|
+
if (!braceMatch) {
|
|
18941
|
+
const defaultMatch = importLine.match(/import\s+(?:type\s+)?(\w+)\s+from/);
|
|
18942
|
+
return defaultMatch?.[1] ? [defaultMatch[1]] : [];
|
|
18943
|
+
}
|
|
18944
|
+
return braceMatch[1].split(",").map((s) => {
|
|
18945
|
+
const parts = s.trim().split(/\s+as\s+/);
|
|
18946
|
+
return parts[parts.length - 1].trim();
|
|
18947
|
+
}).filter(Boolean);
|
|
18948
|
+
}
|
|
18949
|
+
resolveImportPath(sourcePath, fromFile) {
|
|
18950
|
+
if (!sourcePath.startsWith(".")) return sourcePath;
|
|
18951
|
+
const dir = dirname(fromFile);
|
|
18952
|
+
const resolved = resolve(dir, sourcePath);
|
|
18953
|
+
const extensions = [".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
|
|
18954
|
+
for (const ext2 of extensions) {
|
|
18955
|
+
const candidate = resolved + ext2;
|
|
19130
18956
|
try {
|
|
19131
|
-
|
|
19132
|
-
|
|
18957
|
+
accessSync(candidate);
|
|
18958
|
+
return candidate;
|
|
19133
18959
|
} catch {
|
|
19134
18960
|
}
|
|
19135
18961
|
}
|
|
19136
|
-
|
|
19137
|
-
const totalFiles = this.db.prepare("SELECT COUNT(*) as cnt FROM files").get();
|
|
19138
|
-
const totalSymbols = this.db.prepare("SELECT COUNT(*) as cnt FROM symbols").get();
|
|
19139
|
-
const totalImports = this.db.prepare("SELECT COUNT(*) as cnt FROM imports").get();
|
|
19140
|
-
return {
|
|
19141
|
-
totalFiles: totalFiles.cnt,
|
|
19142
|
-
totalSymbols: totalSymbols.cnt,
|
|
19143
|
-
totalImports: totalImports.cnt,
|
|
19144
|
-
indexedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19145
|
-
reindexedFiles: parsedFiles.length,
|
|
19146
|
-
skippedFiles: 0,
|
|
19147
|
-
durationMs: Date.now() - startMs
|
|
19148
|
-
};
|
|
18962
|
+
return resolved;
|
|
19149
18963
|
}
|
|
19150
18964
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
19151
|
-
//
|
|
18965
|
+
// CALL EDGES (basic extraction)
|
|
19152
18966
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
19153
|
-
|
|
19154
|
-
|
|
19155
|
-
|
|
19156
|
-
|
|
19157
|
-
|
|
19158
|
-
|
|
19159
|
-
|
|
19160
|
-
|
|
19161
|
-
|
|
19162
|
-
|
|
19163
|
-
|
|
19164
|
-
|
|
19165
|
-
|
|
19166
|
-
|
|
19167
|
-
|
|
19168
|
-
|
|
19169
|
-
|
|
19170
|
-
|
|
19171
|
-
|
|
19172
|
-
|
|
19173
|
-
|
|
19174
|
-
|
|
19175
|
-
|
|
19176
|
-
}
|
|
19177
|
-
/**
|
|
19178
|
-
* Get the last index timestamp.
|
|
19179
|
-
*/
|
|
19180
|
-
getLastIndexedAt() {
|
|
19181
|
-
return this.getMeta("last_index_at");
|
|
19182
|
-
}
|
|
19183
|
-
/**
|
|
19184
|
-
* Get the last index stats.
|
|
19185
|
-
*/
|
|
19186
|
-
getLastIndexStats() {
|
|
19187
|
-
const raw = this.getMeta("last_index_stats");
|
|
19188
|
-
if (!raw) return null;
|
|
19189
|
-
try {
|
|
19190
|
-
return JSON.parse(raw);
|
|
19191
|
-
} catch {
|
|
19192
|
-
return null;
|
|
18967
|
+
extractCallEdges(content, symbols, filePath) {
|
|
18968
|
+
const edges = [];
|
|
18969
|
+
const functionNames = new Set(symbols.filter((s) => s.kind === "function" || s.kind === "method").map((s) => s.name));
|
|
18970
|
+
for (const caller of symbols) {
|
|
18971
|
+
if (caller.kind !== "function" && caller.kind !== "method") continue;
|
|
18972
|
+
const body = content.split("\n").slice(caller.startLine - 1, caller.endLine).join("\n");
|
|
18973
|
+
for (const calleeName of functionNames) {
|
|
18974
|
+
if (calleeName === caller.name) continue;
|
|
18975
|
+
const callRe = new RegExp(`\\b${calleeName}\\s*\\(`, "g");
|
|
18976
|
+
if (callRe.test(body)) {
|
|
18977
|
+
const callee = symbols.find((s) => s.name === calleeName);
|
|
18978
|
+
if (callee) {
|
|
18979
|
+
edges.push({
|
|
18980
|
+
callerId: caller.id,
|
|
18981
|
+
calleeId: callee.id,
|
|
18982
|
+
callerName: caller.name,
|
|
18983
|
+
calleeName: callee.name,
|
|
18984
|
+
callerFile: filePath,
|
|
18985
|
+
calleeFile: filePath
|
|
18986
|
+
});
|
|
18987
|
+
}
|
|
18988
|
+
}
|
|
18989
|
+
}
|
|
19193
18990
|
}
|
|
18991
|
+
return edges;
|
|
19194
18992
|
}
|
|
19195
18993
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
19196
|
-
//
|
|
18994
|
+
// HELPERS
|
|
19197
18995
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
19198
|
-
|
|
19199
|
-
|
|
19200
|
-
|
|
19201
|
-
|
|
19202
|
-
const
|
|
19203
|
-
|
|
19204
|
-
INSERT OR REPLACE INTO embeddings (path, chunk_id, chunk_type, content_hash, vector, metadata)
|
|
19205
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
19206
|
-
`).run(path10, chunkId, chunkType, contentHash, vectorBuf, JSON.stringify(metadata ?? {}));
|
|
19207
|
-
}
|
|
19208
|
-
/**
|
|
19209
|
-
* Load embedding for a specific chunk.
|
|
19210
|
-
*/
|
|
19211
|
-
loadEmbedding(path10, chunkId) {
|
|
19212
|
-
const row = this.db.prepare("SELECT vector, content_hash, metadata FROM embeddings WHERE path = ? AND chunk_id = ?").get(path10, chunkId);
|
|
19213
|
-
if (!row) return null;
|
|
19214
|
-
return {
|
|
19215
|
-
vector: Array.from(new Float32Array(row.vector.buffer, row.vector.byteOffset, row.vector.byteLength / 4)),
|
|
19216
|
-
contentHash: row.content_hash,
|
|
19217
|
-
metadata: JSON.parse(row.metadata)
|
|
19218
|
-
};
|
|
18996
|
+
isExported(content, name, language) {
|
|
18997
|
+
if (language === "python") return true;
|
|
18998
|
+
const re = new RegExp(`export\\s+(?:default\\s+)?(?:async\\s+)?(?:function|class|const|let|var|type|interface|enum)\\s+${name}\\b`);
|
|
18999
|
+
if (re.test(content)) return true;
|
|
19000
|
+
const reExport = new RegExp(`export\\s+\\{[^}]*\\b${name}\\b[^}]*\\}`);
|
|
19001
|
+
return reExport.test(content);
|
|
19219
19002
|
}
|
|
19220
|
-
|
|
19221
|
-
|
|
19222
|
-
|
|
19223
|
-
|
|
19224
|
-
const rows = this.db.prepare("SELECT path, chunk_id, vector, content_hash, metadata FROM embeddings WHERE chunk_type = ?").all(chunkType);
|
|
19225
|
-
return rows.map((row) => ({
|
|
19226
|
-
path: row.path,
|
|
19227
|
-
chunkId: row.chunk_id,
|
|
19228
|
-
vector: Array.from(new Float32Array(row.vector.buffer, row.vector.byteOffset, row.vector.byteLength / 4)),
|
|
19229
|
-
contentHash: row.content_hash,
|
|
19230
|
-
metadata: JSON.parse(row.metadata)
|
|
19231
|
-
}));
|
|
19003
|
+
countParams(signature) {
|
|
19004
|
+
const parenMatch = signature.match(/\(([^)]*)\)/);
|
|
19005
|
+
if (!parenMatch || !parenMatch[1].trim()) return 0;
|
|
19006
|
+
return parenMatch[1].split(",").length;
|
|
19232
19007
|
}
|
|
19233
|
-
|
|
19234
|
-
|
|
19235
|
-
|
|
19236
|
-
|
|
19237
|
-
|
|
19238
|
-
|
|
19239
|
-
|
|
19240
|
-
return
|
|
19008
|
+
countBranches(lines, startIdx, endIdx) {
|
|
19009
|
+
let branches = 0;
|
|
19010
|
+
const branchRe = /\b(if|else if|case|for|while|catch|&&|\|\||\?\?)\b/g;
|
|
19011
|
+
for (let i = startIdx; i < Math.min(endIdx, lines.length); i++) {
|
|
19012
|
+
const matches = lines[i].match(branchRe);
|
|
19013
|
+
if (matches) branches += matches.length;
|
|
19014
|
+
}
|
|
19015
|
+
return branches;
|
|
19241
19016
|
}
|
|
19242
|
-
|
|
19243
|
-
|
|
19244
|
-
|
|
19245
|
-
|
|
19246
|
-
|
|
19247
|
-
|
|
19248
|
-
|
|
19249
|
-
|
|
19017
|
+
findBlockEnd(lines, startIdx) {
|
|
19018
|
+
let depth = 0;
|
|
19019
|
+
let seenOpen = false;
|
|
19020
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
19021
|
+
for (const ch of lines[i]) {
|
|
19022
|
+
if (ch === "{") {
|
|
19023
|
+
depth++;
|
|
19024
|
+
seenOpen = true;
|
|
19025
|
+
} else if (ch === "}" && seenOpen) {
|
|
19026
|
+
depth--;
|
|
19027
|
+
if (depth <= 0) return i + 1;
|
|
19028
|
+
}
|
|
19029
|
+
}
|
|
19030
|
+
}
|
|
19031
|
+
return startIdx + 1;
|
|
19250
19032
|
}
|
|
19251
|
-
|
|
19252
|
-
|
|
19253
|
-
|
|
19254
|
-
|
|
19255
|
-
|
|
19033
|
+
};
|
|
19034
|
+
function mapTreeSitterKind(kind) {
|
|
19035
|
+
const map = {
|
|
19036
|
+
function: "function",
|
|
19037
|
+
method: "method",
|
|
19038
|
+
class: "class",
|
|
19039
|
+
interface: "interface",
|
|
19040
|
+
type: "type",
|
|
19041
|
+
enum: "enum",
|
|
19042
|
+
const: "variable",
|
|
19043
|
+
variable: "variable",
|
|
19044
|
+
struct: "class",
|
|
19045
|
+
trait: "interface",
|
|
19046
|
+
export: "variable"
|
|
19047
|
+
};
|
|
19048
|
+
return map[kind] ?? "function";
|
|
19049
|
+
}
|
|
19050
|
+
function flattenCodeSymbols(symbols) {
|
|
19051
|
+
const flat = [];
|
|
19052
|
+
for (const sym of symbols) {
|
|
19053
|
+
flat.push({ name: sym.name, kind: sym.kind, line: sym.line, endLine: sym.endLine, signature: sym.signature });
|
|
19054
|
+
if (sym.children && Array.isArray(sym.children)) {
|
|
19055
|
+
flat.push(...flattenCodeSymbols(sym.children));
|
|
19056
|
+
}
|
|
19256
19057
|
}
|
|
19257
|
-
|
|
19258
|
-
|
|
19259
|
-
|
|
19260
|
-
|
|
19261
|
-
|
|
19262
|
-
|
|
19058
|
+
return flat;
|
|
19059
|
+
}
|
|
19060
|
+
var ContextSynthesizer = class {
|
|
19061
|
+
rootPath;
|
|
19062
|
+
data;
|
|
19063
|
+
dna;
|
|
19064
|
+
graph;
|
|
19065
|
+
ruleResult;
|
|
19066
|
+
constructor(rootPath, data, dna, graph, ruleResult) {
|
|
19067
|
+
this.rootPath = rootPath;
|
|
19068
|
+
this.data = data;
|
|
19069
|
+
this.dna = dna;
|
|
19070
|
+
this.graph = graph;
|
|
19071
|
+
this.ruleResult = ruleResult || null;
|
|
19263
19072
|
}
|
|
19264
19073
|
/**
|
|
19265
|
-
*
|
|
19074
|
+
* Synthesize the full context — the complete brain dump for AI agents.
|
|
19266
19075
|
*/
|
|
19267
|
-
|
|
19268
|
-
const
|
|
19269
|
-
|
|
19076
|
+
synthesize() {
|
|
19077
|
+
const projectIdentity = this.buildProjectIdentity();
|
|
19078
|
+
const archRules = this.buildArchRuleSummary();
|
|
19079
|
+
const activeViolations = this.ruleResult?.violations || [];
|
|
19080
|
+
const codebaseDNA = this.buildDNASummary();
|
|
19081
|
+
const fileContexts = this.buildFileContexts();
|
|
19082
|
+
const taskPlaybooks = this.buildTaskPlaybooks();
|
|
19083
|
+
const verificationSteps = this.buildVerificationSteps();
|
|
19084
|
+
const riskBriefing = this.buildRiskBriefing();
|
|
19085
|
+
return {
|
|
19086
|
+
version: "2.0.0",
|
|
19087
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19088
|
+
projectIdentity,
|
|
19089
|
+
architecturalRules: archRules,
|
|
19090
|
+
activeViolations,
|
|
19091
|
+
codebaseDNA,
|
|
19092
|
+
fileContexts,
|
|
19093
|
+
taskPlaybooks,
|
|
19094
|
+
verificationSteps,
|
|
19095
|
+
riskBriefing
|
|
19096
|
+
};
|
|
19270
19097
|
}
|
|
19271
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
19272
|
-
// CLEANUP
|
|
19273
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
19274
19098
|
/**
|
|
19275
|
-
*
|
|
19099
|
+
* Generate context for a specific file — what an AI agent needs to know
|
|
19100
|
+
* before editing this file.
|
|
19276
19101
|
*/
|
|
19277
|
-
|
|
19278
|
-
this.
|
|
19102
|
+
synthesizeForFile(filePath) {
|
|
19103
|
+
const rel = this.toRelative(filePath);
|
|
19104
|
+
const file = this.data.files.find((f) => f.relativePath === rel || f.path === filePath);
|
|
19105
|
+
if (!file) return null;
|
|
19106
|
+
return this.buildSingleFileContext(file);
|
|
19279
19107
|
}
|
|
19280
19108
|
/**
|
|
19281
|
-
*
|
|
19109
|
+
* Generate a compact markdown context document for IDE rules.
|
|
19110
|
+
* This replaces the old static rule generation with intelligence-driven context.
|
|
19282
19111
|
*/
|
|
19283
|
-
|
|
19284
|
-
this.
|
|
19285
|
-
|
|
19286
|
-
|
|
19287
|
-
|
|
19288
|
-
|
|
19289
|
-
|
|
19290
|
-
|
|
19291
|
-
|
|
19292
|
-
|
|
19293
|
-
|
|
19294
|
-
|
|
19295
|
-
|
|
19296
|
-
|
|
19297
|
-
|
|
19298
|
-
|
|
19299
|
-
|
|
19300
|
-
cwd: this.rootPath,
|
|
19301
|
-
ignore: this.config.excludePatterns,
|
|
19302
|
-
absolute: false,
|
|
19303
|
-
dot: false,
|
|
19304
|
-
onlyFiles: true
|
|
19305
|
-
});
|
|
19306
|
-
const hashes = [];
|
|
19307
|
-
const limit = this.config.maxFiles;
|
|
19308
|
-
for (const relPath of files.slice(0, limit)) {
|
|
19309
|
-
const absPath = join(this.rootPath, relPath);
|
|
19310
|
-
try {
|
|
19311
|
-
const fileStat = await stat(absPath);
|
|
19312
|
-
if (fileStat.size > this.config.maxFileSize) continue;
|
|
19313
|
-
const content = await readFile(absPath, "utf-8");
|
|
19314
|
-
hashes.push({
|
|
19315
|
-
relativePath: relPath.replace(/\\/g, "/"),
|
|
19316
|
-
contentHash: hashContent2(content),
|
|
19317
|
-
sizeBytes: fileStat.size,
|
|
19318
|
-
modifiedMs: Math.floor(fileStat.mtimeMs)
|
|
19319
|
-
});
|
|
19320
|
-
} catch {
|
|
19112
|
+
generateContextDocument() {
|
|
19113
|
+
const ctx = this.synthesize();
|
|
19114
|
+
const lines = [];
|
|
19115
|
+
lines.push(`# ${ctx.projectIdentity.name} \u2014 AI Context`);
|
|
19116
|
+
lines.push(`<!-- Generated by @repo/context-engine at ${ctx.generatedAt} -->`);
|
|
19117
|
+
lines.push("");
|
|
19118
|
+
lines.push("## Project Identity");
|
|
19119
|
+
lines.push(`- **Stack**: ${ctx.projectIdentity.stack}`);
|
|
19120
|
+
lines.push(`- **Architecture**: ${ctx.projectIdentity.architecture}`);
|
|
19121
|
+
if (ctx.projectIdentity.keyPatterns.length > 0) {
|
|
19122
|
+
lines.push(`- **Key Patterns**: ${ctx.projectIdentity.keyPatterns.join(", ")}`);
|
|
19123
|
+
}
|
|
19124
|
+
lines.push("");
|
|
19125
|
+
if (ctx.codebaseDNA.conventions.length > 0) {
|
|
19126
|
+
lines.push("## Conventions (Auto-Discovered)");
|
|
19127
|
+
for (const conv of ctx.codebaseDNA.conventions) {
|
|
19128
|
+
lines.push(`- ${conv}`);
|
|
19321
19129
|
}
|
|
19130
|
+
lines.push("");
|
|
19322
19131
|
}
|
|
19323
|
-
|
|
19324
|
-
|
|
19325
|
-
|
|
19326
|
-
|
|
19327
|
-
|
|
19328
|
-
|
|
19329
|
-
|
|
19330
|
-
|
|
19331
|
-
|
|
19332
|
-
|
|
19333
|
-
modifiedMs: row.modified_ms
|
|
19334
|
-
});
|
|
19132
|
+
if (ctx.architecturalRules.length > 0) {
|
|
19133
|
+
lines.push("## Architecture Rules");
|
|
19134
|
+
for (const rule of ctx.architecturalRules) {
|
|
19135
|
+
const icon = rule.severity === "error" ? "MUST" : rule.severity === "warning" ? "SHOULD" : "MAY";
|
|
19136
|
+
lines.push(`- **[${icon}]** ${rule.name}: ${rule.description}`);
|
|
19137
|
+
if (rule.violationCount > 0) {
|
|
19138
|
+
lines.push(` - ${rule.violationCount} active violations`);
|
|
19139
|
+
}
|
|
19140
|
+
}
|
|
19141
|
+
lines.push("");
|
|
19335
19142
|
}
|
|
19336
|
-
|
|
19337
|
-
|
|
19338
|
-
|
|
19339
|
-
|
|
19340
|
-
|
|
19341
|
-
|
|
19342
|
-
|
|
19343
|
-
|
|
19344
|
-
"
|
|
19345
|
-
|
|
19346
|
-
|
|
19347
|
-
|
|
19348
|
-
|
|
19349
|
-
|
|
19350
|
-
|
|
19351
|
-
|
|
19352
|
-
|
|
19353
|
-
|
|
19354
|
-
|
|
19355
|
-
|
|
19356
|
-
|
|
19357
|
-
|
|
19358
|
-
|
|
19359
|
-
|
|
19360
|
-
|
|
19361
|
-
|
|
19362
|
-
|
|
19363
|
-
|
|
19364
|
-
|
|
19365
|
-
|
|
19366
|
-
|
|
19367
|
-
|
|
19368
|
-
|
|
19369
|
-
|
|
19370
|
-
);
|
|
19371
|
-
for (const
|
|
19372
|
-
|
|
19373
|
-
sym.id,
|
|
19374
|
-
sym.name,
|
|
19375
|
-
sym.kind,
|
|
19376
|
-
sym.filePath,
|
|
19377
|
-
sym.startLine,
|
|
19378
|
-
sym.endLine,
|
|
19379
|
-
sym.exported ? 1 : 0,
|
|
19380
|
-
sym.async ? 1 : 0,
|
|
19381
|
-
sym.params ?? null,
|
|
19382
|
-
sym.branches ?? null,
|
|
19383
|
-
sym.signature ?? null
|
|
19384
|
-
);
|
|
19143
|
+
if (ctx.codebaseDNA.boundaries.length > 0) {
|
|
19144
|
+
lines.push("## Module Boundaries");
|
|
19145
|
+
for (const boundary of ctx.codebaseDNA.boundaries) {
|
|
19146
|
+
lines.push(`- ${boundary}`);
|
|
19147
|
+
}
|
|
19148
|
+
lines.push("");
|
|
19149
|
+
}
|
|
19150
|
+
if (ctx.codebaseDNA.hotFiles.length > 0) {
|
|
19151
|
+
lines.push("## High-Impact Files (Edit With Care)");
|
|
19152
|
+
for (const file of ctx.codebaseDNA.hotFiles.slice(0, 10)) {
|
|
19153
|
+
lines.push(`- \`${file}\``);
|
|
19154
|
+
}
|
|
19155
|
+
lines.push("");
|
|
19156
|
+
}
|
|
19157
|
+
if (ctx.riskBriefing.securityConcerns.length > 0 || ctx.riskBriefing.testGaps.length > 0) {
|
|
19158
|
+
lines.push("## Risk Areas");
|
|
19159
|
+
for (const concern of ctx.riskBriefing.securityConcerns) {
|
|
19160
|
+
lines.push(`- ${concern}`);
|
|
19161
|
+
}
|
|
19162
|
+
for (const gap of ctx.riskBriefing.testGaps.slice(0, 5)) {
|
|
19163
|
+
lines.push(`- ${gap}`);
|
|
19164
|
+
}
|
|
19165
|
+
lines.push("");
|
|
19166
|
+
}
|
|
19167
|
+
if (ctx.projectIdentity.noGoZones.length > 0) {
|
|
19168
|
+
lines.push("## No-Go Zones");
|
|
19169
|
+
for (const zone of ctx.projectIdentity.noGoZones) {
|
|
19170
|
+
lines.push(`- ${zone}`);
|
|
19171
|
+
}
|
|
19172
|
+
lines.push("");
|
|
19173
|
+
}
|
|
19174
|
+
if (ctx.taskPlaybooks.length > 0) {
|
|
19175
|
+
lines.push("## Task Playbooks");
|
|
19176
|
+
for (const playbook of ctx.taskPlaybooks) {
|
|
19177
|
+
lines.push(`### ${playbook.taskType}`);
|
|
19178
|
+
for (const step of playbook.steps) {
|
|
19179
|
+
lines.push(`1. ${step}`);
|
|
19385
19180
|
}
|
|
19386
|
-
|
|
19387
|
-
|
|
19388
|
-
imp.fileId,
|
|
19389
|
-
imp.filePath,
|
|
19390
|
-
imp.sourcePath,
|
|
19391
|
-
imp.resolvedPath,
|
|
19392
|
-
JSON.stringify(imp.importedSymbols),
|
|
19393
|
-
imp.isTypeOnly ? 1 : 0,
|
|
19394
|
-
imp.isDynamic ? 1 : 0,
|
|
19395
|
-
imp.line
|
|
19396
|
-
);
|
|
19181
|
+
if (playbook.mustVerify.length > 0) {
|
|
19182
|
+
lines.push(`**Verify**: ${playbook.mustVerify.join(", ")}`);
|
|
19397
19183
|
}
|
|
19398
|
-
|
|
19399
|
-
|
|
19400
|
-
|
|
19401
|
-
|
|
19402
|
-
|
|
19403
|
-
|
|
19404
|
-
|
|
19405
|
-
|
|
19406
|
-
);
|
|
19184
|
+
lines.push("");
|
|
19185
|
+
}
|
|
19186
|
+
}
|
|
19187
|
+
if (ctx.verificationSteps.length > 0) {
|
|
19188
|
+
lines.push("## Verification Protocol");
|
|
19189
|
+
for (const step of ctx.verificationSteps) {
|
|
19190
|
+
lines.push(`### On ${step.trigger}`);
|
|
19191
|
+
for (const check of step.checks) {
|
|
19192
|
+
lines.push(`- ${check}`);
|
|
19407
19193
|
}
|
|
19194
|
+
if (step.commands.length > 0) {
|
|
19195
|
+
lines.push(`**Run**: \`${step.commands.join(" && ")}\``);
|
|
19196
|
+
}
|
|
19197
|
+
lines.push("");
|
|
19408
19198
|
}
|
|
19409
|
-
}
|
|
19410
|
-
|
|
19199
|
+
}
|
|
19200
|
+
lines.push("## Codebase Health");
|
|
19201
|
+
lines.push(`- **Overall**: ${this.dna.healthScore.overall}/100`);
|
|
19202
|
+
const dims = this.dna.healthScore.dimensions;
|
|
19203
|
+
lines.push(`- Architecture: ${dims.architecture} | Tests: ${dims.testCoverage} | Conventions: ${dims.conventions} | Dependencies: ${dims.dependencies}`);
|
|
19204
|
+
lines.push("");
|
|
19205
|
+
lines.push("---");
|
|
19206
|
+
lines.push("<!-- context-engine:v2 -->");
|
|
19207
|
+
return lines.join("\n");
|
|
19411
19208
|
}
|
|
19412
|
-
|
|
19413
|
-
|
|
19414
|
-
|
|
19415
|
-
|
|
19416
|
-
|
|
19417
|
-
|
|
19418
|
-
|
|
19419
|
-
|
|
19420
|
-
|
|
19421
|
-
|
|
19422
|
-
|
|
19423
|
-
|
|
19424
|
-
|
|
19425
|
-
|
|
19209
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
19210
|
+
// IDENTITY
|
|
19211
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
19212
|
+
buildProjectIdentity() {
|
|
19213
|
+
const fp = this.dna.fingerprint;
|
|
19214
|
+
const stack = [fp.framework, fp.language, fp.orm, fp.validator, fp.authLib, fp.router].filter(Boolean).join(" | ");
|
|
19215
|
+
const keyPatterns = this.dna.patterns.map((p) => p.name);
|
|
19216
|
+
const criticalPaths = this.dna.hotspots.slice(0, 5).map((h) => h.file);
|
|
19217
|
+
const noGoZones = [];
|
|
19218
|
+
if (this.ruleResult) {
|
|
19219
|
+
const errorRules = this.ruleResult.violations.filter((v) => v.severity === "error");
|
|
19220
|
+
const uniqueMessages = [...new Set(errorRules.map((v) => v.message))];
|
|
19221
|
+
noGoZones.push(...uniqueMessages.slice(0, 5));
|
|
19222
|
+
}
|
|
19223
|
+
for (const cycle of this.graph.cycles) {
|
|
19224
|
+
noGoZones.push(`Circular dependency: ${cycle.nodes.slice(0, 3).join(" \u2192 ")}...`);
|
|
19225
|
+
}
|
|
19226
|
+
const architecture = this.dna.conventions.filter((c) => c.area === "structure").map((c) => c.description).join("; ") || fp.framework;
|
|
19227
|
+
return {
|
|
19228
|
+
name: fp.name,
|
|
19229
|
+
stack,
|
|
19230
|
+
architecture,
|
|
19231
|
+
keyPatterns,
|
|
19232
|
+
criticalPaths,
|
|
19233
|
+
noGoZones: noGoZones.slice(0, 10)
|
|
19234
|
+
};
|
|
19426
19235
|
}
|
|
19427
19236
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
19428
|
-
//
|
|
19237
|
+
// RULES SUMMARY
|
|
19429
19238
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
19430
|
-
|
|
19431
|
-
|
|
19432
|
-
|
|
19433
|
-
|
|
19434
|
-
|
|
19435
|
-
|
|
19436
|
-
|
|
19437
|
-
|
|
19438
|
-
|
|
19439
|
-
|
|
19440
|
-
|
|
19239
|
+
buildArchRuleSummary() {
|
|
19240
|
+
if (!this.ruleResult) return [];
|
|
19241
|
+
const breakdown = this.ruleResult.ruleBreakdown;
|
|
19242
|
+
return Object.entries(breakdown).map(([ruleId, count]) => {
|
|
19243
|
+
const violation = this.ruleResult.violations.find((v) => v.ruleId === ruleId);
|
|
19244
|
+
return {
|
|
19245
|
+
id: ruleId,
|
|
19246
|
+
name: violation?.ruleName || ruleId,
|
|
19247
|
+
type: "import_forbidden",
|
|
19248
|
+
severity: violation?.severity || "warning",
|
|
19249
|
+
scope: violation?.sourceSymbol.filePath || "",
|
|
19250
|
+
description: violation?.message || "",
|
|
19251
|
+
violationCount: count
|
|
19252
|
+
};
|
|
19253
|
+
});
|
|
19441
19254
|
}
|
|
19442
|
-
|
|
19443
|
-
|
|
19444
|
-
|
|
19445
|
-
|
|
19446
|
-
|
|
19447
|
-
|
|
19448
|
-
|
|
19449
|
-
|
|
19450
|
-
|
|
19451
|
-
|
|
19452
|
-
|
|
19453
|
-
params: row.params ?? void 0,
|
|
19454
|
-
branches: row.branches ?? void 0
|
|
19455
|
-
}));
|
|
19255
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
19256
|
+
// DNA SUMMARY
|
|
19257
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
19258
|
+
buildDNASummary() {
|
|
19259
|
+
return {
|
|
19260
|
+
conventions: this.dna.conventions.filter((c) => c.confidence > 0.5).map((c) => c.description),
|
|
19261
|
+
patterns: this.dna.patterns.map((p) => `${p.name}: ${p.description}`),
|
|
19262
|
+
boundaries: this.dna.boundaries.filter((b) => b.importCount > 3).map((b) => `${b.from} \u2192 ${b.to} (${b.importCount} imports${b.isCircular ? ", CIRCULAR" : ""})`),
|
|
19263
|
+
hotFiles: this.dna.hotspots.slice(0, 10).map((h) => h.file),
|
|
19264
|
+
riskAreas: this.dna.riskMap.filter((r) => r.riskLevel === "critical" || r.riskLevel === "high").map((r) => `${r.file}: ${r.factors[0]}`)
|
|
19265
|
+
};
|
|
19456
19266
|
}
|
|
19457
|
-
|
|
19458
|
-
|
|
19459
|
-
|
|
19460
|
-
|
|
19461
|
-
|
|
19462
|
-
|
|
19463
|
-
|
|
19464
|
-
|
|
19465
|
-
|
|
19466
|
-
exported: row.exported === 1,
|
|
19467
|
-
async: row.async === 1,
|
|
19468
|
-
params: row.params ?? void 0,
|
|
19469
|
-
branches: row.branches ?? void 0
|
|
19470
|
-
}));
|
|
19267
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
19268
|
+
// FILE CONTEXTS
|
|
19269
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
19270
|
+
buildFileContexts() {
|
|
19271
|
+
const contexts = /* @__PURE__ */ new Map();
|
|
19272
|
+
for (const file of this.data.files) {
|
|
19273
|
+
contexts.set(file.relativePath, this.buildSingleFileContext(file));
|
|
19274
|
+
}
|
|
19275
|
+
return contexts;
|
|
19471
19276
|
}
|
|
19472
|
-
|
|
19473
|
-
const
|
|
19474
|
-
|
|
19475
|
-
|
|
19476
|
-
|
|
19477
|
-
|
|
19478
|
-
|
|
19479
|
-
|
|
19480
|
-
|
|
19481
|
-
|
|
19482
|
-
|
|
19483
|
-
|
|
19277
|
+
buildSingleFileContext(file) {
|
|
19278
|
+
const rel = file.relativePath;
|
|
19279
|
+
const role = this.classifyRole(file);
|
|
19280
|
+
const graphNode = this.graph.nodes.find((n) => n.relativePath === rel);
|
|
19281
|
+
const layer = graphNode?.layer;
|
|
19282
|
+
const dependsOn = this.graph.edges.filter((e) => e.from === rel).map((e) => e.to);
|
|
19283
|
+
const dependedOnBy = this.graph.edges.filter((e) => e.to === rel).map((e) => e.from);
|
|
19284
|
+
const applicableRules = [];
|
|
19285
|
+
if (this.ruleResult) {
|
|
19286
|
+
for (const v of this.ruleResult.violations) {
|
|
19287
|
+
if (v.sourceSymbol.filePath.includes(rel) || v.targetSymbol && v.targetSymbol.filePath.includes(rel)) {
|
|
19288
|
+
if (!applicableRules.includes(v.ruleId)) applicableRules.push(v.ruleId);
|
|
19289
|
+
}
|
|
19290
|
+
}
|
|
19291
|
+
}
|
|
19292
|
+
const conventions = this.dna.conventions.filter((c) => this.conventionAppliesToFile(c.area, file)).map((c) => c.description);
|
|
19293
|
+
const patterns = this.dna.patterns.filter((p) => p.fileMatches.some((m) => m === rel)).map((p) => p.name);
|
|
19294
|
+
const riskEntry = this.dna.riskMap.find((r) => r.file === rel);
|
|
19295
|
+
const riskLevel = riskEntry?.riskLevel || "low";
|
|
19296
|
+
const relatedFiles = this.findRelatedFiles(file, role).slice(0, 8);
|
|
19297
|
+
const editGuidance = this.generateEditGuidance(file, role, layer, dependedOnBy, conventions);
|
|
19298
|
+
return {
|
|
19299
|
+
filePath: file.path,
|
|
19300
|
+
role,
|
|
19301
|
+
layer,
|
|
19302
|
+
dependsOn,
|
|
19303
|
+
dependedOnBy,
|
|
19304
|
+
applicableRules,
|
|
19305
|
+
conventions,
|
|
19306
|
+
patterns,
|
|
19307
|
+
riskLevel,
|
|
19308
|
+
relatedFiles,
|
|
19309
|
+
editGuidance
|
|
19310
|
+
};
|
|
19484
19311
|
}
|
|
19485
|
-
|
|
19486
|
-
const
|
|
19487
|
-
|
|
19488
|
-
|
|
19489
|
-
|
|
19490
|
-
|
|
19491
|
-
|
|
19492
|
-
|
|
19493
|
-
|
|
19494
|
-
|
|
19312
|
+
classifyRole(file) {
|
|
19313
|
+
const rel = file.relativePath.toLowerCase();
|
|
19314
|
+
if (rel.includes(".test.") || rel.includes(".spec.") || rel.includes("__tests__")) return "test";
|
|
19315
|
+
if (rel.includes("fixture") || rel.includes("mock")) return "fixture";
|
|
19316
|
+
if (rel.includes("migration")) return "migration";
|
|
19317
|
+
if (rel.match(/\.(css|scss|less|styl)$/)) return "style";
|
|
19318
|
+
if (rel.includes(".config.") || rel.includes("config/") || rel === "tsconfig.json") return "config";
|
|
19319
|
+
if (rel.includes("middleware")) return "middleware";
|
|
19320
|
+
if (rel.includes("/api/") || rel.includes("route")) return "route-handler";
|
|
19321
|
+
if (rel.includes("service") || rel.includes("Service")) return "service";
|
|
19322
|
+
if (rel.includes("repositor") || rel.includes("Repositor")) return "repository";
|
|
19323
|
+
if (rel.endsWith(".tsx") && !rel.includes("page.")) return "component";
|
|
19324
|
+
if (rel.includes("/types") || rel.endsWith(".d.ts")) return "type";
|
|
19325
|
+
if (rel.includes("util") || rel.includes("helper") || rel.includes("lib/")) return "util";
|
|
19326
|
+
if (rel.includes("script") || rel.includes("bin/")) return "script";
|
|
19327
|
+
if (rel.match(/^(src\/)?index\.|^(src\/)?main\.|^(src\/)?app\./)) return "entry";
|
|
19328
|
+
return "unknown";
|
|
19495
19329
|
}
|
|
19496
|
-
|
|
19497
|
-
|
|
19498
|
-
|
|
19499
|
-
|
|
19500
|
-
|
|
19501
|
-
|
|
19502
|
-
|
|
19503
|
-
|
|
19504
|
-
|
|
19505
|
-
|
|
19506
|
-
|
|
19330
|
+
conventionAppliesToFile(area, file) {
|
|
19331
|
+
switch (area) {
|
|
19332
|
+
case "naming":
|
|
19333
|
+
return true;
|
|
19334
|
+
case "imports":
|
|
19335
|
+
return file.relativePath.endsWith(".ts") || file.relativePath.endsWith(".tsx");
|
|
19336
|
+
case "exports":
|
|
19337
|
+
return file.exports.length > 0;
|
|
19338
|
+
case "testing":
|
|
19339
|
+
return file.relativePath.includes(".test.") || file.relativePath.includes(".spec.");
|
|
19340
|
+
case "error-handling":
|
|
19341
|
+
return !file.relativePath.includes(".test.");
|
|
19342
|
+
case "types":
|
|
19343
|
+
return file.relativePath.endsWith(".ts") || file.relativePath.endsWith(".tsx");
|
|
19344
|
+
default:
|
|
19345
|
+
return true;
|
|
19346
|
+
}
|
|
19507
19347
|
}
|
|
19508
|
-
|
|
19509
|
-
const
|
|
19510
|
-
|
|
19511
|
-
|
|
19512
|
-
|
|
19513
|
-
|
|
19514
|
-
|
|
19348
|
+
findRelatedFiles(file, role) {
|
|
19349
|
+
const related = [];
|
|
19350
|
+
const dir = path2.dirname(file.relativePath);
|
|
19351
|
+
for (const other of this.data.files) {
|
|
19352
|
+
if (other.path === file.path) continue;
|
|
19353
|
+
if (path2.dirname(other.relativePath) === dir) {
|
|
19354
|
+
related.push(other.relativePath);
|
|
19355
|
+
}
|
|
19356
|
+
}
|
|
19357
|
+
if (related.length < 5) {
|
|
19358
|
+
for (const other of this.data.files) {
|
|
19359
|
+
if (other.path === file.path) continue;
|
|
19360
|
+
if (related.includes(other.relativePath)) continue;
|
|
19361
|
+
if (this.classifyRole(other) === role) {
|
|
19362
|
+
related.push(other.relativePath);
|
|
19363
|
+
if (related.length >= 8) break;
|
|
19364
|
+
}
|
|
19365
|
+
}
|
|
19366
|
+
}
|
|
19367
|
+
return related;
|
|
19515
19368
|
}
|
|
19516
|
-
|
|
19517
|
-
|
|
19518
|
-
|
|
19519
|
-
}
|
|
19520
|
-
|
|
19521
|
-
|
|
19522
|
-
|
|
19523
|
-
|
|
19524
|
-
|
|
19525
|
-
|
|
19526
|
-
|
|
19527
|
-
|
|
19528
|
-
|
|
19529
|
-
|
|
19530
|
-
|
|
19531
|
-
|
|
19532
|
-
|
|
19533
|
-
|
|
19534
|
-
|
|
19535
|
-
|
|
19536
|
-
|
|
19537
|
-
|
|
19538
|
-
|
|
19539
|
-
|
|
19540
|
-
|
|
19541
|
-
|
|
19542
|
-
|
|
19543
|
-
|
|
19544
|
-
|
|
19545
|
-
|
|
19546
|
-
|
|
19547
|
-
|
|
19548
|
-
|
|
19549
|
-
|
|
19550
|
-
|
|
19551
|
-
|
|
19552
|
-
|
|
19369
|
+
generateEditGuidance(file, role, layer, dependedOnBy, conventions) {
|
|
19370
|
+
const guidance = [];
|
|
19371
|
+
if (dependedOnBy.length > 10) {
|
|
19372
|
+
guidance.push(`HIGH IMPACT: ${dependedOnBy.length} files depend on this. Changes have wide blast radius.`);
|
|
19373
|
+
}
|
|
19374
|
+
switch (role) {
|
|
19375
|
+
case "route-handler":
|
|
19376
|
+
guidance.push("Validate all inputs with schemas before processing.");
|
|
19377
|
+
guidance.push("Return consistent response shapes ({ success, data } or { success, error }).");
|
|
19378
|
+
guidance.push("Ensure authentication middleware is applied to protected endpoints.");
|
|
19379
|
+
break;
|
|
19380
|
+
case "service":
|
|
19381
|
+
guidance.push("Keep business logic here, not in controllers/routes.");
|
|
19382
|
+
guidance.push("Use dependency injection for testability.");
|
|
19383
|
+
if (layer) guidance.push(`This is in the ${layer} layer \u2014 only import from lower layers.`);
|
|
19384
|
+
break;
|
|
19385
|
+
case "repository":
|
|
19386
|
+
guidance.push("Only data access logic belongs here \u2014 no business rules.");
|
|
19387
|
+
guidance.push("Return domain objects, not raw database rows.");
|
|
19388
|
+
break;
|
|
19389
|
+
case "component":
|
|
19390
|
+
guidance.push("Keep components focused and composable.");
|
|
19391
|
+
guidance.push("Extract complex logic to custom hooks.");
|
|
19392
|
+
break;
|
|
19393
|
+
case "middleware":
|
|
19394
|
+
guidance.push("Middleware must call next() or return a response \u2014 never leave the request hanging.");
|
|
19395
|
+
guidance.push("Keep middleware focused on a single concern.");
|
|
19396
|
+
break;
|
|
19397
|
+
case "test":
|
|
19398
|
+
guidance.push("Follow Arrange-Act-Assert pattern.");
|
|
19399
|
+
guidance.push("Test edge cases and error conditions, not just happy path.");
|
|
19400
|
+
break;
|
|
19401
|
+
}
|
|
19402
|
+
for (const conv of conventions.slice(0, 3)) {
|
|
19403
|
+
guidance.push(`Convention: ${conv}`);
|
|
19404
|
+
}
|
|
19405
|
+
return guidance;
|
|
19553
19406
|
}
|
|
19554
|
-
|
|
19555
|
-
|
|
19556
|
-
|
|
19557
|
-
|
|
19558
|
-
const
|
|
19559
|
-
const
|
|
19560
|
-
|
|
19561
|
-
|
|
19562
|
-
|
|
19563
|
-
|
|
19564
|
-
|
|
19565
|
-
|
|
19566
|
-
|
|
19567
|
-
|
|
19568
|
-
|
|
19569
|
-
|
|
19570
|
-
|
|
19571
|
-
|
|
19572
|
-
|
|
19573
|
-
|
|
19574
|
-
|
|
19407
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
19408
|
+
// TASK PLAYBOOKS
|
|
19409
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
19410
|
+
buildTaskPlaybooks() {
|
|
19411
|
+
const fp = this.dna.fingerprint;
|
|
19412
|
+
const playbooks = [];
|
|
19413
|
+
playbooks.push({
|
|
19414
|
+
taskType: "Bug Fix",
|
|
19415
|
+
steps: [
|
|
19416
|
+
"Reproduce the bug and understand the expected vs actual behavior",
|
|
19417
|
+
"Identify the root cause file(s) using the dependency graph",
|
|
19418
|
+
"Write a failing test that reproduces the bug",
|
|
19419
|
+
"Apply the minimal fix at the root cause",
|
|
19420
|
+
"Verify the fix passes the test and does not break existing tests",
|
|
19421
|
+
"Check that the fix does not violate any architecture rules"
|
|
19422
|
+
],
|
|
19423
|
+
mustRead: this.dna.hotspots.slice(0, 3).map((h) => h.file),
|
|
19424
|
+
mustUpdate: ["The buggy file", "Related test file"],
|
|
19425
|
+
mustVerify: ["All existing tests pass", "New regression test passes", "No new arch rule violations"],
|
|
19426
|
+
stopConditions: ["Never modify tests to make them pass \u2014 fix the code", "Do not change public API signatures without discussion"]
|
|
19427
|
+
});
|
|
19428
|
+
if (fp.router) {
|
|
19429
|
+
playbooks.push({
|
|
19430
|
+
taskType: "Add API Endpoint",
|
|
19431
|
+
steps: [
|
|
19432
|
+
"Check existing routes in truthpack to avoid duplicates",
|
|
19433
|
+
"Create the route handler following existing patterns",
|
|
19434
|
+
"Add input validation using the project validator",
|
|
19435
|
+
"Add authentication middleware if the route is protected",
|
|
19436
|
+
"Write tests for success, validation failure, and auth failure",
|
|
19437
|
+
"Update the truthpack (run vibecheck scan)"
|
|
19438
|
+
],
|
|
19439
|
+
mustRead: ["truthpack/routes.json", ...this.dna.patterns.filter((p) => p.category === "api").map((p) => p.exemplar)],
|
|
19440
|
+
mustUpdate: ["Route file", "Test file", "Truthpack"],
|
|
19441
|
+
mustVerify: ["Route responds correctly", "Input validation works", "Auth is enforced", "Test passes"],
|
|
19442
|
+
stopConditions: ["Do not create duplicate routes", "Do not hardcode mock data in handlers"]
|
|
19443
|
+
});
|
|
19444
|
+
}
|
|
19445
|
+
if (fp.framework.includes("Next") || fp.framework.includes("React")) {
|
|
19446
|
+
playbooks.push({
|
|
19447
|
+
taskType: "Add UI Component",
|
|
19448
|
+
steps: [
|
|
19449
|
+
"Check if a similar component already exists",
|
|
19450
|
+
"Create the component following existing naming and structure patterns",
|
|
19451
|
+
"Add TypeScript props interface",
|
|
19452
|
+
"Add unit test for the component",
|
|
19453
|
+
"If using state, determine if it should be a client component"
|
|
19454
|
+
],
|
|
19455
|
+
mustRead: this.dna.patterns.filter((p) => p.category === "ui" || p.category === "state").map((p) => p.exemplar),
|
|
19456
|
+
mustUpdate: ["Component file", "Test file", "Parent component that uses it"],
|
|
19457
|
+
mustVerify: ["Component renders correctly", "Props are typed", "Test passes"],
|
|
19458
|
+
stopConditions: ['Do not use "any" type for props', 'Do not add useState in server components without "use client"']
|
|
19459
|
+
});
|
|
19460
|
+
}
|
|
19461
|
+
playbooks.push({
|
|
19462
|
+
taskType: "Refactor",
|
|
19463
|
+
steps: [
|
|
19464
|
+
"Identify all callers/dependents of the code being refactored",
|
|
19465
|
+
"Ensure comprehensive tests exist before refactoring",
|
|
19466
|
+
"Apply changes incrementally, testing after each step",
|
|
19467
|
+
"Update all dependents to use the new API",
|
|
19468
|
+
"Remove old code only after all dependents are migrated",
|
|
19469
|
+
"Verify no architecture rules are violated"
|
|
19470
|
+
],
|
|
19471
|
+
mustRead: ["Dependency graph for affected files"],
|
|
19472
|
+
mustUpdate: ["Refactored file", "All dependent files", "Tests"],
|
|
19473
|
+
mustVerify: ["All tests pass", "No new violations", "No regressions"],
|
|
19474
|
+
stopConditions: ["Never break existing public APIs without migration path", "Do not refactor and add features in the same change"]
|
|
19475
|
+
});
|
|
19476
|
+
return playbooks;
|
|
19575
19477
|
}
|
|
19576
19478
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
19577
|
-
//
|
|
19479
|
+
// VERIFICATION STEPS
|
|
19578
19480
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
19579
|
-
|
|
19580
|
-
|
|
19581
|
-
const
|
|
19582
|
-
|
|
19583
|
-
|
|
19584
|
-
|
|
19481
|
+
buildVerificationSteps() {
|
|
19482
|
+
const fp = this.dna.fingerprint;
|
|
19483
|
+
const steps = [];
|
|
19484
|
+
if (fp.language === "TypeScript") {
|
|
19485
|
+
steps.push({
|
|
19486
|
+
trigger: "Any TypeScript file change",
|
|
19487
|
+
checks: ["TypeScript compilation succeeds", "No new type errors introduced"],
|
|
19488
|
+
commands: [fp.packageManager === "pnpm" ? "pnpm run check-types" : "npm run check-types"],
|
|
19489
|
+
artifacts: []
|
|
19490
|
+
});
|
|
19585
19491
|
}
|
|
19586
|
-
if (
|
|
19587
|
-
|
|
19492
|
+
if (fp.testRunner) {
|
|
19493
|
+
steps.push({
|
|
19494
|
+
trigger: "Any source file change",
|
|
19495
|
+
checks: ["Related tests pass", "No test regressions"],
|
|
19496
|
+
commands: [`${fp.packageManager} run test`],
|
|
19497
|
+
artifacts: ["test-results.json"]
|
|
19498
|
+
});
|
|
19588
19499
|
}
|
|
19589
|
-
|
|
19590
|
-
|
|
19591
|
-
|
|
19592
|
-
|
|
19593
|
-
|
|
19500
|
+
if (fp.router) {
|
|
19501
|
+
steps.push({
|
|
19502
|
+
trigger: "Route handler added or modified",
|
|
19503
|
+
checks: ["Route responds with correct status", "Auth middleware is applied", "Input validation works"],
|
|
19504
|
+
commands: ["vibecheck scan"],
|
|
19505
|
+
artifacts: ["truthpack/routes.json"]
|
|
19506
|
+
});
|
|
19507
|
+
}
|
|
19508
|
+
steps.push({
|
|
19509
|
+
trigger: "Any source file change",
|
|
19510
|
+
checks: ["No new architecture rule violations", "No new circular dependencies"],
|
|
19511
|
+
commands: ["vibecheck arch-rules"],
|
|
19512
|
+
artifacts: []
|
|
19513
|
+
});
|
|
19514
|
+
return steps;
|
|
19515
|
+
}
|
|
19516
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
19517
|
+
// RISK BRIEFING
|
|
19518
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
19519
|
+
buildRiskBriefing() {
|
|
19520
|
+
const criticalFiles = this.dna.hotspots.filter((h) => h.score > 30).slice(0, 10).map((h) => h.file);
|
|
19521
|
+
const recentViolations = this.ruleResult?.violations.filter((v) => v.severity === "error").slice(0, 10) || [];
|
|
19522
|
+
const securityConcerns = [];
|
|
19523
|
+
for (const risk of this.dna.riskMap) {
|
|
19524
|
+
if (risk.riskLevel === "critical") {
|
|
19525
|
+
securityConcerns.push(`${risk.file}: ${risk.factors.join(", ")}`);
|
|
19594
19526
|
}
|
|
19595
19527
|
}
|
|
19596
|
-
|
|
19528
|
+
const testGaps = [];
|
|
19529
|
+
const sourceFiles = this.data.files.filter(
|
|
19530
|
+
(f) => !f.relativePath.includes(".test.") && !f.relativePath.includes(".spec.") && (f.relativePath.endsWith(".ts") || f.relativePath.endsWith(".tsx")) && !f.relativePath.includes("config") && !f.relativePath.includes(".d.ts")
|
|
19531
|
+
);
|
|
19532
|
+
const testFiles = this.data.files.filter(
|
|
19533
|
+
(f) => f.relativePath.includes(".test.") || f.relativePath.includes(".spec.")
|
|
19534
|
+
);
|
|
19535
|
+
const testedBases = new Set(testFiles.map(
|
|
19536
|
+
(f) => path2.basename(f.relativePath).replace(/\.(test|spec)\.(ts|tsx|js|jsx)$/, "")
|
|
19537
|
+
));
|
|
19538
|
+
for (const file of sourceFiles) {
|
|
19539
|
+
const baseName = path2.basename(file.relativePath).replace(/\.(ts|tsx|js|jsx)$/, "");
|
|
19540
|
+
if (!testedBases.has(baseName) && file.exports.length > 0) {
|
|
19541
|
+
testGaps.push(`${file.relativePath} has exports but no test file`);
|
|
19542
|
+
}
|
|
19543
|
+
}
|
|
19544
|
+
const driftWarnings = [];
|
|
19545
|
+
for (const cycle of this.graph.cycles) {
|
|
19546
|
+
driftWarnings.push(`Circular dependency: ${cycle.nodes.slice(0, 3).join(" \u2192 ")}${cycle.nodes.length > 3 ? "..." : ""}`);
|
|
19547
|
+
}
|
|
19548
|
+
return {
|
|
19549
|
+
criticalFiles,
|
|
19550
|
+
recentViolations,
|
|
19551
|
+
securityConcerns: securityConcerns.slice(0, 5),
|
|
19552
|
+
testGaps: testGaps.slice(0, 10),
|
|
19553
|
+
driftWarnings: driftWarnings.slice(0, 5)
|
|
19554
|
+
};
|
|
19597
19555
|
}
|
|
19598
19556
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
19599
|
-
//
|
|
19557
|
+
// HELPERS
|
|
19600
19558
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
19601
|
-
|
|
19602
|
-
|
|
19603
|
-
|
|
19604
|
-
return tsSymbols.map((sym, i) => ({
|
|
19605
|
-
id: `sym:${relativePath2}:${sym.name}:${sym.line}`,
|
|
19606
|
-
name: sym.name,
|
|
19607
|
-
kind: mapTreeSitterKind(sym.kind),
|
|
19608
|
-
filePath: absolutePath,
|
|
19609
|
-
startLine: sym.line,
|
|
19610
|
-
endLine: sym.endLine,
|
|
19611
|
-
exported: this.isExported(content, sym.name, language),
|
|
19612
|
-
async: sym.signature.includes("async "),
|
|
19613
|
-
params: this.countParams(sym.signature),
|
|
19614
|
-
branches: void 0,
|
|
19615
|
-
signature: sym.signature
|
|
19616
|
-
}));
|
|
19559
|
+
toRelative(filePath) {
|
|
19560
|
+
if (filePath.startsWith(this.rootPath)) {
|
|
19561
|
+
return filePath.slice(this.rootPath.length + 1).replace(/\\/g, "/");
|
|
19617
19562
|
}
|
|
19618
|
-
return
|
|
19563
|
+
return filePath.replace(/\\/g, "/");
|
|
19619
19564
|
}
|
|
19620
|
-
|
|
19621
|
-
|
|
19622
|
-
|
|
19623
|
-
|
|
19624
|
-
|
|
19625
|
-
|
|
19626
|
-
|
|
19627
|
-
|
|
19565
|
+
};
|
|
19566
|
+
var ContextExplainer = class {
|
|
19567
|
+
/**
|
|
19568
|
+
* Generate a rich explanation for a file.
|
|
19569
|
+
*/
|
|
19570
|
+
explain(filePath, ctx) {
|
|
19571
|
+
const fc = ctx.fileContext;
|
|
19572
|
+
const paragraphs = [];
|
|
19573
|
+
const quickFacts = [];
|
|
19574
|
+
const warnings = [];
|
|
19575
|
+
const relatedFiles = [];
|
|
19576
|
+
const overlays = [];
|
|
19577
|
+
const role = fc?.role ?? "unknown";
|
|
19578
|
+
const roleSummary = this.buildRoleSummary(filePath, role, ctx);
|
|
19579
|
+
if (fc) {
|
|
19580
|
+
const depCount = fc.dependedOnBy.length;
|
|
19581
|
+
const depsOnCount = fc.dependsOn.length;
|
|
19582
|
+
if (depCount > 0 || depsOnCount > 0) {
|
|
19583
|
+
const parts = [];
|
|
19584
|
+
if (depCount > 0) {
|
|
19585
|
+
const critical = depCount > 10 ? " \u2014 changes here have wide blast radius" : "";
|
|
19586
|
+
parts.push(`${depCount} file${depCount > 1 ? "s" : ""} depend on this${critical}`);
|
|
19587
|
+
}
|
|
19588
|
+
if (depsOnCount > 0) {
|
|
19589
|
+
parts.push(`it imports from ${depsOnCount} file${depsOnCount > 1 ? "s" : ""}`);
|
|
19590
|
+
}
|
|
19591
|
+
paragraphs.push({
|
|
19592
|
+
heading: "Dependencies",
|
|
19593
|
+
text: parts.join(". ") + ".",
|
|
19594
|
+
importance: depCount > 10 ? "critical" : depCount > 5 ? "high" : "medium"
|
|
19595
|
+
});
|
|
19596
|
+
}
|
|
19597
|
+
quickFacts.push({ label: "Role", value: role, icon: "layer" });
|
|
19598
|
+
if (fc.layer) quickFacts.push({ label: "Layer", value: fc.layer, icon: "layer" });
|
|
19599
|
+
quickFacts.push({ label: "Dependents", value: String(depCount), icon: "dependency" });
|
|
19600
|
+
quickFacts.push({ label: "Dependencies", value: String(depsOnCount), icon: "dependency" });
|
|
19601
|
+
if (fc.riskLevel === "critical" || fc.riskLevel === "high") {
|
|
19602
|
+
quickFacts.push({ label: "Risk", value: fc.riskLevel.toUpperCase(), icon: "warning" });
|
|
19603
|
+
warnings.push({
|
|
19604
|
+
message: `This file is classified as ${fc.riskLevel} risk`,
|
|
19605
|
+
severity: fc.riskLevel === "critical" ? "error" : "warning",
|
|
19606
|
+
action: "Add comprehensive tests and review carefully before merging changes"
|
|
19607
|
+
});
|
|
19608
|
+
}
|
|
19609
|
+
for (const dep of fc.dependedOnBy.slice(0, 5)) {
|
|
19610
|
+
relatedFiles.push({
|
|
19611
|
+
filePath: dep,
|
|
19612
|
+
reason: `Imports from this file`,
|
|
19613
|
+
relationship: "depended-by",
|
|
19614
|
+
confidence: 0.9
|
|
19615
|
+
});
|
|
19628
19616
|
}
|
|
19617
|
+
overlays.push({
|
|
19618
|
+
type: "code-lens",
|
|
19619
|
+
line: 1,
|
|
19620
|
+
text: `${depCount} dependent${depCount !== 1 ? "s" : ""} \xB7 ${depsOnCount} import${depsOnCount !== 1 ? "s" : ""} \xB7 ${role}`,
|
|
19621
|
+
tooltip: `This ${role} file has ${depCount} files that depend on it and imports from ${depsOnCount} files`
|
|
19622
|
+
});
|
|
19629
19623
|
}
|
|
19630
|
-
if (
|
|
19631
|
-
|
|
19632
|
-
|
|
19633
|
-
|
|
19634
|
-
|
|
19635
|
-
|
|
19636
|
-
} catch {
|
|
19637
|
-
return null;
|
|
19624
|
+
if (fc && fc.conventions.length > 0) {
|
|
19625
|
+
paragraphs.push({
|
|
19626
|
+
heading: "Conventions",
|
|
19627
|
+
text: `Follow these discovered conventions: ${fc.conventions.slice(0, 3).join("; ")}.`,
|
|
19628
|
+
importance: "medium"
|
|
19629
|
+
});
|
|
19638
19630
|
}
|
|
19639
|
-
|
|
19640
|
-
|
|
19641
|
-
|
|
19642
|
-
|
|
19643
|
-
|
|
19644
|
-
|
|
19645
|
-
|
|
19646
|
-
{
|
|
19647
|
-
|
|
19648
|
-
|
|
19649
|
-
|
|
19650
|
-
|
|
19651
|
-
|
|
19652
|
-
|
|
19653
|
-
|
|
19654
|
-
|
|
19655
|
-
|
|
19656
|
-
|
|
19657
|
-
|
|
19658
|
-
|
|
19659
|
-
|
|
19660
|
-
|
|
19661
|
-
|
|
19662
|
-
|
|
19663
|
-
|
|
19664
|
-
exported: this.isExported(content, name, language),
|
|
19665
|
-
async: match2[0].includes("async"),
|
|
19666
|
-
params: this.countParams(match2[0]),
|
|
19667
|
-
branches: this.countBranches(lines, line - 1, endLine - 1),
|
|
19668
|
-
signature: match2[0].trim().replace(/\s*\{?\s*$/, "")
|
|
19631
|
+
if (ctx.rules) {
|
|
19632
|
+
const fileViolations = ctx.rules.violations.filter(
|
|
19633
|
+
(v) => v.sourceSymbol.filePath.includes(shortName(filePath)) || v.targetSymbol && v.targetSymbol.filePath.includes(shortName(filePath))
|
|
19634
|
+
);
|
|
19635
|
+
if (fileViolations.length > 0) {
|
|
19636
|
+
const errors = fileViolations.filter((v) => v.severity === "error");
|
|
19637
|
+
const warns = fileViolations.filter((v) => v.severity === "warning");
|
|
19638
|
+
paragraphs.push({
|
|
19639
|
+
heading: "Architecture Violations",
|
|
19640
|
+
text: `${errors.length} error${errors.length !== 1 ? "s" : ""} and ${warns.length} warning${warns.length !== 1 ? "s" : ""} from architecture rules.`,
|
|
19641
|
+
importance: errors.length > 0 ? "critical" : "high"
|
|
19642
|
+
});
|
|
19643
|
+
for (const v of fileViolations.slice(0, 5)) {
|
|
19644
|
+
warnings.push({
|
|
19645
|
+
message: v.message,
|
|
19646
|
+
severity: v.severity === "error" ? "error" : "warning",
|
|
19647
|
+
action: v.suggestedFix ?? "Review and fix the violation",
|
|
19648
|
+
ruleId: v.ruleId
|
|
19649
|
+
});
|
|
19650
|
+
overlays.push({
|
|
19651
|
+
type: "diagnostic",
|
|
19652
|
+
line: v.sourceSymbol.line,
|
|
19653
|
+
text: v.message,
|
|
19654
|
+
severity: v.severity === "error" ? "error" : "warning",
|
|
19655
|
+
tooltip: v.suggestedFix
|
|
19669
19656
|
});
|
|
19670
19657
|
}
|
|
19671
19658
|
}
|
|
19672
19659
|
}
|
|
19673
|
-
if (
|
|
19674
|
-
const
|
|
19675
|
-
|
|
19676
|
-
|
|
19677
|
-
|
|
19678
|
-
|
|
19679
|
-
|
|
19680
|
-
|
|
19681
|
-
|
|
19682
|
-
|
|
19683
|
-
filePath: absolutePath,
|
|
19684
|
-
startLine: line,
|
|
19685
|
-
endLine: line + 10,
|
|
19686
|
-
exported: true,
|
|
19687
|
-
async: match2[0].includes("async"),
|
|
19688
|
-
params: this.countParams(match2[0]),
|
|
19689
|
-
branches: void 0,
|
|
19690
|
-
signature: match2[0].trim()
|
|
19660
|
+
if (ctx.callGraph) {
|
|
19661
|
+
const fileNodes = ctx.callGraph.nodes.filter((n) => n.filePath.includes(shortName(filePath)));
|
|
19662
|
+
const hotFunctions = fileNodes.filter((n) => n.callerCount > 5);
|
|
19663
|
+
if (hotFunctions.length > 0) {
|
|
19664
|
+
paragraphs.push({
|
|
19665
|
+
heading: "Hot Functions",
|
|
19666
|
+
text: hotFunctions.map(
|
|
19667
|
+
(f) => `\`${f.name}\` is called by ${f.callerCount} function${f.callerCount !== 1 ? "s" : ""}${f.calleeCount > 0 ? ` and calls ${f.calleeCount}` : ""}`
|
|
19668
|
+
).join(". ") + ".",
|
|
19669
|
+
importance: "high"
|
|
19691
19670
|
});
|
|
19671
|
+
for (const fn of hotFunctions) {
|
|
19672
|
+
overlays.push({
|
|
19673
|
+
type: "code-lens",
|
|
19674
|
+
line: 0,
|
|
19675
|
+
// Would need symbol line mapping
|
|
19676
|
+
text: `${fn.callerCount} callers \xB7 ${fn.calleeCount} callees`,
|
|
19677
|
+
tooltip: `Function ${fn.name} has ${fn.callerCount} callers and ${fn.calleeCount} callees`
|
|
19678
|
+
});
|
|
19679
|
+
}
|
|
19680
|
+
}
|
|
19681
|
+
const deadInFile = ctx.callGraph.stats.deadFunctions.filter(
|
|
19682
|
+
(d) => d.filePath.includes(shortName(filePath))
|
|
19683
|
+
);
|
|
19684
|
+
if (deadInFile.length > 0) {
|
|
19685
|
+
for (const dead of deadInFile) {
|
|
19686
|
+
warnings.push({
|
|
19687
|
+
message: `\`${dead.name}\` appears to be dead code (exported but never called)`,
|
|
19688
|
+
severity: "info",
|
|
19689
|
+
action: "Verify this function is not called via dynamic dispatch or external consumers, then consider removing it"
|
|
19690
|
+
});
|
|
19691
|
+
}
|
|
19692
19692
|
}
|
|
19693
19693
|
}
|
|
19694
|
-
|
|
19695
|
-
|
|
19696
|
-
|
|
19697
|
-
|
|
19698
|
-
|
|
19699
|
-
|
|
19700
|
-
|
|
19701
|
-
|
|
19702
|
-
if (language === "typescript" || language === "javascript") {
|
|
19703
|
-
for (const match2 of content.matchAll(TS_IMPORT_RE)) {
|
|
19704
|
-
const sourcePath = match2[1];
|
|
19705
|
-
const line = content.slice(0, match2.index).split("\n").length;
|
|
19706
|
-
const isTypeOnly = TS_IMPORT_TYPE_RE.test(match2[0]);
|
|
19707
|
-
const importedSymbols = this.extractImportedSymbols(match2[0]);
|
|
19708
|
-
const resolvedPath = this.resolveImportPath(sourcePath, absolutePath);
|
|
19709
|
-
imports.push({
|
|
19710
|
-
fileId,
|
|
19711
|
-
filePath: absolutePath,
|
|
19712
|
-
sourcePath,
|
|
19713
|
-
resolvedPath,
|
|
19714
|
-
importedSymbols,
|
|
19715
|
-
isTypeOnly,
|
|
19716
|
-
isDynamic: false,
|
|
19717
|
-
line
|
|
19694
|
+
if (ctx.temporal) {
|
|
19695
|
+
const hotspot = ctx.temporal.changeHotspots.find((h) => filePath.includes(h.file) || h.file.includes(shortName(filePath)));
|
|
19696
|
+
if (hotspot) {
|
|
19697
|
+
const daysSince = Math.round((Date.now() - new Date(hotspot.lastChanged).getTime()) / 864e5);
|
|
19698
|
+
paragraphs.push({
|
|
19699
|
+
heading: "Recent Activity",
|
|
19700
|
+
text: `Changed ${hotspot.commits} times in the last ${ctx.temporal.stats.analysisWindowDays} days by ${hotspot.authors} author${hotspot.authors !== 1 ? "s" : ""}. Last modified ${daysSince} day${daysSince !== 1 ? "s" : ""} ago.`,
|
|
19701
|
+
importance: hotspot.commits > 10 ? "high" : "medium"
|
|
19718
19702
|
});
|
|
19703
|
+
quickFacts.push({ label: "Recent commits", value: String(hotspot.commits), icon: "git" });
|
|
19704
|
+
quickFacts.push({ label: "Last changed", value: `${daysSince}d ago`, icon: "git" });
|
|
19705
|
+
quickFacts.push({ label: "Authors", value: String(hotspot.authors), icon: "git" });
|
|
19719
19706
|
}
|
|
19720
|
-
|
|
19721
|
-
|
|
19722
|
-
|
|
19723
|
-
|
|
19724
|
-
|
|
19725
|
-
|
|
19726
|
-
sourcePath,
|
|
19727
|
-
resolvedPath: this.resolveImportPath(sourcePath, absolutePath),
|
|
19728
|
-
importedSymbols: [],
|
|
19729
|
-
isTypeOnly: false,
|
|
19730
|
-
isDynamic: true,
|
|
19731
|
-
line
|
|
19707
|
+
const churn = ctx.temporal.churnFiles.find((c) => filePath.includes(c.file) || c.file.includes(shortName(filePath)));
|
|
19708
|
+
if (churn && churn.severity !== "low") {
|
|
19709
|
+
warnings.push({
|
|
19710
|
+
message: churn.reason,
|
|
19711
|
+
severity: churn.severity === "high" ? "warning" : "info",
|
|
19712
|
+
action: "Consider whether this file needs refactoring to reduce change frequency"
|
|
19732
19713
|
});
|
|
19733
19714
|
}
|
|
19734
|
-
|
|
19735
|
-
|
|
19736
|
-
|
|
19737
|
-
|
|
19738
|
-
|
|
19739
|
-
|
|
19740
|
-
|
|
19741
|
-
|
|
19742
|
-
filePath: absolutePath,
|
|
19743
|
-
sourcePath,
|
|
19744
|
-
resolvedPath: "",
|
|
19745
|
-
importedSymbols: [],
|
|
19746
|
-
isTypeOnly: false,
|
|
19747
|
-
isDynamic: false,
|
|
19748
|
-
line
|
|
19715
|
+
const expertise = ctx.temporal.authorExpertise.find(
|
|
19716
|
+
(e) => filePath.startsWith(e.area) || filePath.includes(e.area)
|
|
19717
|
+
);
|
|
19718
|
+
if (expertise && expertise.busFactor === 1) {
|
|
19719
|
+
warnings.push({
|
|
19720
|
+
message: `Bus factor of 1 \u2014 ${expertise.primaryAuthor} has made ${expertise.authors[0]?.percentage}% of changes to this area`,
|
|
19721
|
+
severity: "info",
|
|
19722
|
+
action: "Consider knowledge sharing or pair programming for this area"
|
|
19749
19723
|
});
|
|
19750
19724
|
}
|
|
19751
19725
|
}
|
|
19752
|
-
if (
|
|
19753
|
-
|
|
19754
|
-
|
|
19755
|
-
|
|
19756
|
-
|
|
19757
|
-
|
|
19758
|
-
|
|
19759
|
-
|
|
19760
|
-
|
|
19761
|
-
|
|
19762
|
-
sourcePath,
|
|
19763
|
-
resolvedPath: "",
|
|
19764
|
-
importedSymbols: [],
|
|
19765
|
-
isTypeOnly: false,
|
|
19766
|
-
isDynamic: false,
|
|
19767
|
-
line: 0
|
|
19726
|
+
if (ctx.learned) {
|
|
19727
|
+
const coEdits = ctx.learned.coEdits.filter((p) => p.files[0].includes(shortName(filePath)) || p.files[1].includes(shortName(filePath))).slice(0, 3);
|
|
19728
|
+
if (coEdits.length > 0) {
|
|
19729
|
+
for (const pair of coEdits) {
|
|
19730
|
+
const other = pair.files[0].includes(shortName(filePath)) ? pair.files[1] : pair.files[0];
|
|
19731
|
+
relatedFiles.push({
|
|
19732
|
+
filePath: other,
|
|
19733
|
+
reason: `Often edited together (${pair.count} times)`,
|
|
19734
|
+
relationship: "co-edited",
|
|
19735
|
+
confidence: pair.weight
|
|
19768
19736
|
});
|
|
19769
19737
|
}
|
|
19770
19738
|
}
|
|
19771
19739
|
}
|
|
19772
|
-
|
|
19773
|
-
|
|
19774
|
-
|
|
19775
|
-
|
|
19776
|
-
|
|
19777
|
-
|
|
19778
|
-
return defaultMatch?.[1] ? [defaultMatch[1]] : [];
|
|
19740
|
+
if (fc && fc.editGuidance.length > 0) {
|
|
19741
|
+
paragraphs.push({
|
|
19742
|
+
heading: "Edit Guidance",
|
|
19743
|
+
text: fc.editGuidance.join(" "),
|
|
19744
|
+
importance: "medium"
|
|
19745
|
+
});
|
|
19779
19746
|
}
|
|
19780
|
-
return
|
|
19781
|
-
|
|
19782
|
-
|
|
19783
|
-
|
|
19747
|
+
return {
|
|
19748
|
+
filePath,
|
|
19749
|
+
roleSummary,
|
|
19750
|
+
paragraphs,
|
|
19751
|
+
quickFacts,
|
|
19752
|
+
warnings,
|
|
19753
|
+
relatedFiles,
|
|
19754
|
+
overlays
|
|
19755
|
+
};
|
|
19784
19756
|
}
|
|
19785
|
-
|
|
19786
|
-
|
|
19787
|
-
|
|
19788
|
-
|
|
19789
|
-
const
|
|
19790
|
-
|
|
19791
|
-
|
|
19792
|
-
|
|
19793
|
-
|
|
19794
|
-
|
|
19795
|
-
|
|
19757
|
+
/**
|
|
19758
|
+
* Generate a compact markdown explanation for AI agent consumption.
|
|
19759
|
+
*/
|
|
19760
|
+
explainForAgent(filePath, ctx) {
|
|
19761
|
+
const explanation = this.explain(filePath, ctx);
|
|
19762
|
+
const lines = [];
|
|
19763
|
+
lines.push(`## ${shortName(filePath)}: ${explanation.roleSummary}`);
|
|
19764
|
+
lines.push("");
|
|
19765
|
+
if (explanation.quickFacts.length > 0) {
|
|
19766
|
+
lines.push(explanation.quickFacts.map((f) => `**${f.label}**: ${f.value}`).join(" \xB7 "));
|
|
19767
|
+
lines.push("");
|
|
19768
|
+
}
|
|
19769
|
+
for (const p of explanation.paragraphs.filter((p2) => p2.importance === "critical" || p2.importance === "high")) {
|
|
19770
|
+
lines.push(`### ${p.heading}`);
|
|
19771
|
+
lines.push(p.text);
|
|
19772
|
+
lines.push("");
|
|
19773
|
+
}
|
|
19774
|
+
if (explanation.warnings.length > 0) {
|
|
19775
|
+
lines.push("### Warnings");
|
|
19776
|
+
for (const w of explanation.warnings) {
|
|
19777
|
+
const icon = w.severity === "error" ? "MUST FIX" : w.severity === "warning" ? "SHOULD FIX" : "NOTE";
|
|
19778
|
+
lines.push(`- **[${icon}]** ${w.message} \u2014 ${w.action}`);
|
|
19796
19779
|
}
|
|
19780
|
+
lines.push("");
|
|
19797
19781
|
}
|
|
19798
|
-
|
|
19799
|
-
|
|
19800
|
-
|
|
19801
|
-
|
|
19802
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
19803
|
-
extractCallEdges(content, symbols, filePath) {
|
|
19804
|
-
const edges = [];
|
|
19805
|
-
const functionNames = new Set(symbols.filter((s) => s.kind === "function" || s.kind === "method").map((s) => s.name));
|
|
19806
|
-
for (const caller of symbols) {
|
|
19807
|
-
if (caller.kind !== "function" && caller.kind !== "method") continue;
|
|
19808
|
-
const body = content.split("\n").slice(caller.startLine - 1, caller.endLine).join("\n");
|
|
19809
|
-
for (const calleeName of functionNames) {
|
|
19810
|
-
if (calleeName === caller.name) continue;
|
|
19811
|
-
const callRe = new RegExp(`\\b${calleeName}\\s*\\(`, "g");
|
|
19812
|
-
if (callRe.test(body)) {
|
|
19813
|
-
const callee = symbols.find((s) => s.name === calleeName);
|
|
19814
|
-
if (callee) {
|
|
19815
|
-
edges.push({
|
|
19816
|
-
callerId: caller.id,
|
|
19817
|
-
calleeId: callee.id,
|
|
19818
|
-
callerName: caller.name,
|
|
19819
|
-
calleeName: callee.name,
|
|
19820
|
-
callerFile: filePath,
|
|
19821
|
-
calleeFile: filePath
|
|
19822
|
-
});
|
|
19823
|
-
}
|
|
19824
|
-
}
|
|
19782
|
+
if (explanation.relatedFiles.length > 0) {
|
|
19783
|
+
lines.push("### Related Files");
|
|
19784
|
+
for (const rf of explanation.relatedFiles.slice(0, 5)) {
|
|
19785
|
+
lines.push(`- \`${rf.filePath}\` \u2014 ${rf.reason}`);
|
|
19825
19786
|
}
|
|
19787
|
+
lines.push("");
|
|
19826
19788
|
}
|
|
19827
|
-
return
|
|
19789
|
+
return lines.join("\n");
|
|
19790
|
+
}
|
|
19791
|
+
/**
|
|
19792
|
+
* Generate IDE overlay data for the VS Code extension to consume.
|
|
19793
|
+
*/
|
|
19794
|
+
getIDEOverlays(filePath, ctx) {
|
|
19795
|
+
const explanation = this.explain(filePath, ctx);
|
|
19796
|
+
return explanation.overlays;
|
|
19797
|
+
}
|
|
19798
|
+
/**
|
|
19799
|
+
* Generate a health report explanation.
|
|
19800
|
+
*/
|
|
19801
|
+
explainHealth(health, dna) {
|
|
19802
|
+
const lines = [];
|
|
19803
|
+
const dims = health.dimensions;
|
|
19804
|
+
lines.push(`## Codebase Health: ${health.overall}/100`);
|
|
19805
|
+
lines.push("");
|
|
19806
|
+
const entries = [
|
|
19807
|
+
{ name: "Architecture", score: dims.architecture, explain: this.explainArchScore(dims.architecture, dna) },
|
|
19808
|
+
{ name: "Test Coverage", score: dims.testCoverage, explain: this.explainTestScore(dims.testCoverage) },
|
|
19809
|
+
{ name: "Conventions", score: dims.conventions, explain: this.explainConventionScore(dims.conventions, dna) },
|
|
19810
|
+
{ name: "Dependencies", score: dims.dependencies, explain: this.explainDependencyScore(dims.dependencies, dna) },
|
|
19811
|
+
{ name: "Security", score: dims.security, explain: dims.security >= 80 ? "No critical security concerns detected" : "Critical risk areas identified" },
|
|
19812
|
+
{ name: "Complexity", score: dims.complexity, explain: dims.complexity >= 80 ? "Complexity is well-managed" : "High-complexity hotspots detected" }
|
|
19813
|
+
];
|
|
19814
|
+
for (const entry of entries) {
|
|
19815
|
+
const bar = this.renderBar(entry.score);
|
|
19816
|
+
lines.push(`${bar} **${entry.name}**: ${entry.score}/100 \u2014 ${entry.explain}`);
|
|
19817
|
+
}
|
|
19818
|
+
return lines.join("\n");
|
|
19828
19819
|
}
|
|
19829
19820
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
19830
|
-
//
|
|
19821
|
+
// PRIVATE
|
|
19831
19822
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
19832
|
-
|
|
19833
|
-
|
|
19834
|
-
|
|
19835
|
-
|
|
19836
|
-
|
|
19837
|
-
|
|
19823
|
+
buildRoleSummary(filePath, role, ctx) {
|
|
19824
|
+
const parts = [];
|
|
19825
|
+
switch (role) {
|
|
19826
|
+
case "service":
|
|
19827
|
+
parts.push("Business logic service");
|
|
19828
|
+
break;
|
|
19829
|
+
case "route-handler":
|
|
19830
|
+
parts.push("API route handler");
|
|
19831
|
+
break;
|
|
19832
|
+
case "component":
|
|
19833
|
+
parts.push("UI component");
|
|
19834
|
+
break;
|
|
19835
|
+
case "repository":
|
|
19836
|
+
parts.push("Data access layer");
|
|
19837
|
+
break;
|
|
19838
|
+
case "middleware":
|
|
19839
|
+
parts.push("Request middleware");
|
|
19840
|
+
break;
|
|
19841
|
+
case "test":
|
|
19842
|
+
parts.push("Test file");
|
|
19843
|
+
break;
|
|
19844
|
+
case "config":
|
|
19845
|
+
parts.push("Configuration");
|
|
19846
|
+
break;
|
|
19847
|
+
case "type":
|
|
19848
|
+
parts.push("Type definitions");
|
|
19849
|
+
break;
|
|
19850
|
+
case "util":
|
|
19851
|
+
parts.push("Utility module");
|
|
19852
|
+
break;
|
|
19853
|
+
case "entry":
|
|
19854
|
+
parts.push("Entry point");
|
|
19855
|
+
break;
|
|
19856
|
+
default:
|
|
19857
|
+
parts.push("Source file");
|
|
19858
|
+
}
|
|
19859
|
+
if (ctx.fileContext) {
|
|
19860
|
+
if (ctx.fileContext.layer) parts.push(`in ${ctx.fileContext.layer} layer`);
|
|
19861
|
+
if (ctx.fileContext.dependedOnBy.length > 10) parts.push("(high-impact)");
|
|
19862
|
+
}
|
|
19863
|
+
return parts.join(" ");
|
|
19838
19864
|
}
|
|
19839
|
-
|
|
19840
|
-
|
|
19841
|
-
if (
|
|
19842
|
-
return
|
|
19865
|
+
explainArchScore(score, dna) {
|
|
19866
|
+
if (score >= 80) return `Strong architecture with ${dna.patterns.length} recognized patterns`;
|
|
19867
|
+
if (score >= 50) return `Moderate architecture \u2014 ${dna.patterns.length} patterns detected, room to strengthen boundaries`;
|
|
19868
|
+
return "Architecture needs attention \u2014 few recognized patterns or boundaries";
|
|
19843
19869
|
}
|
|
19844
|
-
|
|
19845
|
-
|
|
19846
|
-
|
|
19847
|
-
|
|
19848
|
-
const matches = lines[i].match(branchRe);
|
|
19849
|
-
if (matches) branches += matches.length;
|
|
19850
|
-
}
|
|
19851
|
-
return branches;
|
|
19870
|
+
explainTestScore(score) {
|
|
19871
|
+
if (score >= 80) return "Good test coverage across source files";
|
|
19872
|
+
if (score >= 50) return "Moderate coverage \u2014 some source files lack tests";
|
|
19873
|
+
return "Low test coverage \u2014 many exported modules have no test files";
|
|
19852
19874
|
}
|
|
19853
|
-
|
|
19854
|
-
|
|
19855
|
-
|
|
19856
|
-
|
|
19857
|
-
|
|
19858
|
-
if (ch === "{") {
|
|
19859
|
-
depth++;
|
|
19860
|
-
seenOpen = true;
|
|
19861
|
-
} else if (ch === "}" && seenOpen) {
|
|
19862
|
-
depth--;
|
|
19863
|
-
if (depth <= 0) return i + 1;
|
|
19864
|
-
}
|
|
19865
|
-
}
|
|
19866
|
-
}
|
|
19867
|
-
return startIdx + 1;
|
|
19875
|
+
explainConventionScore(score, dna) {
|
|
19876
|
+
const strong = dna.conventions.filter((c) => c.confidence > 0.6).length;
|
|
19877
|
+
if (score >= 80) return `${strong} strong conventions enforced consistently`;
|
|
19878
|
+
if (score >= 50) return `${strong} conventions detected but inconsistently applied`;
|
|
19879
|
+
return "Few consistent conventions \u2014 codebase style varies across files";
|
|
19868
19880
|
}
|
|
19869
|
-
|
|
19870
|
-
|
|
19871
|
-
|
|
19872
|
-
|
|
19873
|
-
|
|
19874
|
-
class: "class",
|
|
19875
|
-
interface: "interface",
|
|
19876
|
-
type: "type",
|
|
19877
|
-
enum: "enum",
|
|
19878
|
-
const: "variable",
|
|
19879
|
-
variable: "variable",
|
|
19880
|
-
struct: "class",
|
|
19881
|
-
trait: "interface",
|
|
19882
|
-
export: "variable"
|
|
19883
|
-
};
|
|
19884
|
-
return map[kind] ?? "function";
|
|
19885
|
-
}
|
|
19886
|
-
function flattenCodeSymbols(symbols) {
|
|
19887
|
-
const flat = [];
|
|
19888
|
-
for (const sym of symbols) {
|
|
19889
|
-
flat.push({ name: sym.name, kind: sym.kind, line: sym.line, endLine: sym.endLine, signature: sym.signature });
|
|
19890
|
-
if (sym.children && Array.isArray(sym.children)) {
|
|
19891
|
-
flat.push(...flattenCodeSymbols(sym.children));
|
|
19892
|
-
}
|
|
19881
|
+
explainDependencyScore(score, dna) {
|
|
19882
|
+
const circular = dna.boundaries.filter((b) => b.isCircular).length;
|
|
19883
|
+
if (score >= 80) return "Clean dependency graph with no circular dependencies";
|
|
19884
|
+
if (circular > 0) return `${circular} circular dependency${circular > 1 ? "ies" : "y"} detected \u2014 these increase coupling and make code harder to reason about`;
|
|
19885
|
+
return "Dependency health needs improvement";
|
|
19893
19886
|
}
|
|
19894
|
-
|
|
19887
|
+
renderBar(score) {
|
|
19888
|
+
const filled = Math.round(score / 10);
|
|
19889
|
+
return "\u2588".repeat(filled) + "\u2591".repeat(10 - filled);
|
|
19890
|
+
}
|
|
19891
|
+
};
|
|
19892
|
+
function shortName(filePath) {
|
|
19893
|
+
const parts = filePath.split("/");
|
|
19894
|
+
return parts[parts.length - 1] ?? filePath;
|
|
19895
19895
|
}
|
|
19896
19896
|
function normalizePath(p) {
|
|
19897
19897
|
return p.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
@@ -21986,7 +21986,7 @@ function createScanIdempotencyKey(prefix) {
|
|
|
21986
21986
|
// src/mcp-scan-meter-client.ts
|
|
21987
21987
|
var MCP_SCAN_METER_CLIENT = {
|
|
21988
21988
|
type: "mcp",
|
|
21989
|
-
version: "24.6.
|
|
21989
|
+
version: "24.6.7"
|
|
21990
21990
|
};
|
|
21991
21991
|
|
|
21992
21992
|
// ../shared/dist/chunk-YYSV5CG4.js
|
|
@@ -23008,7 +23008,7 @@ function createMcpServer(runtimeOverrides = {}) {
|
|
|
23008
23008
|
const server = new Server(
|
|
23009
23009
|
{
|
|
23010
23010
|
name: "vibecheck-mcp",
|
|
23011
|
-
version: "24.6.
|
|
23011
|
+
version: "24.6.7"
|
|
23012
23012
|
},
|
|
23013
23013
|
{
|
|
23014
23014
|
capabilities: {
|