@skein-code/cli 0.3.17 → 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 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. The included fixture only validates the
445
- metric pipeline; it is not evidence for performance on a production repository.
446
- `--fresh-index` deletes and rebuilds that workspace's local index, so use it only
447
- where rebuilding the index is intended.
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.17",
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,12 @@ 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",
242
+ "Large MCP catalogs now disclose at most eight request-relevant schemas per run while preserving network permissions",
243
+ "Token Ledger receipts report deferred progressive schemas and selected definitions remain stable across tool turns",
244
+ "A deterministic Token Economy replay benchmark guards evidence, output-firewall, and no-progress invariants",
239
245
  "Calibrated Type-1/2 duplication matches now block completion until reuse or exact audited suppression",
240
246
  "Type-3 duplication remains warning-only and Type-4 semantic equivalence remains explicitly unsupported",
241
247
  "Duplication benchmark fixtures report deterministic recall, precision, false-positive rate, and latency",
@@ -278,6 +284,7 @@ var package_default = {
278
284
  "test:pty": "sh test/pty/run-visual.sh",
279
285
  "benchmark:context": "tsx scripts/benchmark-local-index.ts",
280
286
  "benchmark:duplication": "tsx scripts/benchmark-duplication.ts",
287
+ "benchmark:token-economy": "tsx scripts/benchmark-token-economy.ts",
281
288
  "verify:package": "node scripts/verify-package.mjs",
282
289
  "release:verify": "npm run check && npm run verify:package --",
283
290
  typecheck: "tsc --noEmit",
@@ -297,6 +304,7 @@ var package_default = {
297
304
  react: "^19.2.3",
298
305
  "string-width": "^8.2.2",
299
306
  "strip-ansi": "^7.2.0",
307
+ typescript: "^5.9.3",
300
308
  yaml: "^2.8.2",
301
309
  zod: "^4.4.3"
302
310
  },
@@ -306,7 +314,6 @@ var package_default = {
306
314
  "@types/react": "^19.2.7",
307
315
  tsup: "^8.5.1",
308
316
  tsx: "^4.21.0",
309
- typescript: "^5.9.3",
310
317
  vitest: "^4.0.18"
311
318
  },
312
319
  overrides: {
@@ -2606,8 +2613,9 @@ function countExplicitPaths(value) {
2606
2613
  // src/context/local-index.ts
2607
2614
  import { createHash as createHash6 } from "node:crypto";
2608
2615
  import { lstat as lstat8, readFile as readFile3, stat as stat3 } from "node:fs/promises";
2609
- 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";
2610
2617
  import fg from "fast-glob";
2618
+ import ts from "typescript";
2611
2619
  import { z as z3 } from "zod";
2612
2620
 
2613
2621
  // src/tools/workspace.ts
@@ -3059,10 +3067,13 @@ var indexedFileSchema = z3.object({
3059
3067
  ctimeMs: z3.number().optional(),
3060
3068
  size: z3.number().nonnegative(),
3061
3069
  contentHash: contentHashSchema,
3062
- 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())
3063
3074
  }).strict();
3064
3075
  var localIndexSchema = z3.object({
3065
- version: z3.literal(2),
3076
+ version: z3.literal(3),
3066
3077
  createdAt: z3.string(),
3067
3078
  generation: z3.string().min(1),
3068
3079
  roots: z3.array(z3.string()),
@@ -3108,6 +3119,8 @@ var LocalContextIndex = class {
3108
3119
  index;
3109
3120
  workspace;
3110
3121
  queryCache = /* @__PURE__ */ new Map();
3122
+ queryCacheHits = 0;
3123
+ queryCacheMisses = 0;
3111
3124
  fingerprintCache;
3112
3125
  dirtyPaths = /* @__PURE__ */ new Set();
3113
3126
  refreshState = "current";
@@ -3253,7 +3266,7 @@ var LocalContextIndex = class {
3253
3266
  const nextFiles = [...files.values()].sort((left, right) => left.absolutePath.localeCompare(right.absolutePath));
3254
3267
  const generation = createGeneration(nextFiles);
3255
3268
  this.index = {
3256
- version: 2,
3269
+ version: 3,
3257
3270
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3258
3271
  generation,
3259
3272
  roots: this.roots,
@@ -3305,6 +3318,8 @@ var LocalContextIndex = class {
3305
3318
  files: this.index?.files.length ?? 0,
3306
3319
  chunks: this.index?.files.reduce((total, file) => total + file.chunks.length, 0) ?? 0,
3307
3320
  queryCacheEntries: this.queryCache.size,
3321
+ queryCacheHits: this.queryCacheHits,
3322
+ queryCacheMisses: this.queryCacheMisses,
3308
3323
  refreshState: this.refreshState,
3309
3324
  dirtyPaths: this.dirtyPaths.size,
3310
3325
  ...this.refreshError ? { refreshError: this.refreshError } : {},
@@ -3387,12 +3402,13 @@ var LocalContextIndex = class {
3387
3402
  ctimeMs: info.ctimeMs,
3388
3403
  size: info.size,
3389
3404
  contentHash,
3390
- chunks: chunkFile(safeItem, content)
3405
+ chunks: chunkFile(safeItem, content),
3406
+ ...extractSourceFacts(safeItem.path, content)
3391
3407
  });
3392
3408
  }
3393
3409
  const generation = createGeneration(files);
3394
3410
  this.index = {
3395
- version: 2,
3411
+ version: 3,
3396
3412
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3397
3413
  generation,
3398
3414
  roots: this.roots,
@@ -3457,7 +3473,8 @@ var LocalContextIndex = class {
3457
3473
  ctimeMs: info.ctimeMs,
3458
3474
  size: info.size,
3459
3475
  contentHash: hashContent(content),
3460
- chunks: chunkFile(safeItem, content)
3476
+ chunks: chunkFile(safeItem, content),
3477
+ ...extractSourceFacts(safeItem.path, content)
3461
3478
  };
3462
3479
  } catch {
3463
3480
  return void 0;
@@ -3480,7 +3497,8 @@ var LocalContextIndex = class {
3480
3497
  path: file.path,
3481
3498
  absolutePath: file.absolutePath
3482
3499
  }, content);
3483
- if (!chunksMatch(expectedChunks, file.chunks)) return false;
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;
3484
3502
  } catch {
3485
3503
  return false;
3486
3504
  }
@@ -3520,10 +3538,14 @@ var LocalContextIndex = class {
3520
3538
  return matches.length === 1 ? { root, path, absolutePath } : void 0;
3521
3539
  }
3522
3540
  rank(query, topK) {
3523
- const chunks = (this.index?.files ?? []).flatMap((file) => file.chunks);
3541
+ const index = this.index;
3542
+ const files = index?.files ?? [];
3543
+ const chunks = files.flatMap((file) => file.chunks);
3524
3544
  if (!chunks.length) return [];
3525
- const terms = [...new Set(tokenize(query))];
3526
- if (!terms.length) return [];
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])];
3527
3549
  const queryTerms = new Set(terms);
3528
3550
  const documentFrequency = new Map([...queryTerms].map((term) => [term, 0]));
3529
3551
  for (const chunk of chunks) {
@@ -3532,22 +3554,51 @@ var LocalContextIndex = class {
3532
3554
  }
3533
3555
  }
3534
3556
  const averageLength = chunks.reduce((sum, chunk) => sum + chunk.tokens.length, 0) / Math.max(chunks.length, 1);
3535
- return chunks.map((chunk) => ({ chunk, score: scoreChunk(
3536
- chunk,
3537
- terms,
3538
- query,
3539
- documentFrequency,
3540
- chunks.length,
3541
- averageLength
3542
- ) })).filter(({ score }) => score > 0).sort((left, right) => right.score - left.score || left.chunk.absolutePath.localeCompare(right.chunk.absolutePath)).slice(0, topK).map(({ chunk, score }) => ({
3543
- path: chunk.absolutePath,
3544
- startLine: chunk.startLine,
3545
- endLine: chunk.endLine,
3546
- content: chunk.content,
3547
- score,
3548
- source: "local-bm25+path+symbol",
3549
- ...chunk.symbol ? { symbol: chunk.symbol } : {}
3550
- }));
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
+ });
3551
3602
  }
