sigmap 8.28.1 → 8.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +539 -824
- package/CHANGELOG.md +28 -0
- package/README.md +2 -2
- package/gen-context.js +650 -137
- package/llms-full.txt +2 -2
- package/llms.txt +2 -2
- package/package.json +5 -3
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/src/eval/runner.js +18 -49
- package/src/graph/builder.js +4 -3
- package/src/graph/call-graph.js +7 -3
- package/src/graph/path-key.js +26 -0
- package/src/mcp/server.js +1 -1
- package/src/retrieval/bm25.js +56 -6
- package/src/retrieval/module-doc.js +120 -0
- package/src/retrieval/ranker.js +189 -65
- package/src/retrieval/sig-index-store.js +95 -0
package/gen-context.js
CHANGED
|
@@ -4623,47 +4623,12 @@ __factories["./src/eval/runner"] = function(module, exports) {
|
|
|
4623
4623
|
* @returns {Map<string, string[]>}
|
|
4624
4624
|
*/
|
|
4625
4625
|
function buildSigIndex(cwd) {
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
const lines = content.split('\n');
|
|
4633
|
-
|
|
4634
|
-
let currentFile = null;
|
|
4635
|
-
let inBlock = false;
|
|
4636
|
-
let sigs = [];
|
|
4637
|
-
|
|
4638
|
-
for (const line of lines) {
|
|
4639
|
-
// Section header: ### path/to/file.js
|
|
4640
|
-
const headerMatch = line.match(/^###\s+(\S+\.\w+)\s*$/);
|
|
4641
|
-
if (headerMatch) {
|
|
4642
|
-
if (currentFile !== null) {
|
|
4643
|
-
index.set(currentFile, sigs);
|
|
4644
|
-
}
|
|
4645
|
-
currentFile = headerMatch[1];
|
|
4646
|
-
sigs = [];
|
|
4647
|
-
inBlock = false;
|
|
4648
|
-
continue;
|
|
4649
|
-
}
|
|
4650
|
-
|
|
4651
|
-
if (line.startsWith('```')) {
|
|
4652
|
-
inBlock = !inBlock;
|
|
4653
|
-
continue;
|
|
4654
|
-
}
|
|
4655
|
-
|
|
4656
|
-
if (inBlock && currentFile && line.trim()) {
|
|
4657
|
-
sigs.push(line.trim());
|
|
4658
|
-
}
|
|
4659
|
-
}
|
|
4660
|
-
|
|
4661
|
-
// Flush last file
|
|
4662
|
-
if (currentFile !== null) {
|
|
4663
|
-
index.set(currentFile, sigs);
|
|
4664
|
-
}
|
|
4665
|
-
|
|
4666
|
-
return index;
|
|
4626
|
+
// Delegate to the production index builder. This used to parse
|
|
4627
|
+
// .github/copilot-instructions.md directly — a second parallel implementation
|
|
4628
|
+
// that saw only the token-BUDGETED view, ignored the other adapters, the
|
|
4629
|
+
// hot-cold/per-module strategy splits, and the complete retrieval index. The
|
|
4630
|
+
// corpus therefore scored a smaller index than `sigmap ask` actually uses.
|
|
4631
|
+
return __require('./src/retrieval/ranker').buildSigIndex(cwd);
|
|
4667
4632
|
}
|
|
4668
4633
|
|
|
4669
4634
|
// ---------------------------------------------------------------------------
|
|
@@ -4681,12 +4646,13 @@ __factories["./src/eval/runner"] = function(module, exports) {
|
|
|
4681
4646
|
* @param {number} topK
|
|
4682
4647
|
* @returns {{ file: string, score: number, sigs: string[] }[]}
|
|
4683
4648
|
*/
|
|
4684
|
-
function rank(query, index, topK = 10) {
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4649
|
+
function rank(query, index, topK = 10, opts = {}) {
|
|
4650
|
+
// Measure the ranker users actually hit. This used to call bm25rank directly,
|
|
4651
|
+
// which meant the corpus scored a parallel implementation with no penalties,
|
|
4652
|
+
// graph boost, recency or learned weights — so no ranking regression in
|
|
4653
|
+
// src/retrieval/ranker.js could ever show up in the benchmark numbers.
|
|
4654
|
+
const { rank: prodRank } = __require('./src/retrieval/ranker');
|
|
4655
|
+
return prodRank(query, index, Object.assign({ topK }, opts)).slice(0, topK);
|
|
4690
4656
|
}
|
|
4691
4657
|
|
|
4692
4658
|
// ---------------------------------------------------------------------------
|
|
@@ -4773,11 +4739,14 @@ __factories["./src/eval/runner"] = function(module, exports) {
|
|
|
4773
4739
|
|
|
4774
4740
|
// Build index once (re-used across all tasks in the same repo)
|
|
4775
4741
|
const index = buildSigIndex(cwd);
|
|
4742
|
+
// Import graph built once too — the hop-1/hop-2 boost is part of what ships.
|
|
4743
|
+
let graph = null;
|
|
4744
|
+
try { graph = __require('./src/graph/builder').buildFromCwd(cwd); } catch (_) {}
|
|
4776
4745
|
|
|
4777
4746
|
const taskResults = [];
|
|
4778
4747
|
for (const task of tasks) {
|
|
4779
|
-
const
|
|
4780
|
-
const
|
|
4748
|
+
const topResult = rank(task.query, index, topK, { cwd, graph, learned: opts.learned });
|
|
4749
|
+
const ranked = topResult.map((r) => r.file);
|
|
4781
4750
|
const tokens = topResult.reduce((sum, r) => sum + estimateTokens(r.sigs), 0);
|
|
4782
4751
|
|
|
4783
4752
|
const { hitAtK, reciprocalRank, precisionAtK } = __require('./src/eval/scorer');
|
|
@@ -11345,10 +11314,11 @@ __factories["./src/graph/builder"] = function(module, exports) {
|
|
|
11345
11314
|
const fs = require('fs');
|
|
11346
11315
|
const path = require('path');
|
|
11347
11316
|
|
|
11348
|
-
//
|
|
11349
|
-
//
|
|
11317
|
+
// Cross-platform node key. Delegates to the ONE shared definition so this graph
|
|
11318
|
+
// and the call-graph cannot drift apart again (see src/graph/path-key.js).
|
|
11319
|
+
const { graphKey } = __require('./src/graph/path-key');
|
|
11350
11320
|
function normalizePath(p) {
|
|
11351
|
-
return
|
|
11321
|
+
return graphKey(p);
|
|
11352
11322
|
}
|
|
11353
11323
|
|
|
11354
11324
|
// ---------------------------------------------------------------------------
|
|
@@ -11877,7 +11847,8 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
|
|
|
11877
11847
|
'synchronized',
|
|
11878
11848
|
]);
|
|
11879
11849
|
|
|
11880
|
-
|
|
11850
|
+
const { graphKey } = __require('./src/graph/path-key');
|
|
11851
|
+
function normalizePath(p) { return graphKey(p); }
|
|
11881
11852
|
function toRel(cwd, f) { return path.relative(cwd, f).replace(/\\/g, '/'); }
|
|
11882
11853
|
function symId(cwd, absFile, name) { return `${toRel(cwd, absFile)}#${name}`; }
|
|
11883
11854
|
|
|
@@ -12309,8 +12280,11 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
|
|
|
12309
12280
|
for (const calleeId of calleeIds) {
|
|
12310
12281
|
const calleeDef = graph.defs.get(calleeId);
|
|
12311
12282
|
if (!calleeDef || calleeDef.file === callerDef.file) continue;
|
|
12312
|
-
|
|
12313
|
-
|
|
12283
|
+
// Keyed through graphKey so the file-level call graph shares ONE key space
|
|
12284
|
+
// with the import graph. Previously this kept case while builder.js
|
|
12285
|
+
// lowercased, so a lookup correct for one silently missed on the other.
|
|
12286
|
+
const a = graphKey(path.resolve(cwd, callerDef.file));
|
|
12287
|
+
const b = graphKey(path.resolve(cwd, calleeDef.file));
|
|
12314
12288
|
add(a, b);
|
|
12315
12289
|
add(b, a);
|
|
12316
12290
|
}
|
|
@@ -12716,6 +12690,36 @@ __factories["./src/graph/impact"] = function(module, exports) {
|
|
|
12716
12690
|
|
|
12717
12691
|
};
|
|
12718
12692
|
|
|
12693
|
+
// ── ./src/graph/path-key ──
|
|
12694
|
+
__factories["./src/graph/path-key"] = function(module, exports) {
|
|
12695
|
+
|
|
12696
|
+
/**
|
|
12697
|
+
* The single definition of a graph node key.
|
|
12698
|
+
*
|
|
12699
|
+
* src/graph/builder.js and src/graph/call-graph.js each used to normalise paths
|
|
12700
|
+
* their own way — builder lowercased, call-graph did not — so a lookup written
|
|
12701
|
+
* for one silently missed on the other. That divergence disabled the import
|
|
12702
|
+
* boost on every repo whose path contains an uppercase letter, and later caused
|
|
12703
|
+
* a "fix" for one graph to break the other. Both now key through this function,
|
|
12704
|
+
* so there is one convention rather than two conventions and a convention.
|
|
12705
|
+
*
|
|
12706
|
+
* Lowercasing keeps lookups stable across case-insensitive filesystems (macOS,
|
|
12707
|
+
* Windows), where the same file legitimately arrives spelled two ways.
|
|
12708
|
+
*
|
|
12709
|
+
* Zero-dependency, pure, bundle-safe.
|
|
12710
|
+
*/
|
|
12711
|
+
|
|
12712
|
+
const path = require('path');
|
|
12713
|
+
|
|
12714
|
+
/** Canonical key for a filesystem path used as a graph node. */
|
|
12715
|
+
function graphKey(p) {
|
|
12716
|
+
return path.normalize(String(p)).toLowerCase();
|
|
12717
|
+
}
|
|
12718
|
+
|
|
12719
|
+
module.exports = { graphKey };
|
|
12720
|
+
|
|
12721
|
+
};
|
|
12722
|
+
|
|
12719
12723
|
// ── ./src/health/scorer ──
|
|
12720
12724
|
__factories["./src/health/scorer"] = function(module, exports) {
|
|
12721
12725
|
|
|
@@ -15393,7 +15397,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
15393
15397
|
|
|
15394
15398
|
const SERVER_INFO = {
|
|
15395
15399
|
name: 'sigmap',
|
|
15396
|
-
version: '8.
|
|
15400
|
+
version: '8.29.0',
|
|
15397
15401
|
description: 'SigMap MCP server — code signatures on demand',
|
|
15398
15402
|
};
|
|
15399
15403
|
|
|
@@ -16357,6 +16361,33 @@ __factories["./src/retrieval/bm25"] = function(module, exports) {
|
|
|
16357
16361
|
// the literal query token always outranks a synonym-only match.
|
|
16358
16362
|
const EXPANSION_WEIGHT = 0.15;
|
|
16359
16363
|
|
|
16364
|
+
// Module-doc prose is indexed as a `# module: ...` pseudo-signature (index-only,
|
|
16365
|
+
// see src/retrieval/module-doc.js). Per token it is a weaker relevance signal
|
|
16366
|
+
// than a real signature — descriptive rather than definitional — so it is scored
|
|
16367
|
+
// as its own BM25F field rather than pooled with the code terms.
|
|
16368
|
+
const MODULE_DOC_RE = /^#\s*(module|docs):/;
|
|
16369
|
+
|
|
16370
|
+
// Line anchors are metadata, not content, and this ranker is documented as
|
|
16371
|
+
// anchor-invariant. The previous strip was end-anchored, so it only fired when
|
|
16372
|
+
// the anchor was the last thing on the line — but extractors append a doc hint
|
|
16373
|
+
// AFTER it ("... :27-59 # Compute a normalized centrality score"). 27% of
|
|
16374
|
+
// signatures therefore leaked their line numbers into the term space as tokens
|
|
16375
|
+
// like "27" and "59": 840 junk terms, inflating document length for exactly the
|
|
16376
|
+
// well-documented files, which BM25 then penalised via length normalisation.
|
|
16377
|
+
const ANCHOR_RE = /\s*:\d+(?:-\d+)?(?=\s|$)/g;
|
|
16378
|
+
|
|
16379
|
+
function stripAnchor(line) {
|
|
16380
|
+
return String(line).replace(ANCHOR_RE, '');
|
|
16381
|
+
}
|
|
16382
|
+
// Tuned on the 30-task leak-free corpus. The 0.5-0.8 band is flat
|
|
16383
|
+
// (hit@5 63.3-66.7%, easy MRR steady at 0.825); adjacent values swing by up to
|
|
16384
|
+
// 6.7pp, which at 30 tasks is literally two tasks — noise, not signal. 0.6 is
|
|
16385
|
+
// chosen from the middle of that band rather than at its peak, because a
|
|
16386
|
+
// per-token weight below 1 is the principled position (prose is descriptive,
|
|
16387
|
+
// a signature is definitional) and picking the argmax of a 30-task sweep is
|
|
16388
|
+
// how you overfit a benchmark.
|
|
16389
|
+
const DOC_WEIGHT = 0.6;
|
|
16390
|
+
|
|
16360
16391
|
// Build a stemmed lookup: stem(member) → Set of the group's other stemmed members.
|
|
16361
16392
|
const EXPANSIONS = (() => {
|
|
16362
16393
|
const map = new Map();
|
|
@@ -16400,22 +16431,45 @@ __factories["./src/retrieval/bm25"] = function(module, exports) {
|
|
|
16400
16431
|
* @param {{ file: string, sigs: string[] }[]} candidates
|
|
16401
16432
|
* @returns {Array<object & { score: number }>}
|
|
16402
16433
|
*/
|
|
16403
|
-
function bm25rank(query, candidates) {
|
|
16434
|
+
function bm25rank(query, candidates, opts) {
|
|
16404
16435
|
if (!Array.isArray(candidates) || candidates.length === 0) return [];
|
|
16405
16436
|
|
|
16406
16437
|
const k1 = 1.5;
|
|
16407
16438
|
const b = 0.75;
|
|
16408
16439
|
|
|
16440
|
+
const docWeight = (opts && typeof opts.docWeight === 'number') ? opts.docWeight : DOC_WEIGHT;
|
|
16441
|
+
|
|
16409
16442
|
const docs = candidates.map((c) => {
|
|
16410
16443
|
const pathToks = tokenize(c.file || '');
|
|
16411
16444
|
// Ranking is anchor-invariant: `:start-end` line anchors are metadata,
|
|
16412
16445
|
// not content — strip them before tokenizing so adding anchors to an
|
|
16413
16446
|
// extractor never shifts BM25 length normalization or token counts.
|
|
16414
|
-
|
|
16415
|
-
|
|
16447
|
+
// BM25F-style fields. Module-doc prose and code signatures are different
|
|
16448
|
+
// kinds of evidence and must not share one term-frequency pool: prose is
|
|
16449
|
+
// ~30% of all indexed tokens, and a short file with a long header (few
|
|
16450
|
+
// signatures, lots of description) otherwise wins unrelated queries purely
|
|
16451
|
+
// through length normalisation.
|
|
16452
|
+
const docLines = [];
|
|
16453
|
+
const codeLines = [];
|
|
16454
|
+
for (const line of (c.sigs || [])) (MODULE_DOC_RE.test(line) ? docLines : codeLines).push(line);
|
|
16455
|
+
// TRIED AND REJECTED: splitting the declared symbol NAME into its own
|
|
16456
|
+
// weighted BM25F field, on the IR prior that a name is a "title" and params
|
|
16457
|
+
// are "body". Swept 1.0-4.0. hit@5 on the mined corpus rose 60.9% -> 65.2%,
|
|
16458
|
+
// which is a single task crossing the rank-5 line — over 113 combined tasks
|
|
16459
|
+
// hit@1 fell 52.2% -> 51.3%, hit@3 fell 66.4% -> 65.5%, hit@10 was identical
|
|
16460
|
+
// and MRR dropped. It moves correct answers DOWN and happens to nudge one
|
|
16461
|
+
// past a cutoff. A hit@5-only view would have shipped this.
|
|
16462
|
+
const codeToks = tokenize(codeLines.map((x) => stripAnchor(x)).join(' '));
|
|
16463
|
+
const docToks = tokenize(docLines.join(' '));
|
|
16416
16464
|
const tf = new Map();
|
|
16417
|
-
for (const t of toks) tf.set(t, (tf.get(t) || 0) +
|
|
16418
|
-
|
|
16465
|
+
const addField = (toks, weight) => { for (const t of toks) tf.set(t, (tf.get(t) || 0) + weight); };
|
|
16466
|
+
addField(codeToks, 1);
|
|
16467
|
+
addField(pathToks, PATH_BOOST);
|
|
16468
|
+
addField(docToks, docWeight);
|
|
16469
|
+
// Length accumulates with the SAME weights, or a field's influence leaks
|
|
16470
|
+
// back in through the normalisation term.
|
|
16471
|
+
const len = codeToks.length + (PATH_BOOST * pathToks.length) + (docWeight * docToks.length);
|
|
16472
|
+
return { cand: c, tf, len };
|
|
16419
16473
|
});
|
|
16420
16474
|
|
|
16421
16475
|
const N = docs.length || 1;
|
|
@@ -16444,7 +16498,7 @@ __factories["./src/retrieval/bm25"] = function(module, exports) {
|
|
|
16444
16498
|
.sort((a, c) => c.score - a.score || String(a.file).localeCompare(String(c.file)));
|
|
16445
16499
|
}
|
|
16446
16500
|
|
|
16447
|
-
module.exports = { tokenize, stem, bm25rank, PATH_BOOST, STOP, expandQuery, EXPANSIONS, EXPANSION_WEIGHT };
|
|
16501
|
+
module.exports = { tokenize, stem, bm25rank, PATH_BOOST, STOP, expandQuery, EXPANSIONS, EXPANSION_WEIGHT, DOC_WEIGHT, MODULE_DOC_RE, stripAnchor };
|
|
16448
16502
|
|
|
16449
16503
|
};
|
|
16450
16504
|
|
|
@@ -16507,6 +16561,130 @@ __factories["./src/retrieval/enrich-from-maps"] = function(module, exports) {
|
|
|
16507
16561
|
|
|
16508
16562
|
};
|
|
16509
16563
|
|
|
16564
|
+
// ── ./src/retrieval/module-doc ──
|
|
16565
|
+
__factories["./src/retrieval/module-doc"] = function(module, exports) {
|
|
16566
|
+
|
|
16567
|
+
/**
|
|
16568
|
+
* Module-level documentation extractor (retrieval only).
|
|
16569
|
+
*
|
|
16570
|
+
* Signatures describe a file's SHAPE — names, params, return types. A module's
|
|
16571
|
+
* leading comment describes its PURPOSE, in prose, using the words a person
|
|
16572
|
+
* would actually search with. That prose is the bridge between a behavioural
|
|
16573
|
+
* query ("what fraction of the repo made it into the output") and the code
|
|
16574
|
+
* that implements it (`coverageScore(cwd, fileEntries, config)`), which shares
|
|
16575
|
+
* not one token with the query.
|
|
16576
|
+
*
|
|
16577
|
+
* This text is added to the RETRIEVAL INDEX ONLY — never to the generated
|
|
16578
|
+
* context file. The prompt artifact is token-budgeted and prose is expensive
|
|
16579
|
+
* there; the index is not injected into any prompt, so it can afford the words
|
|
16580
|
+
* that make a file findable.
|
|
16581
|
+
*
|
|
16582
|
+
* Zero-dependency, pure, bundle-safe.
|
|
16583
|
+
*/
|
|
16584
|
+
|
|
16585
|
+
// Enough to characterise a module without letting one verbose header dominate
|
|
16586
|
+
// BM25 length normalisation for the whole corpus.
|
|
16587
|
+
const MAX_CHARS = 400;
|
|
16588
|
+
const MAX_SCAN_LINES = 60;
|
|
16589
|
+
|
|
16590
|
+
// Legal boilerplate is high-frequency noise: it appears in many files, shares no
|
|
16591
|
+
// vocabulary with real queries, and would flatten idf across the corpus.
|
|
16592
|
+
const BOILERPLATE = /\b(copyright|licensed under|SPDX-License|all rights reserved|permission is hereby granted)\b/i;
|
|
16593
|
+
|
|
16594
|
+
const BLOCK_LANGS = new Set(['js', 'jsx', 'ts', 'tsx', 'java', 'go', 'rs', 'kt', 'swift', 'scala', 'cs', 'php', 'dart', 'c', 'cpp', 'h']);
|
|
16595
|
+
const HASH_LANGS = new Set(['py', 'rb', 'r', 'sh', 'yml', 'yaml', 'toml']);
|
|
16596
|
+
|
|
16597
|
+
function _extOf(filePath) {
|
|
16598
|
+
const m = String(filePath).match(/\.([A-Za-z0-9]+)$/);
|
|
16599
|
+
return m ? m[1].toLowerCase() : '';
|
|
16600
|
+
}
|
|
16601
|
+
|
|
16602
|
+
/** Strip comment furniture, JSDoc tags, and markup from one raw comment line. */
|
|
16603
|
+
function _cleanLine(line) {
|
|
16604
|
+
return String(line)
|
|
16605
|
+
.replace(/^\s*[/*#-]+\s?/, '') // leading // /* * # ---
|
|
16606
|
+
.replace(/\*+\/\s*$/, '') // trailing */
|
|
16607
|
+
.replace(/^\s*@\w+.*$/, '') // @param / @returns tag lines
|
|
16608
|
+
.replace(/[*_`]/g, '') // markdown emphasis / code ticks
|
|
16609
|
+
.trim();
|
|
16610
|
+
}
|
|
16611
|
+
|
|
16612
|
+
/**
|
|
16613
|
+
* Extract a module's leading documentation prose.
|
|
16614
|
+
*
|
|
16615
|
+
* @param {string} src file contents
|
|
16616
|
+
* @param {string} filePath used only to pick a comment syntax
|
|
16617
|
+
* @returns {string} collapsed prose, capped, or '' when there is none
|
|
16618
|
+
*/
|
|
16619
|
+
function extractModuleDoc(src, filePath) {
|
|
16620
|
+
if (!src || typeof src !== 'string') return '';
|
|
16621
|
+
const ext = _extOf(filePath);
|
|
16622
|
+
const lines = src.split('\n', MAX_SCAN_LINES);
|
|
16623
|
+
|
|
16624
|
+
const collected = [];
|
|
16625
|
+
let inBlock = false;
|
|
16626
|
+
let started = false;
|
|
16627
|
+
|
|
16628
|
+
for (const raw of lines) {
|
|
16629
|
+
const line = raw.trim();
|
|
16630
|
+
if (!started) {
|
|
16631
|
+
// Skip preamble that precedes the real header comment.
|
|
16632
|
+
if (!line) continue;
|
|
16633
|
+
if (line.startsWith('#!')) continue; // shebang
|
|
16634
|
+
if (/^['"]use strict['"];?$/.test(line)) continue;
|
|
16635
|
+
if (/^(package|import|from|using|#include)\b/.test(line)) continue;
|
|
16636
|
+
}
|
|
16637
|
+
|
|
16638
|
+
if (BLOCK_LANGS.has(ext) || ext === '') {
|
|
16639
|
+
if (!inBlock && line.startsWith('/*')) { inBlock = true; started = true; }
|
|
16640
|
+
if (inBlock) {
|
|
16641
|
+
const cleaned = _cleanLine(line);
|
|
16642
|
+
if (cleaned) collected.push(cleaned);
|
|
16643
|
+
if (line.includes('*/')) break;
|
|
16644
|
+
continue;
|
|
16645
|
+
}
|
|
16646
|
+
if (line.startsWith('//')) { // run of // lines
|
|
16647
|
+
started = true;
|
|
16648
|
+
const cleaned = _cleanLine(line);
|
|
16649
|
+
if (cleaned) collected.push(cleaned);
|
|
16650
|
+
continue;
|
|
16651
|
+
}
|
|
16652
|
+
if (started || collected.length) break;
|
|
16653
|
+
break; // first real code — no header
|
|
16654
|
+
}
|
|
16655
|
+
|
|
16656
|
+
if (HASH_LANGS.has(ext)) {
|
|
16657
|
+
if (/^("""|''')/.test(line)) { // python docstring
|
|
16658
|
+
started = true; inBlock = !inBlock;
|
|
16659
|
+
const cleaned = _cleanLine(line.replace(/^("""|''')/, '').replace(/("""|''')$/, ''));
|
|
16660
|
+
if (cleaned) collected.push(cleaned);
|
|
16661
|
+
if (!inBlock) break;
|
|
16662
|
+
continue;
|
|
16663
|
+
}
|
|
16664
|
+
if (inBlock) { const c = _cleanLine(line); if (c) collected.push(c); continue; }
|
|
16665
|
+
if (line.startsWith('#')) { started = true; const c = _cleanLine(line); if (c) collected.push(c); continue; }
|
|
16666
|
+
if (collected.length) break;
|
|
16667
|
+
break;
|
|
16668
|
+
}
|
|
16669
|
+
break;
|
|
16670
|
+
}
|
|
16671
|
+
|
|
16672
|
+
const text = collected.join(' ').replace(/\s+/g, ' ').trim();
|
|
16673
|
+
if (!text || BOILERPLATE.test(text)) return '';
|
|
16674
|
+
if (text.length <= MAX_CHARS) return text;
|
|
16675
|
+
return text.slice(0, MAX_CHARS).replace(/\s+\S*$/, ''); // never cut mid-word
|
|
16676
|
+
}
|
|
16677
|
+
|
|
16678
|
+
/** Render as an index-only pseudo-signature, or '' when there is no doc. */
|
|
16679
|
+
function moduleDocSig(src, filePath) {
|
|
16680
|
+
const doc = extractModuleDoc(src, filePath);
|
|
16681
|
+
return doc ? `# module: ${doc}` : '';
|
|
16682
|
+
}
|
|
16683
|
+
|
|
16684
|
+
module.exports = { extractModuleDoc, moduleDocSig, MAX_CHARS };
|
|
16685
|
+
|
|
16686
|
+
};
|
|
16687
|
+
|
|
16510
16688
|
// ── ./src/retrieval/ranker ──
|
|
16511
16689
|
__factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
16512
16690
|
|
|
@@ -16529,7 +16707,7 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
|
16529
16707
|
|
|
16530
16708
|
const { loadWeights } = __require('./src/learning/weights');
|
|
16531
16709
|
const { tokenize, STOP_WORDS } = __require('./src/retrieval/tokenizer');
|
|
16532
|
-
const { bm25rank } = __require('./src/retrieval/bm25');
|
|
16710
|
+
const { bm25rank, MODULE_DOC_RE } = __require('./src/retrieval/bm25');
|
|
16533
16711
|
|
|
16534
16712
|
// ---------------------------------------------------------------------------
|
|
16535
16713
|
// Default weights
|
|
@@ -16553,17 +16731,28 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
|
16553
16731
|
// Max additive prior for import-graph centrality (opt-in retrieval.centralityBlend)
|
|
16554
16732
|
const CENTRALITY_BLEND_WEIGHT = 0.3;
|
|
16555
16733
|
|
|
16556
|
-
//
|
|
16557
|
-
|
|
16558
|
-
|
|
16559
|
-
|
|
16560
|
-
|
|
16561
|
-
|
|
16562
|
-
|
|
16563
|
-
|
|
16564
|
-
|
|
16565
|
-
|
|
16566
|
-
|
|
16734
|
+
// Per-intent weight profiles were removed in favour of a single weight set.
|
|
16735
|
+
// They were provably inert: scoreFile's score was discarded by rank(), so the
|
|
16736
|
+
// profiles only ever reached the explain table. Once the signal WAS wired into
|
|
16737
|
+
// the score (see SIGNAL_BLEND below), a sweep over the leak-free hard corpus
|
|
16738
|
+
// showed intent-specific profiles produced byte-identical metrics to the flat
|
|
16739
|
+
// DEFAULT_WEIGHTS at every blend value — so they earn nothing and are gone.
|
|
16740
|
+
// `detectIntent` is retained: it is still reported to the user and is the right
|
|
16741
|
+
// hook for shaping OUTPUT depth later.
|
|
16742
|
+
|
|
16743
|
+
// How much the weighted keyword/symbol/path signal modulates the BM25 base.
|
|
16744
|
+
// Multiplicative and bounded, so it can only reorder files that already match —
|
|
16745
|
+
// it can never lift a zero-BM25 file into the results. Tuned on the leak-free
|
|
16746
|
+
// corpus: 0.5 gave hit@5 50.0% -> 56.7% and MRR 0.419 -> 0.447; higher values
|
|
16747
|
+
// held hit@5 but degraded MRR.
|
|
16748
|
+
const SIGNAL_BLEND = 0.5;
|
|
16749
|
+
|
|
16750
|
+
// TRIED AND REJECTED: a same-line co-occurrence bonus, on the theory that a file
|
|
16751
|
+
// declaring `parseAuthToken` should outrank one mentioning `parseAuth` and
|
|
16752
|
+
// `token` on separate lines. Swept 0.15-1.0: hit@5 did not move on either the
|
|
16753
|
+
// 90-task authored corpus or the 32-task mined one, and MRR degraded
|
|
16754
|
+
// monotonically as the weight rose. Signatures are short and dense enough that
|
|
16755
|
+
// BM25's bag already captures this. Not reinstated without new evidence.
|
|
16567
16756
|
|
|
16568
16757
|
// Penalty multipliers for negative signals
|
|
16569
16758
|
const PENALTY_SIGNALS = {
|
|
@@ -16573,12 +16762,36 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
|
16573
16762
|
nodeModules: 0.0, // node_modules (zero score)
|
|
16574
16763
|
};
|
|
16575
16764
|
|
|
16576
|
-
|
|
16765
|
+
// Query terms that mean the penalised category IS the target. Read from the
|
|
16766
|
+
// query tokens directly, NOT via detectIntent: that classifier is first-match-
|
|
16767
|
+
// wins over its pattern object, and `debug` precedes `test`, so "fix the failing
|
|
16768
|
+
// test" classifies as debug and never reaches the test branch.
|
|
16769
|
+
const WANTS_TESTS = new Set(['test', 'tests', 'spec', 'specs', 'unit', 'integration', 'e2e', 'assertion', 'assert', 'mock', 'fixture', 'coverage', 'testing']);
|
|
16770
|
+
const WANTS_DOCS = new Set(['doc', 'docs', 'documentation', 'readme', 'changelog', 'guide', 'tutorial']);
|
|
16771
|
+
|
|
16772
|
+
/** Which penalised categories the query is explicitly asking for. */
|
|
16773
|
+
function _queryWants(queryTokens) {
|
|
16774
|
+
const wants = { tests: false, docs: false };
|
|
16775
|
+
for (const t of queryTokens || []) {
|
|
16776
|
+
if (WANTS_TESTS.has(t)) wants.tests = true;
|
|
16777
|
+
if (WANTS_DOCS.has(t)) wants.docs = true;
|
|
16778
|
+
}
|
|
16779
|
+
return wants;
|
|
16780
|
+
}
|
|
16781
|
+
|
|
16782
|
+
function _computePenalty(filePath, wants) {
|
|
16577
16783
|
const pathLower = filePath.toLowerCase();
|
|
16578
16784
|
if (pathLower.includes('node_modules')) return PENALTY_SIGNALS.nodeModules;
|
|
16579
|
-
|
|
16785
|
+
// A penalty must never fire on the very thing the user asked for. Before
|
|
16786
|
+
// this, "write tests for the ranker" multiplied every test file by 0.4 —
|
|
16787
|
+
// the query and the penalty were pulling in opposite directions.
|
|
16788
|
+
if (/(^|\/)(test|tests|spec|__tests__|e2e)($|\/)/.test(pathLower) || /\.(test|spec)\./.test(pathLower)) {
|
|
16789
|
+
return (wants && wants.tests) ? 1.0 : PENALTY_SIGNALS.testFile;
|
|
16790
|
+
}
|
|
16580
16791
|
if (/(^|\/)(dist|build|\.next|\.nuxt|out|\.venv|venv)($|\/)/.test(pathLower)) return PENALTY_SIGNALS.generatedCode;
|
|
16581
|
-
if (/(^|\/)(docs|doc|readme|changelog)($|\/)/.test(pathLower))
|
|
16792
|
+
if (/(^|\/)(docs|doc|readme|changelog)($|\/)/.test(pathLower)) {
|
|
16793
|
+
return (wants && wants.docs) ? 1.0 : PENALTY_SIGNALS.docsFile;
|
|
16794
|
+
}
|
|
16582
16795
|
return 1.0;
|
|
16583
16796
|
}
|
|
16584
16797
|
|
|
@@ -16597,6 +16810,26 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
|
16597
16810
|
}
|
|
16598
16811
|
|
|
16599
16812
|
// Common utility paths that should be treated as hubs regardless of fanout
|
|
16813
|
+
// The graph builders disagree on key case: src/graph/builder.js lowercases every
|
|
16814
|
+
// node (normalizePath), while src/graph/call-graph.js keys by a case-preserving
|
|
16815
|
+
// path.resolve. Assuming either one breaks the other, so every graph lookup in
|
|
16816
|
+
// this file probes both forms — the same thing the centrality blend already does.
|
|
16817
|
+
function _graphKeys(p) {
|
|
16818
|
+
const norm = require('path').normalize(p);
|
|
16819
|
+
const lower = norm.toLowerCase();
|
|
16820
|
+
return lower === norm ? [norm] : [norm, lower];
|
|
16821
|
+
}
|
|
16822
|
+
function _graphGet(map, absPath) {
|
|
16823
|
+
for (const k of _graphKeys(absPath)) {
|
|
16824
|
+
const hit = map.get(k);
|
|
16825
|
+
if (hit !== undefined) return hit;
|
|
16826
|
+
}
|
|
16827
|
+
return undefined;
|
|
16828
|
+
}
|
|
16829
|
+
function _registerKeys(map, absPath, value) {
|
|
16830
|
+
for (const k of _graphKeys(absPath)) if (!map.has(k)) map.set(k, value);
|
|
16831
|
+
}
|
|
16832
|
+
|
|
16600
16833
|
function _isHub(filePath) {
|
|
16601
16834
|
return /\/(utils|helpers|shared|common|constants|types|interfaces|index|zzz|globals)\.(ts|tsx|js|jsx|r|R)$/.test(filePath)
|
|
16602
16835
|
|| filePath.endsWith('/index.ts') || filePath.endsWith('/index.js')
|
|
@@ -16612,14 +16845,18 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
|
16612
16845
|
* @param {object} weights
|
|
16613
16846
|
* @returns {{ score: number, signals: { exactToken: number, symbolMatch: number, prefixMatch: number, pathMatch: number, penalty: number } }}
|
|
16614
16847
|
*/
|
|
16615
|
-
function scoreFile(filePath, sigs, queryTokens, weights) {
|
|
16848
|
+
function scoreFile(filePath, sigs, queryTokens, weights, wants) {
|
|
16616
16849
|
if (!sigs || sigs.length === 0) return { score: 0, signals: { exactToken: 0, symbolMatch: 0, prefixMatch: 0, pathMatch: 0, penalty: 1.0 } };
|
|
16617
16850
|
|
|
16618
16851
|
const w = weights || DEFAULT_WEIGHTS;
|
|
16619
|
-
const signals = { exactToken: 0, symbolMatch: 0, prefixMatch: 0, pathMatch: 0, penalty: _computePenalty(filePath) };
|
|
16620
|
-
|
|
16621
|
-
//
|
|
16622
|
-
|
|
16852
|
+
const signals = { exactToken: 0, symbolMatch: 0, prefixMatch: 0, pathMatch: 0, penalty: _computePenalty(filePath, wants) };
|
|
16853
|
+
|
|
16854
|
+
// Module-doc prose is excluded here on purpose. This signal measures overlap
|
|
16855
|
+
// with DECLARED IDENTIFIERS; prose relevance is BM25's job, where it is scored
|
|
16856
|
+
// as its own weighted field. Letting descriptive text inflate the identifier
|
|
16857
|
+
// signal double-counts it and measurably degraded MRR.
|
|
16858
|
+
const codeSigs = sigs.filter((line) => !MODULE_DOC_RE.test(line));
|
|
16859
|
+
const sigText = codeSigs.join(' ');
|
|
16623
16860
|
const sigTokenSet = new Set(tokenize(sigText));
|
|
16624
16861
|
|
|
16625
16862
|
// Build token set from the file path
|
|
@@ -16637,7 +16874,7 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
|
16637
16874
|
signals.exactToken += bonus;
|
|
16638
16875
|
|
|
16639
16876
|
// Bonus: appears directly in a function/class/method name line
|
|
16640
|
-
const nameLineMatch =
|
|
16877
|
+
const nameLineMatch = codeSigs.some((sig) => {
|
|
16641
16878
|
const nt = tokenize(sig.replace(/[^a-zA-Z0-9_\s]/g, ' '));
|
|
16642
16879
|
return nt.includes(qt);
|
|
16643
16880
|
});
|
|
@@ -16699,13 +16936,18 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
|
16699
16936
|
const graph = (opts && opts.graph && opts.graph.forward instanceof Map) ? opts.graph : null;
|
|
16700
16937
|
const cwd = (opts && opts.cwd) || null;
|
|
16701
16938
|
|
|
16702
|
-
//
|
|
16939
|
+
// Intent is reported to the user and shapes output depth; it no longer
|
|
16940
|
+
// selects scoring weights (see SIGNAL_BLEND).
|
|
16703
16941
|
const intent = detectIntent(query);
|
|
16704
|
-
const
|
|
16705
|
-
|
|
16706
|
-
|
|
16942
|
+
const weights = (opts && opts.weights) ? Object.assign({}, DEFAULT_WEIGHTS, opts.weights) : DEFAULT_WEIGHTS;
|
|
16943
|
+
// Learned per-file multipliers are a LOCAL, evolving signal (.context/weights.json).
|
|
16944
|
+
// Benchmarks and CI gates must opt out via { learned: false }, or a developer's
|
|
16945
|
+
// local learned state silently changes the score and CI stops being reproducible.
|
|
16946
|
+
const useLearned = !(opts && opts.learned === false);
|
|
16947
|
+
const learnedWeights = opts && opts.cwd && useLearned ? loadWeights(opts.cwd) : null;
|
|
16707
16948
|
|
|
16708
16949
|
const queryTokens = tokenize(query);
|
|
16950
|
+
const queryWants = _queryWants(queryTokens);
|
|
16709
16951
|
if (queryTokens.length === 0) {
|
|
16710
16952
|
// Empty query: return top-K by file count (most signatures = most useful)
|
|
16711
16953
|
const all = [];
|
|
@@ -16722,18 +16964,32 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
|
16722
16964
|
// are matched. The existing negative-signal penalty and recency/graph/learned
|
|
16723
16965
|
// boosts are layered on top; the per-token signals stay for the explain table.
|
|
16724
16966
|
const bm25Scores = new Map();
|
|
16725
|
-
for (const c of bm25rank(query, [...sigIndex.entries()].map(([file, sigs]) => ({ file, sigs })))) {
|
|
16967
|
+
for (const c of bm25rank(query, [...sigIndex.entries()].map(([file, sigs]) => ({ file, sigs })), opts)) {
|
|
16726
16968
|
bm25Scores.set(c.file, c.score);
|
|
16727
16969
|
}
|
|
16728
16970
|
|
|
16729
|
-
|
|
16971
|
+
// Two passes: scoreFile's weighted signal needs the max across the corpus to
|
|
16972
|
+
// normalise against, so collect first, then combine.
|
|
16973
|
+
const prescored = [];
|
|
16974
|
+
let maxSignal = 0;
|
|
16730
16975
|
for (const [file, sigs] of sigIndex.entries()) {
|
|
16731
|
-
const result = scoreFile(file, sigs, queryTokens, weights);
|
|
16976
|
+
const result = scoreFile(file, sigs, queryTokens, weights, queryWants);
|
|
16977
|
+
if (result.score > maxSignal) maxSignal = result.score;
|
|
16978
|
+
prescored.push({ file, sigs, result });
|
|
16979
|
+
}
|
|
16980
|
+
|
|
16981
|
+
const scored = [];
|
|
16982
|
+
for (const { file, sigs, result } of prescored) {
|
|
16732
16983
|
const penalty = result.signals.penalty;
|
|
16733
16984
|
const base = bm25Scores.get(file) || 0;
|
|
16734
|
-
|
|
16985
|
+
// Blend the weighted keyword/symbol/path signal into the BM25 base. This
|
|
16986
|
+
// was previously computed and thrown away — `result.score` was never read,
|
|
16987
|
+
// which silently made DEFAULT_WEIGHTS and every intent profile dead config.
|
|
16988
|
+
const signalNorm = maxSignal > 0 ? result.score / maxSignal : 0;
|
|
16989
|
+
let score = base * penalty * (1 + SIGNAL_BLEND * signalNorm);
|
|
16735
16990
|
const signals = result.signals;
|
|
16736
16991
|
signals.bm25 = base;
|
|
16992
|
+
signals.signalBlend = signalNorm;
|
|
16737
16993
|
|
|
16738
16994
|
// Recency boost
|
|
16739
16995
|
if (recencySet && recencySet.has(file) && score > 0) {
|
|
@@ -16763,44 +17019,46 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
|
16763
17019
|
// Hub suppression: files with high fanout (>20%) are not boosted
|
|
16764
17020
|
if (graph && cwd) {
|
|
16765
17021
|
const path = require('path');
|
|
16766
|
-
//
|
|
16767
|
-
|
|
16768
|
-
|
|
17022
|
+
// Every graph node is keyed by `path.normalize(p).toLowerCase()` (see
|
|
17023
|
+
// normalizePath in src/graph/builder.js and src/graph/call-graph.js).
|
|
17024
|
+
// Lookups MUST use the same key space: a bare path.resolve() preserves
|
|
17025
|
+
// case, so on any repo whose absolute path contains an uppercase letter
|
|
17026
|
+
// every .get() missed and this entire block was silently inert.
|
|
17027
|
+
const keyToIdx = new Map();
|
|
16769
17028
|
for (let i = 0; i < scored.length; i++) {
|
|
16770
|
-
|
|
16771
|
-
const abs = path.resolve(cwd, scored[i].file);
|
|
16772
|
-
absToRel.set(abs, scored[i].file);
|
|
17029
|
+
_registerKeys(keyToIdx, path.resolve(cwd, scored[i].file), i);
|
|
16773
17030
|
}
|
|
16774
17031
|
|
|
16775
17032
|
const hubs = _computeHubs(graph);
|
|
16776
|
-
const hop1Files = new Set(); //
|
|
17033
|
+
const hop1Files = new Set(); // normalised keys that received a hop1 boost
|
|
17034
|
+
const hop1Seeds = []; // original (un-normalised) paths, for hop-2 lookup
|
|
16777
17035
|
|
|
16778
17036
|
// Hop 1: direct neighbors of scored files
|
|
16779
17037
|
for (const entry of scored) {
|
|
16780
17038
|
if (entry.score <= 0) continue;
|
|
16781
|
-
const
|
|
16782
|
-
const neighbors = graph.forward.get(abs) || [];
|
|
17039
|
+
const neighbors = _graphGet(graph.forward, path.resolve(cwd, entry.file)) || [];
|
|
16783
17040
|
for (const neighborAbs of neighbors) {
|
|
16784
|
-
|
|
16785
|
-
|
|
16786
|
-
const idx =
|
|
17041
|
+
const nk = path.normalize(neighborAbs);
|
|
17042
|
+
if (_isHub(nk) || hubs.has(nk) || hubs.has(nk.toLowerCase())) continue;
|
|
17043
|
+
const idx = _graphGet(keyToIdx, nk);
|
|
16787
17044
|
if (idx !== undefined) {
|
|
16788
17045
|
scored[idx].score += GRAPH_BOOST_AMOUNTS.hop1;
|
|
16789
17046
|
scored[idx].signals.graphBoost = (scored[idx].signals.graphBoost || 0) + GRAPH_BOOST_AMOUNTS.hop1;
|
|
16790
|
-
hop1Files.add(
|
|
17047
|
+
hop1Files.add(nk);
|
|
17048
|
+
hop1Seeds.push(neighborAbs);
|
|
16791
17049
|
}
|
|
16792
17050
|
}
|
|
16793
17051
|
}
|
|
16794
17052
|
|
|
16795
17053
|
// Hop 2: neighbors of hop1 files (only if they didn't get a direct score)
|
|
16796
|
-
for (const
|
|
16797
|
-
if (
|
|
16798
|
-
const neighbors = graph.forward
|
|
17054
|
+
for (const hop1Key of hop1Seeds) {
|
|
17055
|
+
if (_graphGet(keyToIdx, hop1Key) === undefined) continue; // skip files not in index
|
|
17056
|
+
const neighbors = _graphGet(graph.forward, hop1Key) || [];
|
|
16799
17057
|
for (const neighborAbs of neighbors) {
|
|
16800
|
-
|
|
16801
|
-
if (
|
|
16802
|
-
|
|
16803
|
-
const idx =
|
|
17058
|
+
const nk = path.normalize(neighborAbs);
|
|
17059
|
+
if (_isHub(nk) || hubs.has(nk) || hubs.has(nk.toLowerCase())) continue;
|
|
17060
|
+
if (hop1Files.has(nk)) continue; // skip already hop1-boosted
|
|
17061
|
+
const idx = _graphGet(keyToIdx, nk);
|
|
16804
17062
|
if (idx !== undefined && scored[idx].score > 0) {
|
|
16805
17063
|
// Only boost files that have some baseline score (not noise)
|
|
16806
17064
|
scored[idx].score += GRAPH_BOOST_AMOUNTS.hop2;
|
|
@@ -16817,16 +17075,17 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
|
16817
17075
|
const callGraph = (opts && opts.callGraph && opts.callGraph.forward instanceof Map) ? opts.callGraph : null;
|
|
16818
17076
|
if (callGraph && cwd) {
|
|
16819
17077
|
const path = require('path');
|
|
16820
|
-
|
|
16821
|
-
|
|
17078
|
+
// buildCallFileGraph keys by a CASE-PRESERVING path.resolve, unlike the
|
|
17079
|
+
// import graph builder which lowercases — hence the dual-form probe.
|
|
17080
|
+
const keyToIdx = new Map();
|
|
17081
|
+
for (let i = 0; i < scored.length; i++) _registerKeys(keyToIdx, path.resolve(cwd, scored[i].file), i);
|
|
16822
17082
|
const hubs = _computeHubs(callGraph);
|
|
16823
17083
|
const seeds = scored.filter((e) => e.score > 0).map((e) => e.file);
|
|
16824
17084
|
for (const file of seeds) {
|
|
16825
|
-
const
|
|
16826
|
-
|
|
16827
|
-
if (_isHub(
|
|
16828
|
-
const
|
|
16829
|
-
const idx = relToIdx.get(neighborRel);
|
|
17085
|
+
for (const neighborAbs of (_graphGet(callGraph.forward, path.resolve(cwd, file)) || [])) {
|
|
17086
|
+
const nk = path.normalize(neighborAbs);
|
|
17087
|
+
if (_isHub(nk) || hubs.has(nk) || hubs.has(nk.toLowerCase())) continue;
|
|
17088
|
+
const idx = _graphGet(keyToIdx, nk);
|
|
16830
17089
|
if (idx !== undefined && scored[idx].file !== file) {
|
|
16831
17090
|
scored[idx].score += GRAPH_BOOST_AMOUNTS.callHop;
|
|
16832
17091
|
scored[idx].signals.callGraphBoost = (scored[idx].signals.callGraphBoost || 0) + GRAPH_BOOST_AMOUNTS.callHop;
|
|
@@ -17000,6 +17259,18 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
|
17000
17259
|
}
|
|
17001
17260
|
} catch (_) {}
|
|
17002
17261
|
_mergeSigIndex(index, _buildSigIndexFromCache(cwd));
|
|
17262
|
+
|
|
17263
|
+
// The complete retrieval index (written by generate before applyTokenBudget)
|
|
17264
|
+
// takes precedence: it is the only source containing files the budget dropped,
|
|
17265
|
+
// and full signatures for files the budget collapsed to line anchors. It is
|
|
17266
|
+
// merged as the BASE rather than on top because _mergeSigIndex only replaces
|
|
17267
|
+
// when the source has MORE signatures — a collapsed entry has the same count
|
|
17268
|
+
// as its full form, so merging the other way would keep the anchors.
|
|
17269
|
+
try {
|
|
17270
|
+
const full = __require('./src/retrieval/sig-index-store').readFullIndex(cwd);
|
|
17271
|
+
if (full.size > 0) return _mergeSigIndex(full, index);
|
|
17272
|
+
} catch (_) { /* absent → budgeted view is still served */ }
|
|
17273
|
+
|
|
17003
17274
|
return index;
|
|
17004
17275
|
}
|
|
17005
17276
|
|
|
@@ -17125,25 +17396,155 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
|
17125
17396
|
// ---------------------------------------------------------------------------
|
|
17126
17397
|
// Intent detection — 7 intents
|
|
17127
17398
|
// ---------------------------------------------------------------------------
|
|
17399
|
+
// Nouns carry an optional plural: `\btest\b` does not match "tests", so
|
|
17400
|
+
// "write unit tests for the ranker" matched NO intent at all and fell through
|
|
17401
|
+
// to the 'search' default.
|
|
17128
17402
|
const INTENT_PATTERNS = {
|
|
17129
|
-
debug: /\b(
|
|
17403
|
+
debug: /\b(bugs?|fix(es|ed)?|errors?|crash(es)?|exceptions?|broken|failing|failures?|issues?|problems?|regressions?)\b/i,
|
|
17130
17404
|
explain: /\b(explain|how does|what is|understand|overview|architecture|describe|walk me|teach)\b/i,
|
|
17131
|
-
refactor: /\b(refactor|restructure|redesign|clean up|extract|move|rename|simplify|
|
|
17405
|
+
refactor: /\b(refactor|restructure|redesign|clean up|extract|move|rename|simplify|optimi[sz]e)\b/i,
|
|
17132
17406
|
review: /\b(review|check|audit|security|pr|pull request|assess|validate)\b/i,
|
|
17133
|
-
test: /\b(
|
|
17134
|
-
integrate:/\b(
|
|
17407
|
+
test: /\b(tests?|unit tests?|integration tests?|testing|specs?|assert(ion)?s?|mocks?|fixtures?)\b/i,
|
|
17408
|
+
integrate:/\b(imports?|integrate|connect|wire|bind|requires?|exports?|depends?|dependenc(y|ies)|graph)\b/i,
|
|
17135
17409
|
navigate: /\b(find|locate|where|search|look for|show me|navigate|browse|list)\b/i,
|
|
17136
17410
|
};
|
|
17137
17411
|
|
|
17138
|
-
|
|
17139
|
-
|
|
17412
|
+
/**
|
|
17413
|
+
* Every intent whose pattern matches, strongest first.
|
|
17414
|
+
*
|
|
17415
|
+
* A real request is routinely multi-intent — "fix the failing test" is both a
|
|
17416
|
+
* debug task and a test task — and reporting one label discards that. Worse,
|
|
17417
|
+
* the single-label version returned the FIRST key in INTENT_PATTERNS order, so
|
|
17418
|
+
* `debug` permanently shadowed `test`: no query containing "fix" or "failing"
|
|
17419
|
+
* could ever be labelled a test, no matter how test-shaped it was.
|
|
17420
|
+
*
|
|
17421
|
+
* Ranked by how many distinct terms each pattern matched, so the dominant
|
|
17422
|
+
* intent leads; ties fall back to declaration order for determinism.
|
|
17423
|
+
*
|
|
17424
|
+
* @param {string} query
|
|
17425
|
+
* @returns {string[]} matched intents, never empty (defaults to ['search'])
|
|
17426
|
+
*/
|
|
17427
|
+
function detectIntents(query) {
|
|
17428
|
+
if (!query || typeof query !== 'string') return ['search'];
|
|
17429
|
+
const scored = [];
|
|
17430
|
+
let order = 0;
|
|
17140
17431
|
for (const [intent, re] of Object.entries(INTENT_PATTERNS)) {
|
|
17141
|
-
|
|
17432
|
+
const hits = query.match(new RegExp(re.source, re.flags.includes('g') ? re.flags : re.flags + 'g'));
|
|
17433
|
+
if (hits && hits.length) {
|
|
17434
|
+
scored.push({ intent, hits: new Set(hits.map((h) => h.toLowerCase())).size, order: order });
|
|
17435
|
+
}
|
|
17436
|
+
order++;
|
|
17142
17437
|
}
|
|
17143
|
-
return 'search';
|
|
17438
|
+
if (scored.length === 0) return ['search'];
|
|
17439
|
+
scored.sort((a, b) => (b.hits - a.hits) || (a.order - b.order));
|
|
17440
|
+
return scored.map((s) => s.intent);
|
|
17441
|
+
}
|
|
17442
|
+
|
|
17443
|
+
/** Primary intent. Kept for callers that want a single label. */
|
|
17444
|
+
function detectIntent(query) {
|
|
17445
|
+
return detectIntents(query)[0];
|
|
17144
17446
|
}
|
|
17145
17447
|
|
|
17146
|
-
module.exports = { rank, buildSigIndex, scoreFile, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, GRAPH_BOOST_AMOUNTS, CENTRALITY_BLEND_WEIGHT, detectIntent };
|
|
17448
|
+
module.exports = { rank, buildSigIndex, scoreFile, _queryWants, detectIntents, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, GRAPH_BOOST_AMOUNTS, CENTRALITY_BLEND_WEIGHT, detectIntent };
|
|
17449
|
+
|
|
17450
|
+
};
|
|
17451
|
+
|
|
17452
|
+
// ── ./src/retrieval/sig-index-store ──
|
|
17453
|
+
__factories["./src/retrieval/sig-index-store"] = function(module, exports) {
|
|
17454
|
+
|
|
17455
|
+
/**
|
|
17456
|
+
* Complete, unbudgeted signature index for retrieval.
|
|
17457
|
+
*
|
|
17458
|
+
* WHY THIS EXISTS
|
|
17459
|
+
* ---------------
|
|
17460
|
+
* The generated context file (CLAUDE.md / AGENTS.md / copilot-instructions.md)
|
|
17461
|
+
* is a BUDGETED VIEW: `applyTokenBudget` drops and collapses files so the
|
|
17462
|
+
* artifact stays under `maxTokens`, because it is injected into every prompt.
|
|
17463
|
+
*
|
|
17464
|
+
* `buildSigIndex` used to parse that same artifact to build the ranker's index,
|
|
17465
|
+
* so retrieval inherited the prompt budget. Every file the budget dropped became
|
|
17466
|
+
* permanently unreachable by `sigmap ask` — no ranking change can surface a file
|
|
17467
|
+
* that is not in the index. On this repo that was 53 of 155 source files (34%),
|
|
17468
|
+
* and restoring them moved hit@5 from 50% to 90% on the retrieval corpus.
|
|
17469
|
+
*
|
|
17470
|
+
* The two artifacts have opposite requirements — the prompt file wants to be
|
|
17471
|
+
* small, the index wants to be complete — so they are now separate. This store
|
|
17472
|
+
* is written by `generate` BEFORE the budget is applied, and lives under
|
|
17473
|
+
* `.context/` (gitignored, never injected into a prompt).
|
|
17474
|
+
*
|
|
17475
|
+
* Zero-dependency, bundle-safe (fs + path only).
|
|
17476
|
+
*/
|
|
17477
|
+
|
|
17478
|
+
const fs = require('fs');
|
|
17479
|
+
const path = require('path');
|
|
17480
|
+
|
|
17481
|
+
const INDEX_DIR = '.context';
|
|
17482
|
+
const INDEX_FILE = 'sig-index.json';
|
|
17483
|
+
const SCHEMA = 1;
|
|
17484
|
+
|
|
17485
|
+
/** Absolute path to the retrieval index artifact. */
|
|
17486
|
+
function indexPath(cwd) {
|
|
17487
|
+
return path.join(cwd, INDEX_DIR, INDEX_FILE);
|
|
17488
|
+
}
|
|
17489
|
+
|
|
17490
|
+
/**
|
|
17491
|
+
* Persist the complete signature index.
|
|
17492
|
+
*
|
|
17493
|
+
* @param {string} cwd
|
|
17494
|
+
* @param {Array<{filePath: string, sigs: string[]}>} fileEntries - every
|
|
17495
|
+
* extracted entry, BEFORE applyTokenBudget has dropped or collapsed any.
|
|
17496
|
+
* @param {{ version?: string }} [opts]
|
|
17497
|
+
* @returns {{ path: string, files: number }}
|
|
17498
|
+
*/
|
|
17499
|
+
function writeFullIndex(cwd, fileEntries, opts = {}) {
|
|
17500
|
+
const files = {};
|
|
17501
|
+
let count = 0;
|
|
17502
|
+
for (const e of fileEntries || []) {
|
|
17503
|
+
if (!e || !e.filePath || !Array.isArray(e.sigs) || e.sigs.length === 0) continue;
|
|
17504
|
+
const rel = path.relative(cwd, e.filePath).replace(/\\/g, '/');
|
|
17505
|
+
if (!rel || rel.startsWith('..')) continue;
|
|
17506
|
+
files[rel] = e.sigs;
|
|
17507
|
+
count++;
|
|
17508
|
+
}
|
|
17509
|
+
|
|
17510
|
+
const out = indexPath(cwd);
|
|
17511
|
+
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
17512
|
+
// Write-then-rename so a concurrent `ask` never observes a half-written index.
|
|
17513
|
+
const tmp = `${out}.tmp`;
|
|
17514
|
+
fs.writeFileSync(tmp, JSON.stringify({
|
|
17515
|
+
schema: SCHEMA,
|
|
17516
|
+
sigmapVersion: opts.version || null,
|
|
17517
|
+
generated: new Date().toISOString(),
|
|
17518
|
+
files,
|
|
17519
|
+
}), 'utf8');
|
|
17520
|
+
fs.renameSync(tmp, out);
|
|
17521
|
+
return { path: out, files: count };
|
|
17522
|
+
}
|
|
17523
|
+
|
|
17524
|
+
/**
|
|
17525
|
+
* Load the complete signature index, or an empty Map when absent/unreadable.
|
|
17526
|
+
*
|
|
17527
|
+
* Deliberately NOT version-busted (unlike .sigmap-cache.json): a stale but
|
|
17528
|
+
* complete index still retrieves the right files, whereas discarding it drops
|
|
17529
|
+
* retrieval back to the budgeted view — the exact failure this store exists to
|
|
17530
|
+
* prevent. Staleness is handled by re-running generate or by cache/freshen.
|
|
17531
|
+
*
|
|
17532
|
+
* @param {string} cwd
|
|
17533
|
+
* @returns {Map<string, string[]>}
|
|
17534
|
+
*/
|
|
17535
|
+
function readFullIndex(cwd) {
|
|
17536
|
+
const index = new Map();
|
|
17537
|
+
try {
|
|
17538
|
+
const data = JSON.parse(fs.readFileSync(indexPath(cwd), 'utf8'));
|
|
17539
|
+
if (!data || data.schema !== SCHEMA || !data.files) return index;
|
|
17540
|
+
for (const [rel, sigs] of Object.entries(data.files)) {
|
|
17541
|
+
if (Array.isArray(sigs) && sigs.length > 0) index.set(rel, sigs);
|
|
17542
|
+
}
|
|
17543
|
+
} catch (_) { /* absent or corrupt → caller falls back to the context file */ }
|
|
17544
|
+
return index;
|
|
17545
|
+
}
|
|
17546
|
+
|
|
17547
|
+
module.exports = { writeFullIndex, readFullIndex, indexPath, SCHEMA, INDEX_FILE };
|
|
17147
17548
|
|
|
17148
17549
|
};
|
|
17149
17550
|
|
|
@@ -21235,7 +21636,7 @@ function __tryGit(args, opts = {}) {
|
|
|
21235
21636
|
catch (_) { return ''; }
|
|
21236
21637
|
}
|
|
21237
21638
|
|
|
21238
|
-
const VERSION = '8.
|
|
21639
|
+
const VERSION = '8.29.0';
|
|
21239
21640
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
21240
21641
|
|
|
21241
21642
|
function requireSourceOrBundled(key) {
|
|
@@ -21372,10 +21773,70 @@ function buildFileList(cwd, config) {
|
|
|
21372
21773
|
const found = walkDir(abs, config.exclude, config.maxDepth);
|
|
21373
21774
|
files.push(...found);
|
|
21374
21775
|
}
|
|
21776
|
+
files.push(...declaredEntrypoints(cwd, config, files));
|
|
21375
21777
|
// Deduplicate
|
|
21376
21778
|
return [...new Set(files)];
|
|
21377
21779
|
}
|
|
21378
21780
|
|
|
21781
|
+
/**
|
|
21782
|
+
* Source files a project declares as its own entrypoints in package.json
|
|
21783
|
+
* (`main` and every `bin` target), when they live outside srcDirs.
|
|
21784
|
+
*
|
|
21785
|
+
* A CLI's entrypoint is usually at the repo root, so a srcDirs of [src] left
|
|
21786
|
+
* it unindexed and therefore unreachable by `sigmap ask` — on this repo that
|
|
21787
|
+
* was gen-context.js, which holds the whole generator pipeline. Declared
|
|
21788
|
+
* entrypoints are load-bearing by definition, so they are always scanned.
|
|
21789
|
+
*/
|
|
21790
|
+
function collectTestEntries(cwd, config, existing) {
|
|
21791
|
+
const TEST_ROOTS = ['test', 'tests', '__tests__', 'spec', 'e2e'];
|
|
21792
|
+
const have = new Set((existing || []).map((e) => e.filePath));
|
|
21793
|
+
const out = [];
|
|
21794
|
+
let moduleDocSig = null;
|
|
21795
|
+
try { ({ moduleDocSig } = requireSourceOrBundled('./src/retrieval/module-doc')); } catch (_) {}
|
|
21796
|
+
for (const root of TEST_ROOTS) {
|
|
21797
|
+
const abs = path.join(cwd, root);
|
|
21798
|
+
if (!fs.existsSync(abs)) continue;
|
|
21799
|
+
let files = [];
|
|
21800
|
+
try { files = walkDir(abs, config.exclude, config.maxDepth); } catch (_) { continue; }
|
|
21801
|
+
for (const fp of files) {
|
|
21802
|
+
if (have.has(fp)) continue;
|
|
21803
|
+
let src = '';
|
|
21804
|
+
try { src = fs.readFileSync(fp, 'utf8'); } catch (_) { continue; }
|
|
21805
|
+
let sigs = [];
|
|
21806
|
+
try {
|
|
21807
|
+
const { extractFile } = requireSourceOrBundled('./src/extractors/dispatch');
|
|
21808
|
+
sigs = extractFile(fp, src) || [];
|
|
21809
|
+
} catch (_) { continue; }
|
|
21810
|
+
if (!sigs.length) continue;
|
|
21811
|
+
const doc = moduleDocSig ? moduleDocSig(src, fp) : '';
|
|
21812
|
+
out.push({ filePath: fp, sigs: doc ? [doc, ...sigs] : sigs });
|
|
21813
|
+
}
|
|
21814
|
+
}
|
|
21815
|
+
return out;
|
|
21816
|
+
}
|
|
21817
|
+
|
|
21818
|
+
function declaredEntrypoints(cwd, config, existing) {
|
|
21819
|
+
const out = [];
|
|
21820
|
+
try {
|
|
21821
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
21822
|
+
const have = new Set(existing);
|
|
21823
|
+
const refs = [];
|
|
21824
|
+
if (typeof pkg.main === 'string') refs.push(pkg.main);
|
|
21825
|
+
if (typeof pkg.bin === 'string') refs.push(pkg.bin);
|
|
21826
|
+
else if (pkg.bin && typeof pkg.bin === 'object') refs.push(...Object.values(pkg.bin).filter((v) => typeof v === 'string'));
|
|
21827
|
+
for (const ref of refs) {
|
|
21828
|
+
const abs = path.resolve(cwd, ref);
|
|
21829
|
+
if (have.has(abs) || out.includes(abs)) continue;
|
|
21830
|
+
const rel = path.relative(cwd, abs);
|
|
21831
|
+
if (!rel || rel.startsWith('..')) continue; // outside the repo
|
|
21832
|
+
if ((config.exclude || []).some((x) => rel.split(path.sep).includes(x))) continue;
|
|
21833
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) continue;
|
|
21834
|
+
out.push(abs);
|
|
21835
|
+
}
|
|
21836
|
+
} catch (_) { /* no package.json, or unreadable → nothing to add */ }
|
|
21837
|
+
return out;
|
|
21838
|
+
}
|
|
21839
|
+
|
|
21379
21840
|
// ---------------------------------------------------------------------------
|
|
21380
21841
|
// Extractor loader (lazy, cached)
|
|
21381
21842
|
// ---------------------------------------------------------------------------
|
|
@@ -22647,6 +23108,60 @@ function runGenerate(cwd, config, reportMode, reportJson = false) {
|
|
|
22647
23108
|
|
|
22648
23109
|
let result;
|
|
22649
23110
|
if (!reportMode) {
|
|
23111
|
+
// Retrieval index (complete, unbudgeted) — written BEFORE applyTokenBudget
|
|
23112
|
+
// and before the strategy split, so it holds every extracted file for every
|
|
23113
|
+
// strategy. The generated context file is a budgeted VIEW for prompt
|
|
23114
|
+
// injection; the ranker must not inherit that budget or files dropped to fit
|
|
23115
|
+
// maxTokens become permanently unreachable by `sigmap ask`.
|
|
23116
|
+
try {
|
|
23117
|
+
const __store = requireSourceOrBundled('./src/retrieval/sig-index-store');
|
|
23118
|
+
// Honour --terse: `sigmap ask` renders .context/query-context.md straight
|
|
23119
|
+
// from these signatures, and that IS prompt-bound, so the user's format
|
|
23120
|
+
// choice has to survive into the index. Terse encoding preserves line
|
|
23121
|
+
// anchors byte-exactly, so get_lines still resolves.
|
|
23122
|
+
// Index-only enrichment: a module's leading comment describes its PURPOSE
|
|
23123
|
+
// in prose, which is the vocabulary a behavioural query actually uses.
|
|
23124
|
+
// Signatures carry shape, not intent — `coverageScore(cwd, fileEntries, config)`
|
|
23125
|
+
// shares no token with "what fraction of the repo made it into the output",
|
|
23126
|
+
// but that file's header says exactly that. Added to the retrieval index
|
|
23127
|
+
// ONLY: the prompt artifact is token-budgeted, the index is not.
|
|
23128
|
+
let __entries = fileEntries;
|
|
23129
|
+
try {
|
|
23130
|
+
// TRIED AND REJECTED: also indexing every per-symbol doc sentence
|
|
23131
|
+
// untruncated (src/retrieval/doc-text.js). 39% of extractor doc hints are
|
|
23132
|
+
// cut at 60 chars, so recovering them looked like free vocabulary. It is
|
|
23133
|
+
// not: train hit@5 fell 75.6% -> 73.3% at every docWeight from 0.2 to 1.0,
|
|
23134
|
+
// and the mined corpus never moved off 62.5%. The MODULE HEADER is the
|
|
23135
|
+
// high-signal prose — it states the file's purpose. Per-symbol sentences
|
|
23136
|
+
// describe internal helpers, so they broaden what each file matches
|
|
23137
|
+
// without making any file a better answer.
|
|
23138
|
+
const { moduleDocSig } = requireSourceOrBundled('./src/retrieval/module-doc');
|
|
23139
|
+
__entries = __entries.map((e) => {
|
|
23140
|
+
let src = e.content;
|
|
23141
|
+
if (typeof src !== 'string') { try { src = fs.readFileSync(e.filePath, 'utf8'); } catch (_) { src = ''; } }
|
|
23142
|
+
const doc = moduleDocSig(src, e.filePath);
|
|
23143
|
+
return doc ? Object.assign({}, e, { sigs: [doc, ...e.sigs] }) : e;
|
|
23144
|
+
});
|
|
23145
|
+
} catch (_) { /* enrichment is best-effort */ }
|
|
23146
|
+
if (config && config.terse) {
|
|
23147
|
+
try {
|
|
23148
|
+
const { encodeTerseSigs } = requireSourceOrBundled('./src/format/terse');
|
|
23149
|
+
__entries = fileEntries.map((e) => Object.assign({}, e, { sigs: encodeTerseSigs(e.sigs) }));
|
|
23150
|
+
} catch (_) { /* terse unavailable → index full signatures */ }
|
|
23151
|
+
}
|
|
23152
|
+
// Test files: indexed, never rendered into the prompt. They were the one
|
|
23153
|
+
// whole category `sigmap ask` could not reach at all — srcDirs excludes
|
|
23154
|
+
// them, so "where are the tests for X" had no answer at any rank. The
|
|
23155
|
+
// prompt artifact stays clean because this list never reaches formatOutput.
|
|
23156
|
+
try {
|
|
23157
|
+
__entries = __entries.concat(collectTestEntries(cwd, config, __entries));
|
|
23158
|
+
} catch (_) { /* best-effort */ }
|
|
23159
|
+
const __w = __store.writeFullIndex(cwd, __entries, { version: VERSION });
|
|
23160
|
+
if (process.argv.includes('--verbose')) {
|
|
23161
|
+
console.warn(`[sigmap] retrieval index: ${__w.files} file(s) → ${path.relative(cwd, __w.path)}`);
|
|
23162
|
+
}
|
|
23163
|
+
} catch (_) { /* non-fatal: ranker falls back to parsing the context file */ }
|
|
23164
|
+
|
|
22650
23165
|
if (strategy === 'per-module') {
|
|
22651
23166
|
result = runPerModuleStrategy(cwd, configWithBudget, fileEntries, inputTokenTotal);
|
|
22652
23167
|
} else if (strategy === 'hot-cold') {
|
|
@@ -23333,14 +23848,12 @@ function getRawTokenCount(cwd, config) {
|
|
|
23333
23848
|
return total;
|
|
23334
23849
|
}
|
|
23335
23850
|
|
|
23336
|
-
|
|
23851
|
+
// Intent no longer selects scoring weights — the per-intent multipliers were
|
|
23852
|
+
// measured to have zero effect on ranking (see src/retrieval/ranker.js).
|
|
23853
|
+
// Kept as a single call site so the ask handler keeps one weights source.
|
|
23854
|
+
function getIntentWeights(_intent) {
|
|
23337
23855
|
const { DEFAULT_WEIGHTS } = requireSourceOrBundled('./src/retrieval/ranker');
|
|
23338
|
-
|
|
23339
|
-
if (intent === 'debug') return Object.assign({}, base, { recencyBoost: base.recencyBoost * 1.5 });
|
|
23340
|
-
if (intent === 'explain') return Object.assign({}, base, { symbolMatch: base.symbolMatch * 1.5 });
|
|
23341
|
-
if (intent === 'refactor') return Object.assign({}, base, { pathMatch: base.pathMatch * 1.5 });
|
|
23342
|
-
if (intent === 'review') return Object.assign({}, base, { exactToken: base.exactToken * 1.3 });
|
|
23343
|
-
return base;
|
|
23856
|
+
return Object.assign({}, DEFAULT_WEIGHTS);
|
|
23344
23857
|
}
|
|
23345
23858
|
|
|
23346
23859
|
function extractQuerySymbols(query) {
|
|
@@ -23515,7 +24028,7 @@ function main() {
|
|
|
23515
24028
|
process.exit(1);
|
|
23516
24029
|
}
|
|
23517
24030
|
|
|
23518
|
-
const { detectIntent, buildSigIndex, rank } = requireSourceOrBundled('./src/retrieval/ranker');
|
|
24031
|
+
const { detectIntent, detectIntents, buildSigIndex, rank } = requireSourceOrBundled('./src/retrieval/ranker');
|
|
23519
24032
|
const { coverageScore } = requireSourceOrBundled('./src/analysis/coverage-score');
|
|
23520
24033
|
const { loadSession, saveSession, mergeSessionContext } = requireSourceOrBundled('./src/session/memory');
|
|
23521
24034
|
const { detectWorkspaces, inferPackage, scopeToPackage } = requireSourceOrBundled('./src/workspace/detector');
|
|
@@ -23692,7 +24205,7 @@ function main() {
|
|
|
23692
24205
|
console.log([
|
|
23693
24206
|
bar,
|
|
23694
24207
|
` sigmap ask "${query}"`,
|
|
23695
|
-
` Intent : ${
|
|
24208
|
+
` Intent : ${detectIntents(query).join(', ')}`,
|
|
23696
24209
|
` Context : ${ctxTok.toLocaleString()} tokens → ${path.relative(cwd, outPath)}`,
|
|
23697
24210
|
` Coverage : ${coveragePct}%`,
|
|
23698
24211
|
` Risk : ${riskLevel}`,
|