graph-indexer 2.1.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +260 -1
- package/README.md +232 -312
- package/SECURITY.md +37 -59
- package/bin/graph-indexer.mjs +251 -0
- package/integrations/claude-code/.claude-plugin/plugin.json +8 -0
- package/integrations/claude-code/.mcp.json +8 -0
- package/integrations/claude-code/agents/code-structure.md +22 -0
- package/integrations/claude-code/hooks/gi-hook.mjs +99 -0
- package/integrations/claude-code/hooks/hooks.json +16 -0
- package/integrations/opencode/graph-indexer.js +124 -0
- package/package.json +31 -105
- package/src/cli/helper.mjs +67 -0
- package/src/cli/hook.mjs +356 -0
- package/src/cli/init.mjs +255 -0
- package/src/cli/resident.mjs +175 -0
- package/src/index/discover.mjs +130 -0
- package/src/index/fingerprint.mjs +30 -0
- package/src/index/graph.mjs +61 -0
- package/src/index/indexer.mjs +476 -0
- package/src/index/modules.mjs +331 -0
- package/src/index/resolver.mjs +954 -0
- package/src/mcp/render.mjs +60 -0
- package/src/mcp/server.mjs +147 -0
- package/src/mcp/tools.mjs +973 -0
- package/src/parse/arity.mjs +152 -0
- package/src/parse/extract.mjs +1031 -0
- package/src/parse/lang/csharp.mjs +106 -0
- package/src/parse/lang/ecma.mjs +438 -0
- package/src/parse/lang/go.mjs +194 -0
- package/src/parse/lang/jvm.mjs +118 -0
- package/src/parse/lang/kotlin.mjs +97 -0
- package/src/parse/lang/misc.mjs +115 -0
- package/src/parse/lang/native.mjs +126 -0
- package/src/parse/lang/python.mjs +153 -0
- package/src/parse/lang/rust.mjs +157 -0
- package/src/parse/lang/scripting.mjs +219 -0
- package/src/parse/languages.mjs +48 -0
- package/src/parse/runtime.mjs +56 -0
- package/src/query/check.mjs +343 -0
- package/src/query/facts.mjs +323 -0
- package/src/query/intel.mjs +680 -0
- package/src/query/read.mjs +81 -0
- package/src/query/testcmd.mjs +119 -0
- package/src/query/textsearch.mjs +160 -0
- package/src/search/search.mjs +210 -0
- package/src/search/tokenize.mjs +188 -0
- package/src/store/db.mjs +169 -0
- package/src/util/paths.mjs +24 -0
- package/vendor/grammars/bash.wasm.gz +0 -0
- package/vendor/grammars/c.wasm.gz +0 -0
- package/vendor/grammars/c_sharp.wasm.gz +0 -0
- package/vendor/grammars/cpp.wasm.gz +0 -0
- package/vendor/grammars/css.wasm.gz +0 -0
- package/vendor/grammars/go.wasm.gz +0 -0
- package/vendor/grammars/java.wasm.gz +0 -0
- package/vendor/grammars/javascript.wasm.gz +0 -0
- package/vendor/grammars/kotlin.wasm.gz +0 -0
- package/vendor/grammars/manifest.json +148 -0
- package/vendor/grammars/php.wasm.gz +0 -0
- package/vendor/grammars/python.wasm.gz +0 -0
- package/vendor/grammars/ruby.wasm.gz +0 -0
- package/vendor/grammars/rust.wasm.gz +0 -0
- package/vendor/grammars/scala.wasm.gz +0 -0
- package/vendor/grammars/tsx.wasm.gz +0 -0
- package/vendor/grammars/typescript.wasm.gz +0 -0
- package/vendor/web-tree-sitter/web-tree-sitter.js +3980 -0
- package/vendor/web-tree-sitter/web-tree-sitter.wasm +0 -0
- package/cli-ui.mjs +0 -78
- package/colbert.mjs +0 -168
- package/config.mjs +0 -494
- package/daemon-ctl.mjs +0 -175
- package/daemon-lock.mjs +0 -69
- package/embedders/python/mlx_embed_server.py +0 -61
- package/embedders/python/requirements-mlx.txt +0 -5
- package/embedders/setup-mlx.mjs +0 -110
- package/embeddings.mjs +0 -571
- package/engine/binary.mjs +0 -437
- package/engine/memory.mjs +0 -1003
- package/engine/sqlite.mjs +0 -1136
- package/enrichment.mjs +0 -521
- package/git-signals.mjs +0 -248
- package/indexer.mjs +0 -546
- package/init.mjs +0 -1809
- package/layout.mjs +0 -123
- package/mcp/centrality.mjs +0 -117
- package/mcp/format.mjs +0 -61
- package/mcp/resolver.mjs +0 -116
- package/mcp/symbolgraph.mjs +0 -137
- package/mcp/taint.mjs +0 -225
- package/mcp/tools.mjs +0 -1582
- package/mcp/topology.mjs +0 -676
- package/mcp-server.mjs +0 -197
- package/parse/extractor.mjs +0 -628
- package/parse/imports.mjs +0 -365
- package/parse/interprocedural.mjs +0 -134
- package/parse/languages.mjs +0 -253
- package/parse/metadata.mjs +0 -701
- package/parse/routes.mjs +0 -327
- package/parse/scip.mjs +0 -391
- package/parse/taint-patterns.mjs +0 -119
- package/prompts/CORE.md +0 -91
- package/prompts/DOMAIN_TEMPLATE.md +0 -46
- package/prompts/INTEGRATION.md +0 -123
- package/prompts/frameworks/ANDROID.md +0 -17
- package/prompts/frameworks/ASPNET_CORE.md +0 -19
- package/prompts/frameworks/FASTAPI_DJANGO.md +0 -20
- package/prompts/frameworks/LARAVEL_SYMFONY.md +0 -22
- package/prompts/frameworks/NODE_EXPRESS_NESTJS.md +0 -19
- package/prompts/frameworks/RAILS.md +0 -20
- package/prompts/frameworks/REACT.md +0 -20
- package/prompts/frameworks/SPRING_BOOT.md +0 -20
- package/prompts/languages/BASH.md +0 -22
- package/prompts/languages/C.md +0 -21
- package/prompts/languages/CSHARP.md +0 -24
- package/prompts/languages/CSS_SCSS.md +0 -22
- package/prompts/languages/GO.md +0 -24
- package/prompts/languages/JAVA.md +0 -23
- package/prompts/languages/JAVASCRIPT_TYPESCRIPT.md +0 -24
- package/prompts/languages/KOTLIN.md +0 -24
- package/prompts/languages/PHP.md +0 -23
- package/prompts/languages/PYTHON.md +0 -23
- package/prompts/languages/RUBY.md +0 -23
- package/prompts/languages/RUST.md +0 -21
- package/prompts/languages/SWIFT.md +0 -21
- package/seal.mjs +0 -400
- package/search-core.mjs +0 -777
- package/search-sparse.mjs +0 -183
- package/storage.mjs +0 -73
- package/watch-daemon.mjs +0 -289
|
Binary file
|
package/cli-ui.mjs
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file cli-ui.mjs
|
|
3
|
-
* @description Tiny, dependency-free console styling shared by the `graph-indexer`
|
|
4
|
-
* CLIs (init + daemon control). ANSI colour with graceful degradation
|
|
5
|
-
* when stdout is not a TTY or NO_COLOR is set, plus a few drawing
|
|
6
|
-
* helpers (boxed banner, rules, status glyphs) for a consistent look.
|
|
7
|
-
* @author MaquinaTech <https://github.com/MaquinaTech>
|
|
8
|
-
* @copyright (c) 2026 MaquinaTech. All rights reserved.
|
|
9
|
-
* @license MIT
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
export const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
13
|
-
|
|
14
|
-
const paint = (code) => (s) => (useColor ? `\x1b[${code}m${s}\x1b[0m` : `${s}`);
|
|
15
|
-
|
|
16
|
-
export const c = {
|
|
17
|
-
bold: paint('1'),
|
|
18
|
-
dim: paint('2'),
|
|
19
|
-
red: paint('31'),
|
|
20
|
-
green: paint('32'),
|
|
21
|
-
yellow: paint('33'),
|
|
22
|
-
blue: paint('34'),
|
|
23
|
-
magenta: paint('35'),
|
|
24
|
-
cyan: paint('36'),
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export const glyph = {
|
|
28
|
-
ok: c.green('✓'),
|
|
29
|
-
upd: c.cyan('↻'),
|
|
30
|
-
keep: c.dim('•'),
|
|
31
|
-
move: c.yellow('↪'),
|
|
32
|
-
skip: c.dim('–'),
|
|
33
|
-
warn: c.yellow('⚠'),
|
|
34
|
-
err: c.red('✗'),
|
|
35
|
-
run: c.green('●'),
|
|
36
|
-
stop: c.dim('○'),
|
|
37
|
-
arrow: c.cyan('→'),
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
export const log = (msg = '') => process.stdout.write(msg + '\n');
|
|
41
|
-
|
|
42
|
-
/** A horizontal rule, capped to the terminal width so it never wraps. */
|
|
43
|
-
export const rule = (width = Math.min(60, (process.stdout.columns || 80))) => c.dim('─'.repeat(Math.max(1, width)));
|
|
44
|
-
|
|
45
|
-
const visibleLen = (s) => s.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
46
|
-
|
|
47
|
-
/** ANSI-aware truncate to `limit` visible columns; closes any open colour + adds an ellipsis. */
|
|
48
|
-
function clipVisible(s, limit) {
|
|
49
|
-
if (limit < 1) return '';
|
|
50
|
-
if (visibleLen(s) <= limit) return s;
|
|
51
|
-
let out = '', w = 0, i = 0, ansi = false;
|
|
52
|
-
while (i < s.length && w < limit - 1) {
|
|
53
|
-
if (s[i] === '\x1b') {
|
|
54
|
-
const m = s.slice(i).match(/^\x1b\[[0-9;]*m/);
|
|
55
|
-
if (m) { out += m[0]; i += m[0].length; ansi = true; continue; }
|
|
56
|
-
}
|
|
57
|
-
out += s[i++]; w++;
|
|
58
|
-
}
|
|
59
|
-
return out + (ansi ? '\x1b[0m' : '') + '…';
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Draws a bordered box. ANSI escapes are stripped before measuring width so colour
|
|
64
|
-
* never breaks alignment, and the box (plus each line) is capped to the terminal
|
|
65
|
-
* width so the border stays intact on narrow terminals instead of wrapping.
|
|
66
|
-
*/
|
|
67
|
-
export function box(lines, { pad = 1, color = c.cyan } = {}) {
|
|
68
|
-
const maxInner = Math.max(1, (process.stdout.columns || 80) - 2); // 2 border columns
|
|
69
|
-
const shown = lines.map((line) => clipVisible(line, maxInner - pad * 2));
|
|
70
|
-
const inner = Math.min(maxInner, Math.max(...shown.map(visibleLen)) + pad * 2);
|
|
71
|
-
const top = color('╭' + '─'.repeat(inner) + '╮');
|
|
72
|
-
const bot = color('╰' + '─'.repeat(inner) + '╯');
|
|
73
|
-
const body = shown.map((line) => {
|
|
74
|
-
const gap = ' '.repeat(Math.max(0, inner - visibleLen(line) - pad));
|
|
75
|
-
return color('│') + ' '.repeat(pad) + line + gap + color('│');
|
|
76
|
-
});
|
|
77
|
-
return [top, ...body, bot].join('\n');
|
|
78
|
-
}
|
package/colbert.mjs
DELETED
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file colbert.mjs
|
|
3
|
-
* @description B4 — ColBERT-style LATE-INTERACTION reranker (multi-vector MaxSim). A third,
|
|
4
|
-
* opt-in reranker provider (rerank.provider === 'late-interaction'), sibling to the
|
|
5
|
-
* generative judge and the cross-encoder (B2). Where a single-vector dense channel
|
|
6
|
-
* pools the query and the document each into ONE vector (losing which part matched
|
|
7
|
-
* where), late interaction keeps MULTIPLE vectors per side and scores them with MaxSim:
|
|
8
|
-
*
|
|
9
|
-
* score(q, d) = Σ_{qi} max_{dj} cos(qi, dj)
|
|
10
|
-
*
|
|
11
|
-
* i.e. every query sub-vector finds its best-matching document sub-vector and the hits
|
|
12
|
-
* sum — the compositional-query win ColBERT is known for. It reuses the document
|
|
13
|
-
* sub-vectors the engine ALREADY stores (the base + `|s` summary + `|wN` window vectors
|
|
14
|
-
* of each chunk), so there is **no storage blow-up** — the canonical ColBERT cost. It is
|
|
15
|
-
* deployed as a RERANKER over the over-fetched candidate pool (the standard
|
|
16
|
-
* retrieve-then-late-interact pattern), so it is a post-retrieval re-order — the default
|
|
17
|
-
* ranking is untouched and parity is preserved (both backends read the SAME bin → the
|
|
18
|
-
* SAME doc vectors → the SAME MaxSim → the SAME order).
|
|
19
|
-
*
|
|
20
|
-
* AIR-GAPPED / HONEST SCOPE: the query multi-vectors are produced by the SAME local
|
|
21
|
-
* embedder the dense channel already uses (no new model, no network). A true ColBERT
|
|
22
|
-
* uses a token-level CONTEXTUAL encoder; a general sentence embedder over query
|
|
23
|
-
* sub-units only APPROXIMATES that — so this is "late interaction over the existing
|
|
24
|
-
* encoder," with a real token-level ColBERT model a clean future drop-in behind
|
|
25
|
-
* `encodeMultiVector`. Requires --embeddings (the doc vectors + the embedder); a no-op
|
|
26
|
-
* (returns the original order) when either is absent.
|
|
27
|
-
* @author MaquinaTech <https://github.com/MaquinaTech>
|
|
28
|
-
* @copyright (c) 2026 MaquinaTech. All rights reserved.
|
|
29
|
-
* @license MIT
|
|
30
|
-
*/
|
|
31
|
-
import { collectVectorsByKey } from './engine/binary.mjs';
|
|
32
|
-
import { embeddingKeyFor, SUMMARY_VEC_SUFFIX, WINDOW_VEC_SUFFIX, EMBEDDING_MAX_WINDOWS } from './search-core.mjs';
|
|
33
|
-
|
|
34
|
-
// Query sub-unit extraction is intentionally light (no stemming — surface forms, like the dense
|
|
35
|
-
// channel). A tiny stopword guard keeps function words from spending a query vector slot.
|
|
36
|
-
const STOP = new Set([
|
|
37
|
-
'the', 'a', 'an', 'and', 'or', 'of', 'to', 'in', 'for', 'on', 'with', 'at', 'by', 'from',
|
|
38
|
-
'that', 'this', 'is', 'are', 'was', 'were', 'be', 'how', 'what', 'when', 'where', 'which',
|
|
39
|
-
'who', 'why', 'does', 'do', 'it', 'its', 'into', 'if', 'then', 'there', 'all', 'any',
|
|
40
|
-
]);
|
|
41
|
-
|
|
42
|
-
/** Cosine similarity of two equal-length vectors (0 when either is degenerate). */
|
|
43
|
-
export function cosine(a, b) {
|
|
44
|
-
if (!a || !b || a.length !== b.length) return 0;
|
|
45
|
-
let dot = 0, na = 0, nb = 0;
|
|
46
|
-
for (let i = 0; i < a.length; i++) { dot += a[i] * b[i]; na += a[i] * a[i]; nb += b[i] * b[i]; }
|
|
47
|
-
if (na === 0 || nb === 0) return 0;
|
|
48
|
-
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* MaxSim late-interaction score: Σ over query vectors of the max cosine to any document vector.
|
|
53
|
-
* Order-independent (a sum of maxes) → no parity dependence on vector ordering.
|
|
54
|
-
* @param {Float32Array[]} qVecs query sub-vectors
|
|
55
|
-
* @param {Float32Array[]} dVecs document sub-vectors
|
|
56
|
-
* @returns {number} 0 when either side is empty
|
|
57
|
-
*/
|
|
58
|
-
export function maxSimScore(qVecs, dVecs) {
|
|
59
|
-
if (!qVecs?.length || !dVecs?.length) return 0;
|
|
60
|
-
let total = 0;
|
|
61
|
-
for (const q of qVecs) {
|
|
62
|
-
let best = -Infinity;
|
|
63
|
-
for (const d of dVecs) { const s = cosine(q, d); if (s > best) best = s; }
|
|
64
|
-
if (best > -Infinity) total += best;
|
|
65
|
-
}
|
|
66
|
-
return total;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** The embedding keys that hold a chunk's stored sub-vectors (base + summary + windows). */
|
|
70
|
-
function chunkVectorKeys(chunk) {
|
|
71
|
-
const vecKey = embeddingKeyFor(chunk);
|
|
72
|
-
const keys = [vecKey, chunk.content_hash, vecKey + SUMMARY_VEC_SUFFIX];
|
|
73
|
-
for (let i = 1; i < EMBEDDING_MAX_WINDOWS; i++) keys.push(vecKey + WINDOW_VEC_SUFFIX + i);
|
|
74
|
-
return { vecKey, keys };
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Load each chunk's stored sub-vectors from the shared `.embeddings.bin` in a SINGLE pass (one bin
|
|
79
|
-
* read per rerank, not per candidate). Identical for both backends (same file, same keys) →
|
|
80
|
-
* parity-by-construction. @returns {Map<chunkId, Float32Array[]>}
|
|
81
|
-
*/
|
|
82
|
-
export function loadChunkVectors(binPath, chunks) {
|
|
83
|
-
const meta = new Map(); // chunkId → { vecKey, hash }
|
|
84
|
-
const allKeys = new Set();
|
|
85
|
-
for (const c of chunks) {
|
|
86
|
-
if (!c || !c.content_hash) { meta.set(c?.id, null); continue; }
|
|
87
|
-
const { vecKey, keys } = chunkVectorKeys(c);
|
|
88
|
-
meta.set(c.id, { vecKey, hash: c.content_hash });
|
|
89
|
-
for (const k of keys) allKeys.add(k);
|
|
90
|
-
}
|
|
91
|
-
const found = collectVectorsByKey(binPath, allKeys);
|
|
92
|
-
const out = new Map();
|
|
93
|
-
for (const [id, m] of meta) {
|
|
94
|
-
if (!m) { out.set(id, []); continue; }
|
|
95
|
-
const vecs = [];
|
|
96
|
-
const base = found.get(m.vecKey) || found.get(m.hash); // deterministic order: base, summary, windows
|
|
97
|
-
if (base) vecs.push(base);
|
|
98
|
-
const s = found.get(m.vecKey + SUMMARY_VEC_SUFFIX); if (s) vecs.push(s);
|
|
99
|
-
for (let i = 1; i < EMBEDDING_MAX_WINDOWS; i++) { const w = found.get(m.vecKey + WINDOW_VEC_SUFFIX + i); if (w) vecs.push(w); }
|
|
100
|
-
out.set(id, vecs);
|
|
101
|
-
}
|
|
102
|
-
return out;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Encode a query into MULTIPLE vectors (the whole query plus its content sub-units) with the SAME
|
|
107
|
-
* local embedder the dense channel uses. Best-effort: returns [] on any failure / no embedder.
|
|
108
|
-
* @param {{embedDocuments?:Function, embedQuery?:Function}} embedder
|
|
109
|
-
* @param {string} text
|
|
110
|
-
* @param {object} [opts]
|
|
111
|
-
* @returns {Promise<Float32Array[]>}
|
|
112
|
-
*/
|
|
113
|
-
export async function encodeMultiVector(embedder, text, { maxVecs = 8 } = {}) {
|
|
114
|
-
if (!embedder || typeof embedder.embedDocuments !== 'function') return [];
|
|
115
|
-
const words = String(text).toLowerCase().split(/[\s\W_]+/).filter(Boolean);
|
|
116
|
-
const units = [];
|
|
117
|
-
const seen = new Set();
|
|
118
|
-
for (const w of words) {
|
|
119
|
-
if (w.length < 3 || STOP.has(w) || seen.has(w)) continue;
|
|
120
|
-
seen.add(w); units.push(w);
|
|
121
|
-
if (units.length >= maxVecs - 1) break;
|
|
122
|
-
}
|
|
123
|
-
units.unshift(String(text)); // the holistic query vector always leads
|
|
124
|
-
let vecs;
|
|
125
|
-
try { vecs = await embedder.embedDocuments(units); }
|
|
126
|
-
catch { return []; }
|
|
127
|
-
return (vecs || []).filter(v => v && v.length).map(v => v instanceof Float32Array ? v : new Float32Array(v));
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Late-interaction RERANK of the over-fetched pool. Re-orders the top-`topM` by MaxSim of the query
|
|
132
|
-
* multi-vectors against each candidate's stored doc sub-vectors; the tail is preserved. Best-effort
|
|
133
|
-
* and never throws: any shortfall (no embedder, <2 candidates with vectors, empty query vectors)
|
|
134
|
-
* returns the ORIGINAL order, so the channel can only help, never corrupt. Never mutates `r.score`
|
|
135
|
-
* (protects the downstream git-boost / formatting), exactly like rerankCrossEncoder.
|
|
136
|
-
*
|
|
137
|
-
* @param {Array<{chunk:object, score:number}>} results
|
|
138
|
-
* @param {object} opts
|
|
139
|
-
* @param {Float32Array[]} opts.qVecs query multi-vectors (from encodeMultiVector)
|
|
140
|
-
* @param {string} opts.binPath the `.embeddings.bin` path (db.embeddingBinPath())
|
|
141
|
-
* @param {number} [opts.topM]
|
|
142
|
-
* @returns {Array}
|
|
143
|
-
*/
|
|
144
|
-
export function rerankLateInteraction(results, { qVecs, binPath, topM = 12 } = {}) {
|
|
145
|
-
const head = results.slice(0, topM);
|
|
146
|
-
if (head.length < 2 || !qVecs?.length || !binPath) return results;
|
|
147
|
-
const vecsById = loadChunkVectors(binPath, head.map(r => r.chunk));
|
|
148
|
-
const scored = head.map((r, i) => {
|
|
149
|
-
const dVecs = vecsById.get(r.chunk.id) || [];
|
|
150
|
-
return { i, id: r.chunk.id, s: dVecs.length ? maxSimScore(qVecs, dVecs) : NaN };
|
|
151
|
-
});
|
|
152
|
-
// "Can only help, never corrupt": treat no-signal as a shortfall. Re-order only when at least two
|
|
153
|
-
// candidates carry a FINITE and DISTINCT MaxSim — so all-zero scores (e.g. a dim mismatch making
|
|
154
|
-
// every cosine 0) or undifferentiated ties fall through to the original order instead of being
|
|
155
|
-
// scrambled into id-lexicographic order by the tie-break.
|
|
156
|
-
const finite = scored.filter(x => Number.isFinite(x.s));
|
|
157
|
-
if (finite.length < 2 || new Set(finite.map(x => x.s)).size < 2) return results;
|
|
158
|
-
const order = scored
|
|
159
|
-
// finite MaxSim first (desc); candidates without vectors sink but keep a stable id order
|
|
160
|
-
.sort((a, b) => {
|
|
161
|
-
const af = Number.isFinite(a.s), bf = Number.isFinite(b.s);
|
|
162
|
-
if (af && bf) return (b.s - a.s) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
163
|
-
if (af) return -1; if (bf) return 1;
|
|
164
|
-
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
165
|
-
})
|
|
166
|
-
.map(x => x.i);
|
|
167
|
-
return [...order.map(i => head[i]), ...results.slice(topM)];
|
|
168
|
-
}
|