3552
3603
  async hitsAreCurrent(hits) {
3553
3604
  const files = new Map((this.index?.files ?? []).map((file) => [file.absolutePath, file]));
@@ -3571,10 +3622,17 @@ var LocalContextIndex = class {
3571
3622
  }
3572
3623
  getCachedHits(query, topK) {
3573
3624
  const generation = this.index?.generation;
3574
- if (!generation) return void 0;
3625
+ if (!generation) {
3626
+ this.queryCacheMisses += 1;
3627
+ return void 0;
3628
+ }
3575
3629
  const key = `${generation}\0${topK}\0${query}`;
3576
3630
  const cached = this.queryCache.get(key);
3577
- if (!cached) return void 0;
3631
+ if (!cached) {
3632
+ this.queryCacheMisses += 1;
3633
+ return void 0;
3634
+ }
3635
+ this.queryCacheHits += 1;
3578
3636
  this.queryCache.delete(key);
3579
3637
  this.queryCache.set(key, cached);
3580
3638
  return cloneHits(cached);
@@ -3670,6 +3728,9 @@ function chunksMatch(expected, actual) {
3670
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]);
3671
3729
  });
3672
3730
  }
3731
+ function sameStrings(expected, actual) {
3732
+ return expected.length === actual.length && expected.every((value, index) => value === actual[index]);
3733
+ }
3673
3734
  function existingBuildResult(status, started) {
3674
3735
  if (!status.available || !status.generation) {
3675
3736
  throw new Error("The existing local context index is unavailable.");
@@ -3724,7 +3785,8 @@ function packContextHits(hits, roots, maxTokens, engine) {
3724
3785
  const text = selected.map((hit) => {
3725
3786
  const shownPath = workspaceAliasPath(hit.path, roots);
3726
3787
  const symbol = hit.symbol ? ` symbol="${escapeAttribute(hit.symbol)}"` : "";
3727
- return `<code path="${escapeAttribute(shownPath)}" lines="${hit.startLine}-${hit.endLine}" score="${hit.score.toFixed(3)}"${symbol}>
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}>
3728
3790
  ${hit.content}
3729
3791
  </code>`;
3730
3792
  }).join("\n\n");
@@ -3740,7 +3802,17 @@ ${hit.content}
3740
3802
  };
3741
3803
  }
3742
3804
  function cloneHits(hits) {
3743
- return hits.map((hit) => ({ ...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
+ }));
3744
3816
  }
3745
3817
  function cloneDuplicationBaseline(baseline) {
3746
3818
  return {
@@ -3842,6 +3914,109 @@ function detectSymbol(lines) {
3842
3914
  }
3843
3915
  return void 0;
3844
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
+ }
3845
4020
  function tokenize(input2) {
3846
4021
  const normalized = input2.replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([\p{Ll}])([\p{Lu}])/gu, "$1 $2").toLocaleLowerCase();
3847
4022
  const base = normalized.match(/[\p{L}\p{N}_-]+/gu) ?? [];
@@ -3857,24 +4032,123 @@ function tokenize(input2) {
3857
4032
  }
3858
4033
  return output2;
3859
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
+ }
3860
4127
  function scoreChunk(chunk, terms, rawQuery, documentFrequency, documentCount, averageLength) {
3861
4128
  const frequencies = /* @__PURE__ */ new Map();
3862
4129
  for (const token of chunk.tokens) frequencies.set(token, (frequencies.get(token) ?? 0) + 1);
3863
4130
  const k1 = 1.2;
3864
4131
  const b = 0.75;
3865
- let score = 0;
4132
+ let bm25 = 0;
4133
+ let path = 0;
4134
+ let symbol = 0;
4135
+ const matchedTerms = [];
3866
4136
  for (const term of terms) {
3867
4137
  const frequency = frequencies.get(term) ?? 0;
3868
4138
  if (!frequency) continue;
4139
+ matchedTerms.push(term);
3869
4140
  const df = documentFrequency.get(term) ?? 0;
3870
4141
  const idf = Math.log(1 + (documentCount - df + 0.5) / (df + 0.5));
3871
- score += idf * (frequency * (k1 + 1) / (frequency + k1 * (1 - b + b * chunk.tokens.length / averageLength)));
3872
- if (chunk.path.toLocaleLowerCase().includes(term)) score += 1.5;
3873
- if (chunk.symbol?.toLocaleLowerCase().includes(term)) score += 2.5;
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;
3874
4145
  }
3875
4146
  const phrase = rawQuery.trim().toLocaleLowerCase();
3876
- if (phrase.length > 3 && chunk.content.toLocaleLowerCase().includes(phrase)) score += 3;
3877
- return score;
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));
3878
4152
  }
