@skein-code/cli 0.3.18 → 0.3.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -4
- package/dist/cli.js +315 -42
- package/dist/cli.js.map +1 -1
- package/docs/NEXT_STEPS.md +34 -9
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -441,10 +441,24 @@ npm run benchmark:context -- \
|
|
|
441
441
|
```
|
|
442
442
|
|
|
443
443
|
It reports Recall@5/10/20, MRR, useful-token ratio, stale-hit rate, and cold,
|
|
444
|
-
incremental, and warm-query latency.
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
444
|
+
incremental, and warm-query latency. `context-benchmark-v2` covers TypeScript,
|
|
445
|
+
Python, SQL, CJK, Markdown, mixed-language queries, and a TypeScript import
|
|
446
|
+
neighbor that has no direct lexical match. Its checked-in thresholds fail the
|
|
447
|
+
command instead of allowing a retrieval change to lower the quality bar.
|
|
448
|
+
|
|
449
|
+
The persisted local index now stores content-addressed TypeScript compiler AST
|
|
450
|
+
facts for definitions, calls, and relative imports. Matching definitions can
|
|
451
|
+
expand a query and import/call neighbors may receive a bounded graph score;
|
|
452
|
+
Python and SQL continue to use explicit syntax-aware fallbacks. Search and
|
|
453
|
+
context JSON expose the index generation, file hash, matched/expanded terms,
|
|
454
|
+
and bm25/path/symbol/phrase/graph breakdown without persisting the query or an
|
|
455
|
+
additional copy of source. Index schema v2 artifacts rebuild as v3 rather than
|
|
456
|
+
being trusted after the parser contract changes.
|
|
457
|
+
|
|
458
|
+
The included fixture is a deterministic regression gate, not evidence for
|
|
459
|
+
performance on every production repository. `--fresh-index` deletes and
|
|
460
|
+
rebuilds that workspace's local index, so use it only where rebuilding the
|
|
461
|
+
index is intended.
|
|
448
462
|
|
|
449
463
|
## Safety model
|
|
450
464
|
|
package/dist/cli.js
CHANGED
|
@@ -220,7 +220,7 @@ function isSqliteBusy(error) {
|
|
|
220
220
|
// package.json
|
|
221
221
|
var package_default = {
|
|
222
222
|
name: "@skein-code/cli",
|
|
223
|
-
version: "0.3.
|
|
223
|
+
version: "0.3.19",
|
|
224
224
|
description: "A context-first, model-agnostic coding agent with an auditable terminal workspace.",
|
|
225
225
|
type: "module",
|
|
226
226
|
license: "MIT",
|
|
@@ -236,6 +236,9 @@ var package_default = {
|
|
|
236
236
|
},
|
|
237
237
|
skein: {
|
|
238
238
|
releaseNotes: [
|
|
239
|
+
"Local index v3 records TypeScript AST definitions, calls, and relative import adjacency for graph-assisted retrieval",
|
|
240
|
+
"Context hits expose generation, content hash, matched terms, and bm25/path/symbol/phrase/graph score provenance",
|
|
241
|
+
"Context benchmark v2 locks multilingual recall, MRR, useful-token, stale-hit, incremental-index, and warm-latency gates",
|
|
239
242
|
"Large MCP catalogs now disclose at most eight request-relevant schemas per run while preserving network permissions",
|
|
240
243
|
"Token Ledger receipts report deferred progressive schemas and selected definitions remain stable across tool turns",
|
|
241
244
|
"A deterministic Token Economy replay benchmark guards evidence, output-firewall, and no-progress invariants",
|
|
@@ -301,6 +304,7 @@ var package_default = {
|
|
|
301
304
|
react: "^19.2.3",
|
|
302
305
|
"string-width": "^8.2.2",
|
|
303
306
|
"strip-ansi": "^7.2.0",
|
|
307
|
+
typescript: "^5.9.3",
|
|
304
308
|
yaml: "^2.8.2",
|
|
305
309
|
zod: "^4.4.3"
|
|
306
310
|
},
|
|
@@ -310,7 +314,6 @@ var package_default = {
|
|
|
310
314
|
"@types/react": "^19.2.7",
|
|
311
315
|
tsup: "^8.5.1",
|
|
312
316
|
tsx: "^4.21.0",
|
|
313
|
-
typescript: "^5.9.3",
|
|
314
317
|
vitest: "^4.0.18"
|
|
315
318
|
},
|
|
316
319
|
overrides: {
|
|
@@ -2610,8 +2613,9 @@ function countExplicitPaths(value) {
|
|
|
2610
2613
|
// src/context/local-index.ts
|
|
2611
2614
|
import { createHash as createHash6 } from "node:crypto";
|
|
2612
2615
|
import { lstat as lstat8, readFile as readFile3, stat as stat3 } from "node:fs/promises";
|
|
2613
|
-
import { basename as basename5, dirname as dirname7, extname as extname2, isAbsolute as isAbsolute3, join as join7, relative as relative5, resolve as resolve8, sep as sep4 } from "node:path";
|
|
2616
|
+
import { basename as basename5, dirname as dirname7, extname as extname2, isAbsolute as isAbsolute3, join as join7, posix, relative as relative5, resolve as resolve8, sep as sep4 } from "node:path";
|
|
2614
2617
|
import fg from "fast-glob";
|
|
2618
|
+
import ts from "typescript";
|
|
2615
2619
|
import { z as z3 } from "zod";
|
|
2616
2620
|
|
|
2617
2621
|
// src/tools/workspace.ts
|
|
@@ -3063,10 +3067,13 @@ var indexedFileSchema = z3.object({
|
|
|
3063
3067
|
ctimeMs: z3.number().optional(),
|
|
3064
3068
|
size: z3.number().nonnegative(),
|
|
3065
3069
|
contentHash: contentHashSchema,
|
|
3066
|
-
chunks: z3.array(indexedChunkSchema)
|
|
3070
|
+
chunks: z3.array(indexedChunkSchema),
|
|
3071
|
+
definitions: z3.array(z3.string()),
|
|
3072
|
+
references: z3.array(z3.string()),
|
|
3073
|
+
imports: z3.array(z3.string())
|
|
3067
3074
|
}).strict();
|
|
3068
3075
|
var localIndexSchema = z3.object({
|
|
3069
|
-
version: z3.literal(
|
|
3076
|
+
version: z3.literal(3),
|
|
3070
3077
|
createdAt: z3.string(),
|
|
3071
3078
|
generation: z3.string().min(1),
|
|
3072
3079
|
roots: z3.array(z3.string()),
|
|
@@ -3112,6 +3119,8 @@ var LocalContextIndex = class {
|
|
|
3112
3119
|
index;
|
|
3113
3120
|
workspace;
|
|
3114
3121
|
queryCache = /* @__PURE__ */ new Map();
|
|
3122
|
+
queryCacheHits = 0;
|
|
3123
|
+
queryCacheMisses = 0;
|
|
3115
3124
|
fingerprintCache;
|
|
3116
3125
|
dirtyPaths = /* @__PURE__ */ new Set();
|
|
3117
3126
|
refreshState = "current";
|
|
@@ -3257,7 +3266,7 @@ var LocalContextIndex = class {
|
|
|
3257
3266
|
const nextFiles = [...files.values()].sort((left, right) => left.absolutePath.localeCompare(right.absolutePath));
|
|
3258
3267
|
const generation = createGeneration(nextFiles);
|
|
3259
3268
|
this.index = {
|
|
3260
|
-
version:
|
|
3269
|
+
version: 3,
|
|
3261
3270
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3262
3271
|
generation,
|
|
3263
3272
|
roots: this.roots,
|
|
@@ -3309,6 +3318,8 @@ var LocalContextIndex = class {
|
|
|
3309
3318
|
files: this.index?.files.length ?? 0,
|
|
3310
3319
|
chunks: this.index?.files.reduce((total, file) => total + file.chunks.length, 0) ?? 0,
|
|
3311
3320
|
queryCacheEntries: this.queryCache.size,
|
|
3321
|
+
queryCacheHits: this.queryCacheHits,
|
|
3322
|
+
queryCacheMisses: this.queryCacheMisses,
|
|
3312
3323
|
refreshState: this.refreshState,
|
|
3313
3324
|
dirtyPaths: this.dirtyPaths.size,
|
|
3314
3325
|
...this.refreshError ? { refreshError: this.refreshError } : {},
|
|
@@ -3391,12 +3402,13 @@ var LocalContextIndex = class {
|
|
|
3391
3402
|
ctimeMs: info.ctimeMs,
|
|
3392
3403
|
size: info.size,
|
|
3393
3404
|
contentHash,
|
|
3394
|
-
chunks: chunkFile(safeItem, content)
|
|
3405
|
+
chunks: chunkFile(safeItem, content),
|
|
3406
|
+
...extractSourceFacts(safeItem.path, content)
|
|
3395
3407
|
});
|
|
3396
3408
|
}
|
|
3397
3409
|
const generation = createGeneration(files);
|
|
3398
3410
|
this.index = {
|
|
3399
|
-
version:
|
|
3411
|
+
version: 3,
|
|
3400
3412
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3401
3413
|
generation,
|
|
3402
3414
|
roots: this.roots,
|
|
@@ -3461,7 +3473,8 @@ var LocalContextIndex = class {
|
|
|
3461
3473
|
ctimeMs: info.ctimeMs,
|
|
3462
3474
|
size: info.size,
|
|
3463
3475
|
contentHash: hashContent(content),
|
|
3464
|
-
chunks: chunkFile(safeItem, content)
|
|
3476
|
+
chunks: chunkFile(safeItem, content),
|
|
3477
|
+
...extractSourceFacts(safeItem.path, content)
|
|
3465
3478
|
};
|
|
3466
3479
|
} catch {
|
|
3467
3480
|
return void 0;
|
|
@@ -3484,7 +3497,8 @@ var LocalContextIndex = class {
|
|
|
3484
3497
|
path: file.path,
|
|
3485
3498
|
absolutePath: file.absolutePath
|
|
3486
3499
|
}, content);
|
|
3487
|
-
|
|
3500
|
+
const expectedFacts = extractSourceFacts(file.path, content);
|
|
3501
|
+
if (!chunksMatch(expectedChunks, file.chunks) || !sameStrings(expectedFacts.definitions, file.definitions) || !sameStrings(expectedFacts.references, file.references) || !sameStrings(expectedFacts.imports, file.imports)) return false;
|
|
3488
3502
|
} catch {
|
|
3489
3503
|
return false;
|
|
3490
3504
|
}
|
|
@@ -3524,10 +3538,14 @@ var LocalContextIndex = class {
|
|
|
3524
3538
|
return matches.length === 1 ? { root, path, absolutePath } : void 0;
|
|
3525
3539
|
}
|
|
3526
3540
|
rank(query, topK) {
|
|
3527
|
-
const
|
|
3541
|
+
const index = this.index;
|
|
3542
|
+
const files = index?.files ?? [];
|
|
3543
|
+
const chunks = files.flatMap((file) => file.chunks);
|
|
3528
3544
|
if (!chunks.length) return [];
|
|
3529
|
-
const
|
|
3530
|
-
if (!
|
|
3545
|
+
const directTerms = [...new Set(tokenize(query))];
|
|
3546
|
+
if (!directTerms.length || !index) return [];
|
|
3547
|
+
const expandedTerms = expandQueryTerms(files, directTerms);
|
|
3548
|
+
const terms = [.../* @__PURE__ */ new Set([...directTerms, ...expandedTerms])];
|
|
3531
3549
|
const queryTerms = new Set(terms);
|
|
3532
3550
|
const documentFrequency = new Map([...queryTerms].map((term) => [term, 0]));
|
|
3533
3551
|
for (const chunk of chunks) {
|
|
@@ -3536,22 +3554,51 @@ var LocalContextIndex = class {
|
|
|
3536
3554
|
}
|
|
3537
3555
|
}
|
|
3538
3556
|
const averageLength = chunks.reduce((sum, chunk) => sum + chunk.tokens.length, 0) / Math.max(chunks.length, 1);
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
score
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
}));
|
|
3557
|
+
const filesByPath = new Map(files.map((file) => [file.absolutePath, file]));
|
|
3558
|
+
const graphBoosts = buildGraphBoosts(files, directTerms);
|
|
3559
|
+
const coverageTerms = directTerms.filter((term) => !retrievalStopWords.has(term));
|
|
3560
|
+
const scored = chunks.map((chunk) => {
|
|
3561
|
+
const lexical = scoreChunk(
|
|
3562
|
+
chunk,
|
|
3563
|
+
terms,
|
|
3564
|
+
query,
|
|
3565
|
+
documentFrequency,
|
|
3566
|
+
chunks.length,
|
|
3567
|
+
averageLength
|
|
3568
|
+
);
|
|
3569
|
+
const graph = graphBoosts.get(chunk.absolutePath)?.score ?? 0;
|
|
3570
|
+
const score = lexical.bm25 + lexical.path + lexical.symbol + lexical.phrase + graph;
|
|
3571
|
+
return { chunk, lexical, graph, score };
|
|
3572
|
+
}).filter(({ score }) => score > 0).sort((left, right) => right.score - left.score || left.chunk.absolutePath.localeCompare(right.chunk.absolutePath));
|
|
3573
|
+
const covered = scored.filter(({ lexical, graph }) => graph > 0 || hasSufficientQueryCoverage(lexical.matchedTerms, coverageTerms));
|
|
3574
|
+
return (covered.length ? covered : scored).slice(0, topK).map(({ chunk, lexical, graph, score }) => {
|
|
3575
|
+
const file = filesByPath.get(chunk.absolutePath);
|
|
3576
|
+
const breakdown = {
|
|
3577
|
+
bm25: roundScore(lexical.bm25),
|
|
3578
|
+
path: roundScore(lexical.path),
|
|
3579
|
+
symbol: roundScore(lexical.symbol),
|
|
3580
|
+
phrase: roundScore(lexical.phrase),
|
|
3581
|
+
graph: roundScore(graph),
|
|
3582
|
+
total: roundScore(score)
|
|
3583
|
+
};
|
|
3584
|
+
const provenance = {
|
|
3585
|
+
generation: index.generation,
|
|
3586
|
+
contentHash: file?.contentHash ?? hashContent(chunk.content),
|
|
3587
|
+
matchedTerms: lexical.matchedTerms.slice(0, 16),
|
|
3588
|
+
expandedTerms: expandedTerms.slice(0, 16),
|
|
3589
|
+
score: breakdown
|
|
3590
|
+
};
|
|
3591
|
+
return {
|
|
3592
|
+
path: chunk.absolutePath,
|
|
3593
|
+
startLine: chunk.startLine,
|
|
3594
|
+
endLine: chunk.endLine,
|
|
3595
|
+
content: chunk.content,
|
|
3596
|
+
score,
|
|
3597
|
+
source: "local-bm25+path+symbol+graph",
|
|
3598
|
+
...chunk.symbol ? { symbol: chunk.symbol } : {},
|
|
3599
|
+
provenance
|
|
3600
|
+
};
|
|
3601
|
+
});
|
|
3555
3602
|
}
|
|
3556
3603
|
async hitsAreCurrent(hits) {
|
|
3557
3604
|
const files = new Map((this.index?.files ?? []).map((file) => [file.absolutePath, file]));
|
|
@@ -3575,10 +3622,17 @@ var LocalContextIndex = class {
|
|
|
3575
3622
|
}
|
|
3576
3623
|
getCachedHits(query, topK) {
|
|
3577
3624
|
const generation = this.index?.generation;
|
|
3578
|
-
if (!generation)
|
|
3625
|
+
if (!generation) {
|
|
3626
|
+
this.queryCacheMisses += 1;
|
|
3627
|
+
return void 0;
|
|
3628
|
+
}
|
|
3579
3629
|
const key = `${generation}\0${topK}\0${query}`;
|
|
3580
3630
|
const cached = this.queryCache.get(key);
|
|
3581
|
-
if (!cached)
|
|
3631
|
+
if (!cached) {
|
|
3632
|
+
this.queryCacheMisses += 1;
|
|
3633
|
+
return void 0;
|
|
3634
|
+
}
|
|
3635
|
+
this.queryCacheHits += 1;
|
|
3582
3636
|
this.queryCache.delete(key);
|
|
3583
3637
|
this.queryCache.set(key, cached);
|
|
3584
3638
|
return cloneHits(cached);
|
|
@@ -3674,6 +3728,9 @@ function chunksMatch(expected, actual) {
|
|
|
3674
3728
|
return Boolean(candidate) && chunk.id === candidate?.id && chunk.root === candidate.root && chunk.path === candidate.path && chunk.absolutePath === candidate.absolutePath && chunk.startLine === candidate.startLine && chunk.endLine === candidate.endLine && chunk.content === candidate.content && chunk.symbol === candidate.symbol && chunk.tokens.length === candidate.tokens.length && chunk.tokens.every((token, tokenIndex) => token === candidate.tokens[tokenIndex]);
|
|
3675
3729
|
});
|
|
3676
3730
|
}
|
|
3731
|
+
function sameStrings(expected, actual) {
|
|
3732
|
+
return expected.length === actual.length && expected.every((value, index) => value === actual[index]);
|
|
3733
|
+
}
|
|
3677
3734
|
function existingBuildResult(status, started) {
|
|
3678
3735
|
if (!status.available || !status.generation) {
|
|
3679
3736
|
throw new Error("The existing local context index is unavailable.");
|
|
@@ -3728,7 +3785,8 @@ function packContextHits(hits, roots, maxTokens, engine) {
|
|
|
3728
3785
|
const text = selected.map((hit) => {
|
|
3729
3786
|
const shownPath = workspaceAliasPath(hit.path, roots);
|
|
3730
3787
|
const symbol = hit.symbol ? ` symbol="${escapeAttribute(hit.symbol)}"` : "";
|
|
3731
|
-
|
|
3788
|
+
const provenance = hit.provenance ? ` source="${escapeAttribute(hit.source)}" hash="${hit.provenance.contentHash.slice(0, 12)}" graph-score="${hit.provenance.score.graph.toFixed(3)}"` : "";
|
|
3789
|
+
return `<code path="${escapeAttribute(shownPath)}" lines="${hit.startLine}-${hit.endLine}" score="${hit.score.toFixed(3)}"${symbol}${provenance}>
|
|
3732
3790
|
${hit.content}
|
|
3733
3791
|
</code>`;
|
|
3734
3792
|
}).join("\n\n");
|
|
@@ -3744,7 +3802,17 @@ ${hit.content}
|
|
|
3744
3802
|
};
|
|
3745
3803
|
}
|
|
3746
3804
|
function cloneHits(hits) {
|
|
3747
|
-
return hits.map((hit) => ({
|
|
3805
|
+
return hits.map((hit) => ({
|
|
3806
|
+
...hit,
|
|
3807
|
+
...hit.provenance ? {
|
|
3808
|
+
provenance: {
|
|
3809
|
+
...hit.provenance,
|
|
3810
|
+
matchedTerms: [...hit.provenance.matchedTerms],
|
|
3811
|
+
expandedTerms: [...hit.provenance.expandedTerms],
|
|
3812
|
+
score: { ...hit.provenance.score }
|
|
3813
|
+
}
|
|
3814
|
+
} : {}
|
|
3815
|
+
}));
|
|
3748
3816
|
}
|
|
3749
3817
|
function cloneDuplicationBaseline(baseline) {
|
|
3750
3818
|
return {
|
|
@@ -3846,6 +3914,109 @@ function detectSymbol(lines) {
|
|
|
3846
3914
|
}
|
|
3847
3915
|
return void 0;
|
|
3848
3916
|
}
|
|
3917
|
+
function extractSourceFacts(path, content) {
|
|
3918
|
+
const extension = extname2(path).toLocaleLowerCase();
|
|
3919
|
+
if ([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"].includes(extension)) {
|
|
3920
|
+
return extractTypeScriptFacts(path, content);
|
|
3921
|
+
}
|
|
3922
|
+
if (extension === ".py") return extractPythonFacts(content);
|
|
3923
|
+
if (extension === ".sql") return extractSqlFacts(content);
|
|
3924
|
+
return { definitions: [], references: [], imports: [] };
|
|
3925
|
+
}
|
|
3926
|
+
function extractTypeScriptFacts(path, content) {
|
|
3927
|
+
const definitions = /* @__PURE__ */ new Set();
|
|
3928
|
+
const references = /* @__PURE__ */ new Set();
|
|
3929
|
+
const imports = /* @__PURE__ */ new Set();
|
|
3930
|
+
const source = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, false, scriptKind(path));
|
|
3931
|
+
const addName = (name) => {
|
|
3932
|
+
if (!name) return;
|
|
3933
|
+
if (ts.isIdentifier(name)) definitions.add(name.text);
|
|
3934
|
+
else if (ts.isStringLiteralLike(name) || ts.isNumericLiteral(name)) definitions.add(name.text);
|
|
3935
|
+
else if (ts.isObjectBindingPattern(name) || ts.isArrayBindingPattern(name)) {
|
|
3936
|
+
for (const element of name.elements) if (ts.isBindingElement(element)) addName(element.name);
|
|
3937
|
+
}
|
|
3938
|
+
};
|
|
3939
|
+
const addCallReference = (expression) => {
|
|
3940
|
+
if (ts.isIdentifier(expression)) references.add(expression.text);
|
|
3941
|
+
else if (ts.isPropertyAccessExpression(expression)) references.add(expression.name.text);
|
|
3942
|
+
else if (ts.isElementAccessExpression(expression) && ts.isIdentifier(expression.expression)) {
|
|
3943
|
+
references.add(expression.expression.text);
|
|
3944
|
+
}
|
|
3945
|
+
};
|
|
3946
|
+
const visit = (node) => {
|
|
3947
|
+
if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
|
|
3948
|
+
if (node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) imports.add(node.moduleSpecifier.text);
|
|
3949
|
+
} else if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference) && node.moduleReference.expression && ts.isStringLiteral(node.moduleReference.expression)) {
|
|
3950
|
+
imports.add(node.moduleReference.expression.text);
|
|
3951
|
+
} else if (ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isEnumDeclaration(node) || ts.isModuleDeclaration(node)) {
|
|
3952
|
+
addName(node.name);
|
|
3953
|
+
} else if (ts.isVariableDeclaration(node) || ts.isBindingElement(node) || ts.isMethodDeclaration(node) || ts.isPropertyDeclaration(node) || ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node)) {
|
|
3954
|
+
addName(node.name);
|
|
3955
|
+
} else if (ts.isCallExpression(node) || ts.isNewExpression(node)) {
|
|
3956
|
+
addCallReference(node.expression);
|
|
3957
|
+
}
|
|
3958
|
+
ts.forEachChild(node, visit);
|
|
3959
|
+
};
|
|
3960
|
+
visit(source);
|
|
3961
|
+
return {
|
|
3962
|
+
definitions: sortedValues(definitions),
|
|
3963
|
+
references: sortedValues(references),
|
|
3964
|
+
imports: sortedValues(imports)
|
|
3965
|
+
};
|
|
3966
|
+
}
|
|
3967
|
+
function scriptKind(path) {
|
|
3968
|
+
switch (extname2(path).toLocaleLowerCase()) {
|
|
3969
|
+
case ".tsx":
|
|
3970
|
+
return ts.ScriptKind.TSX;
|
|
3971
|
+
case ".jsx":
|
|
3972
|
+
return ts.ScriptKind.JSX;
|
|
3973
|
+
case ".js":
|
|
3974
|
+
case ".mjs":
|
|
3975
|
+
case ".cjs":
|
|
3976
|
+
return ts.ScriptKind.JS;
|
|
3977
|
+
default:
|
|
3978
|
+
return ts.ScriptKind.TS;
|
|
3979
|
+
}
|
|
3980
|
+
}
|
|
3981
|
+
function extractPythonFacts(content) {
|
|
3982
|
+
const definitions = /* @__PURE__ */ new Set();
|
|
3983
|
+
const references = /* @__PURE__ */ new Set();
|
|
3984
|
+
const imports = /* @__PURE__ */ new Set();
|
|
3985
|
+
for (const line of content.split("\n")) {
|
|
3986
|
+
const definition = line.match(/^\s*(?:async\s+)?(?:def|class)\s+([\p{L}_][\p{L}\p{N}_]*)/u);
|
|
3987
|
+
if (definition?.[1]) definitions.add(definition[1]);
|
|
3988
|
+
const imported = line.match(/^\s*(?:from\s+([\w.]+)\s+import|import\s+([\w.]+))/u);
|
|
3989
|
+
if (imported?.[1] || imported?.[2]) imports.add(imported[1] ?? imported[2] ?? "");
|
|
3990
|
+
for (const call of line.matchAll(/([\p{L}_][\p{L}\p{N}_]*)\s*\(/gu)) {
|
|
3991
|
+
if (call[1] && !["def", "class", "if", "for", "while"].includes(call[1])) references.add(call[1]);
|
|
3992
|
+
}
|
|
3993
|
+
}
|
|
3994
|
+
for (const definition of definitions) references.delete(definition);
|
|
3995
|
+
return {
|
|
3996
|
+
definitions: sortedValues(definitions),
|
|
3997
|
+
references: sortedValues(references),
|
|
3998
|
+
imports: sortedValues(imports)
|
|
3999
|
+
};
|
|
4000
|
+
}
|
|
4001
|
+
function extractSqlFacts(content) {
|
|
4002
|
+
const definitions = /* @__PURE__ */ new Set();
|
|
4003
|
+
const references = /* @__PURE__ */ new Set();
|
|
4004
|
+
const imports = /* @__PURE__ */ new Set();
|
|
4005
|
+
for (const definition of content.matchAll(/\bcreate\s+(?:table|view|function|procedure)\s+(?:if\s+not\s+exists\s+)?([\w.]+)/giu)) {
|
|
4006
|
+
if (definition[1]) definitions.add(definition[1]);
|
|
4007
|
+
}
|
|
4008
|
+
for (const reference2 of content.matchAll(/\b(?:from|join|update|into)\s+([\w.]+)/giu)) {
|
|
4009
|
+
if (reference2[1]) references.add(reference2[1]);
|
|
4010
|
+
}
|
|
4011
|
+
return {
|
|
4012
|
+
definitions: sortedValues(definitions),
|
|
4013
|
+
references: sortedValues(references),
|
|
4014
|
+
imports: sortedValues(imports)
|
|
4015
|
+
};
|
|
4016
|
+
}
|
|
4017
|
+
function sortedValues(values) {
|
|
4018
|
+
return [...values].filter(Boolean).sort((left, right) => left.localeCompare(right));
|
|
4019
|
+
}
|
|
3849
4020
|
function tokenize(input2) {
|
|
3850
4021
|
const normalized = input2.replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([\p{Ll}])([\p{Lu}])/gu, "$1 $2").toLocaleLowerCase();
|
|
3851
4022
|
const base = normalized.match(/[\p{L}\p{N}_-]+/gu) ?? [];
|
|
@@ -3861,24 +4032,123 @@ function tokenize(input2) {
|
|
|
3861
4032
|
}
|
|
3862
4033
|
return output2;
|
|
3863
4034
|
}
|
|
4035
|
+
var retrievalStopWords = /* @__PURE__ */ new Set([
|
|
4036
|
+
"add",
|
|
4037
|
+
"audit",
|
|
4038
|
+
"change",
|
|
4039
|
+
"code",
|
|
4040
|
+
"create",
|
|
4041
|
+
"explain",
|
|
4042
|
+
"find",
|
|
4043
|
+
"fix",
|
|
4044
|
+
"implement",
|
|
4045
|
+
"inspect",
|
|
4046
|
+
"review",
|
|
4047
|
+
"show",
|
|
4048
|
+
"test",
|
|
4049
|
+
"the",
|
|
4050
|
+
"update",
|
|
4051
|
+
"where",
|
|
4052
|
+
"with",
|
|
4053
|
+
"\u4FEE\u6539",
|
|
4054
|
+
"\u4FEE\u590D",
|
|
4055
|
+
"\u5B9E\u73B0",
|
|
4056
|
+
"\u5BA1\u67E5",
|
|
4057
|
+
"\u67E5\u627E",
|
|
4058
|
+
"\u89E3\u91CA",
|
|
4059
|
+
"\u6D4B\u8BD5",
|
|
4060
|
+
"\u66F4\u65B0"
|
|
4061
|
+
]);
|
|
4062
|
+
function hasSufficientQueryCoverage(matchedTerms, coverageTerms) {
|
|
4063
|
+
if (!coverageTerms.length) return matchedTerms.length > 0;
|
|
4064
|
+
const matched = new Set(matchedTerms);
|
|
4065
|
+
const covered = [...new Set(coverageTerms)].filter((term) => matched.has(term)).length;
|
|
4066
|
+
return covered >= Math.min(2, new Set(coverageTerms).size);
|
|
4067
|
+
}
|
|
4068
|
+
function expandQueryTerms(files, directTerms) {
|
|
4069
|
+
const expanded = /* @__PURE__ */ new Set();
|
|
4070
|
+
for (const file of files) {
|
|
4071
|
+
for (const definition of file.definitions) {
|
|
4072
|
+
const definitionTerms = [...new Set(tokenize(definition))];
|
|
4073
|
+
if (!definitionMatchesQuery(definitionTerms, directTerms)) continue;
|
|
4074
|
+
for (const term of definitionTerms) if (!directTerms.includes(term)) expanded.add(term);
|
|
4075
|
+
}
|
|
4076
|
+
}
|
|
4077
|
+
return [...expanded].sort((left, right) => left.localeCompare(right)).slice(0, 24);
|
|
4078
|
+
}
|
|
4079
|
+
function buildGraphBoosts(files, directTerms) {
|
|
4080
|
+
const anchors = /* @__PURE__ */ new Map();
|
|
4081
|
+
for (const file of files) {
|
|
4082
|
+
for (const definition of file.definitions) {
|
|
4083
|
+
if (!definitionMatchesQuery(tokenize(definition), directTerms)) continue;
|
|
4084
|
+
const values = anchors.get(file.absolutePath) ?? /* @__PURE__ */ new Set();
|
|
4085
|
+
values.add(definition.toLocaleLowerCase());
|
|
4086
|
+
anchors.set(file.absolutePath, values);
|
|
4087
|
+
}
|
|
4088
|
+
}
|
|
4089
|
+
if (!anchors.size) return /* @__PURE__ */ new Map();
|
|
4090
|
+
const boosts = /* @__PURE__ */ new Map();
|
|
4091
|
+
const addBoost = (path, score) => {
|
|
4092
|
+
boosts.set(path, { score: (boosts.get(path)?.score ?? 0) + score });
|
|
4093
|
+
};
|
|
4094
|
+
const anchorSymbols = new Set([...anchors.values()].flatMap((symbols) => [...symbols]));
|
|
4095
|
+
for (const file of files) {
|
|
4096
|
+
if (file.references.some((reference2) => anchorSymbols.has(reference2.toLocaleLowerCase()))) {
|
|
4097
|
+
addBoost(file.absolutePath, 1.5);
|
|
4098
|
+
}
|
|
4099
|
+
for (const specifier of file.imports) {
|
|
4100
|
+
for (const target of resolveImportTargets(file, specifier, files)) {
|
|
4101
|
+
if (anchors.has(target)) addBoost(file.absolutePath, 1.25);
|
|
4102
|
+
if (anchors.has(file.absolutePath)) addBoost(target, 0.75);
|
|
4103
|
+
}
|
|
4104
|
+
}
|
|
4105
|
+
}
|
|
4106
|
+
return boosts;
|
|
4107
|
+
}
|
|
4108
|
+
function definitionMatchesQuery(definitionTerms, queryTerms) {
|
|
4109
|
+
const definitions = new Set(definitionTerms);
|
|
4110
|
+
const shared = [...new Set(queryTerms)].filter((term) => definitions.has(term));
|
|
4111
|
+
const required = Math.min(2, Math.max(1, Math.min(definitions.size, queryTerms.length)));
|
|
4112
|
+
return shared.length >= required;
|
|
4113
|
+
}
|
|
4114
|
+
function resolveImportTargets(importer, specifier, files) {
|
|
4115
|
+
if (!specifier.startsWith(".")) return [];
|
|
4116
|
+
const base = posix.normalize(posix.join(posix.dirname(importer.path), specifier));
|
|
4117
|
+
const extensions = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
|
|
4118
|
+
const importedExtension = posix.extname(base);
|
|
4119
|
+
const extensionless = extensions.includes(importedExtension) ? base.slice(0, -importedExtension.length) : base;
|
|
4120
|
+
const candidates = /* @__PURE__ */ new Set([
|
|
4121
|
+
base,
|
|
4122
|
+
...extensions.map((extension) => `${extensionless}${extension}`),
|
|
4123
|
+
...extensions.map((extension) => posix.join(extensionless, `index${extension}`))
|
|
4124
|
+
]);
|
|
4125
|
+
return files.filter((file) => file.root === importer.root && candidates.has(file.path)).map((file) => file.absolutePath);
|
|
4126
|
+
}
|
|
3864
4127
|
function scoreChunk(chunk, terms, rawQuery, documentFrequency, documentCount, averageLength) {
|
|
3865
4128
|
const frequencies = /* @__PURE__ */ new Map();
|
|
3866
4129
|
for (const token of chunk.tokens) frequencies.set(token, (frequencies.get(token) ?? 0) + 1);
|
|
3867
4130
|
const k1 = 1.2;
|
|
3868
4131
|
const b = 0.75;
|
|
3869
|
-
let
|
|
4132
|
+
let bm25 = 0;
|
|
4133
|
+
let path = 0;
|
|
4134
|
+
let symbol = 0;
|
|
4135
|
+
const matchedTerms = [];
|
|
3870
4136
|
for (const term of terms) {
|
|
3871
4137
|
const frequency = frequencies.get(term) ?? 0;
|
|
3872
4138
|
if (!frequency) continue;
|
|
4139
|
+
matchedTerms.push(term);
|
|
3873
4140
|
const df = documentFrequency.get(term) ?? 0;
|
|
3874
4141
|
const idf = Math.log(1 + (documentCount - df + 0.5) / (df + 0.5));
|
|
3875
|
-
|
|
3876
|
-
if (chunk.path.toLocaleLowerCase().includes(term))
|
|
3877
|
-
if (chunk.symbol?.toLocaleLowerCase().includes(term))
|
|
4142
|
+
bm25 += idf * (frequency * (k1 + 1) / (frequency + k1 * (1 - b + b * chunk.tokens.length / averageLength)));
|
|
4143
|
+
if (chunk.path.toLocaleLowerCase().includes(term)) path += 1.5;
|
|
4144
|
+
if (chunk.symbol?.toLocaleLowerCase().includes(term)) symbol += 2.5;
|
|
3878
4145
|
}
|
|
3879
4146
|
const phrase = rawQuery.trim().toLocaleLowerCase();
|
|
3880
|
-
|
|
3881
|
-
return
|
|
4147
|
+
const phraseScore = phrase.length > 3 && chunk.content.toLocaleLowerCase().includes(phrase) ? 3 : 0;
|
|
4148
|
+
return { bm25, path, symbol, phrase: phraseScore, matchedTerms };
|
|
4149
|
+
}
|
|
4150
|
+
function roundScore(value) {
|
|
4151
|
+
return Number(value.toFixed(6));
|
|
3882
4152
|
}
|
|
3883
4153
|
|
|
3884
4154
|
// src/context/context-engine.ts
|
|
@@ -3989,7 +4259,10 @@ function formatContextHits(hits, roots) {
|
|
|
3989
4259
|
return hits.map((hit) => {
|
|
3990
4260
|
const path = workspaceAliasPath(hit.path, roots);
|
|
3991
4261
|
const symbol = hit.symbol ? ` ${hit.symbol}` : "";
|
|
3992
|
-
|
|
4262
|
+
const score = hit.provenance?.score;
|
|
4263
|
+
const breakdown = score ? ` bm25=${score.bm25.toFixed(2)} path=${score.path.toFixed(2)} symbol=${score.symbol.toFixed(2)} graph=${score.graph.toFixed(2)}` : "";
|
|
4264
|
+
const hash4 = hit.provenance?.contentHash.slice(0, 12);
|
|
4265
|
+
return `[${hit.source} ${hit.score.toFixed(3)}${breakdown}${hash4 ? ` hash=${hash4}` : ""}]${symbol} ${path}:${hit.startLine}-${hit.endLine}`;
|
|
3993
4266
|
}).join("\n");
|
|
3994
4267
|
}
|
|
3995
4268
|
|
|
@@ -9719,7 +9992,7 @@ async function evaluateReuseGate(input2) {
|
|
|
9719
9992
|
candidates.push({
|
|
9720
9993
|
path: hit.path,
|
|
9721
9994
|
...hit.symbol ? { symbol: hit.symbol.slice(0, 160) } : {},
|
|
9722
|
-
score:
|
|
9995
|
+
score: roundScore2(hit.score),
|
|
9723
9996
|
read
|
|
9724
9997
|
});
|
|
9725
9998
|
}
|
|
@@ -9827,7 +10100,7 @@ function unresolvedReceipt(input2, queryHash, targetPaths, trigger, detail) {
|
|
|
9827
10100
|
function hash3(value) {
|
|
9828
10101
|
return createHash13("sha256").update(value).digest("hex");
|
|
9829
10102
|
}
|
|
9830
|
-
function
|
|
10103
|
+
function roundScore2(value) {
|
|
9831
10104
|
return Number.isFinite(value) ? Math.round(value * 1e3) / 1e3 : 0;
|
|
9832
10105
|
}
|
|
9833
10106
|
|