3879
4153
 
3880
4154
  // src/context/context-engine.ts
@@ -3985,7 +4259,10 @@ function formatContextHits(hits, roots) {
3985
4259
  return hits.map((hit) => {
3986
4260
  const path = workspaceAliasPath(hit.path, roots);
3987
4261
  const symbol = hit.symbol ? ` ${hit.symbol}` : "";
3988
- return `[${hit.source} ${hit.score.toFixed(3)}]${symbol} ${path}:${hit.startLine}-${hit.endLine}`;
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}`;
3989
4266
  }).join("\n");
3990
4267
  }
3991
4268
 
@@ -9715,7 +9992,7 @@ async function evaluateReuseGate(input2) {
9715
9992
  candidates.push({
9716
9993
  path: hit.path,
9717
9994
  ...hit.symbol ? { symbol: hit.symbol.slice(0, 160) } : {},
9718
- score: roundScore(hit.score),
9995
+ score: roundScore2(hit.score),
9719
9996
  read
9720
9997
  });
9721
9998
  }
@@ -9823,7 +10100,7 @@ function unresolvedReceipt(input2, queryHash, targetPaths, trigger, detail) {
9823
10100
  function hash3(value) {
9824
10101
  return createHash13("sha256").update(value).digest("hex");
9825
10102
  }
9826
- function roundScore(value) {
10103
+ function roundScore2(value) {
9827
10104
  return Number.isFinite(value) ? Math.round(value * 1e3) / 1e3 : 0;
9828
10105
  }
9829
10106
 
@@ -10085,6 +10362,7 @@ var AgentRunner = class {
10085
10362
  };
10086
10363
  const changeSequenceAtStart = this.changeSequence;
10087
10364
  const runStartedAt = (/* @__PURE__ */ new Date()).toISOString();
10365
+ const loadedProgressiveTools = /* @__PURE__ */ new Set();
10088
10366
  const runChangedFiles = /* @__PURE__ */ new Set();
10089
10367
  const verificationEvidence = [];
10090
10368
  const toolRecovery = new ToolRecoveryController();
@@ -10248,7 +10526,9 @@ var AgentRunner = class {
10248
10526
  activeDuplicationMatchIds(
10249
10527
  this.session.audit ?? [],
10250
10528
  this.session.duplicationSuppressions ?? []
10251
- ).size > 0
10529
+ ).size > 0,
10530
+ request,
10531
+ loadedProgressiveTools
10252
10532
  );
10253
10533
  const estimatedInputTokens = estimateMessages2(messages) + estimateToolDefinitions(visibleTools);
10254
10534
  if (availableTokens <= 0 || estimatedInputTokens >= availableTokens) {
@@ -10313,7 +10593,10 @@ var AgentRunner = class {
10313
10593
  },
10314
10594
  inputSource: actualInputTokens === void 0 ? "estimated" : "actual",
10315
10595
  outputSource: actualOutputTokens === void 0 ? "estimated" : "actual",
10316
- tools: { loaded: visibleTools.map((tool) => tool.name), deferredCount: 0 },
10596
+ tools: {
10597
+ loaded: visibleTools.map((tool) => tool.name),
10598
+ deferredCount: Math.max(0, this.tools.definitions().filter((tool) => tool.progressive).length - visibleTools.filter((tool) => tool.progressive).length)
10599
+ },
10317
10600
  retrieval: tokenRetrievalReceipt(packed)
10318
10601
  });
10319
10602
  await emit({
@@ -11079,10 +11362,25 @@ ${content}` : content,
11079
11362
  ...failure ? { metadata: { failure } } : {}
11080
11363
  };
11081
11364
  }
11082
- function visibleToolDefinitions(tools, askMode, contractEnabled, artifactReadAvailable, duplicationAvailable) {
11083
- return tools.definitions().filter(
11365
+ function visibleToolDefinitions(tools, askMode, contractEnabled, artifactReadAvailable, duplicationAvailable, request, loadedProgressiveTools) {
11366
+ const eligible = tools.definitions().filter(
11084
11367
  (tool) => (!askMode || tool.category === "read") && (contractEnabled || tool.name !== "task_contract") && (artifactReadAvailable || tool.name !== "read_tool_artifact") && (duplicationAvailable || tool.name !== "duplication_audit")
11085
11368
  );
11369
+ const progressive = eligible.filter((tool) => tool.progressive);
11370
+ if (progressive.length <= 8) return eligible;
11371
+ for (const tool of selectProgressiveTools(progressive, request, 8)) {
11372
+ loadedProgressiveTools.add(tool.name);
11373
+ }
11374
+ return eligible.filter((tool) => !tool.progressive || loadedProgressiveTools.has(tool.name));
11375
+ }
11376
+ function selectProgressiveTools(tools, request, limit) {
11377
+ const terms = new Set(request.toLocaleLowerCase().match(/[\p{L}\p{N}_-]{2,}/gu) ?? []);
11378
+ return tools.map((tool) => {
11379
+ const searchable = `${tool.name.replaceAll("_", " ")} ${tool.description}`.toLocaleLowerCase();
11380
+ let score = 0;
11381
+ for (const term of terms) if (searchable.includes(term)) score += term.length;
11382
+ return { tool, score };
11383
+ }).sort((left, right) => right.score - left.score || left.tool.name.localeCompare(right.tool.name)).slice(0, limit).map(({ tool }) => tool);
11086
11384
  }
11087
11385
  function formatToolError(error) {
11088
11386
  const normalized = toError(error);
@@ -18995,7 +19293,8 @@ function createMcpToolAdapter(options) {
18995
19293
  // MCP servers are an external trust boundary. Read-only annotations are
18996
19294
  // hints from that server and must not lower the local permission level.
18997
19295
  category: "network",
18998
- inputSchema: inputSchema13
19296
+ inputSchema: inputSchema13,
19297
+ progressive: true
18999
19298
  },
19000
19299
  permissionCategories: () => ["network"],
19001
19300
  async execute(arguments_, context) {