mixdog 0.9.151 → 0.9.152
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 +110 -68
- package/package.json +3 -2
- package/scripts/tool-stress.mjs +0 -2
- package/src/defaults/skills/setup/SKILL.md +18 -27
- package/src/headless-exec.mjs +3 -10
- package/src/headless-exec.test.mjs +2 -7
- package/src/runtime/agent/orchestrator/config.mjs +6 -66
- package/src/runtime/agent/orchestrator/context/collect-skills.test.mjs +153 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +49 -45
- package/src/runtime/agent/orchestrator/mcp/client.mjs +242 -15
- package/src/runtime/agent/orchestrator/mcp/features.test.mjs +66 -0
- package/src/runtime/agent/orchestrator/mcp/security.test.mjs +36 -20
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +54 -33
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +3 -8
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +10 -2
- package/src/runtime/agent/orchestrator/providers/lib/provider-replay.mjs +50 -0
- package/src/runtime/agent/orchestrator/providers/lib/sse-framing.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +3 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +8 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +25 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +35 -4
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +0 -2
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +13 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +4 -7
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/provider-replay.test.mjs +235 -0
- package/src/runtime/agent/orchestrator/providers/sse-framing.test.mjs +719 -0
- package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +399 -39
- package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +26 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +14 -4
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +24 -1
- package/src/runtime/agent/orchestrator/session/compaction-read-reset.test.mjs +161 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +14 -4
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +12 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +31 -18
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +6 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +1 -3
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +556 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.test.mjs +457 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +105 -186
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +115 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +23 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool-cwd-env.test.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -10
- package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +30 -14
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +15 -4
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +73 -15
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +76 -2
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +24 -683
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +64 -72
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +2 -5
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.mjs +75 -0
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.test.mjs +24 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +24 -5
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.test.mjs +44 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +12 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +35 -21
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +28 -13
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.test.mjs +54 -0
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
- package/src/runtime/browser-bridge/client.mjs +103 -0
- package/src/runtime/browser-bridge/tool-defs.mjs +42 -0
- package/src/runtime/computer-bridge/client.mjs +101 -0
- package/src/runtime/computer-bridge/tool-defs.mjs +53 -0
- package/src/runtime/media/store.mjs +18 -8
- package/src/runtime/media/store.test.mjs +14 -0
- package/src/runtime/memory/index.mjs +1 -1
- package/src/runtime/memory/lib/transcript-ingest.mjs +8 -6
- package/src/runtime/memory/lib/transcript-ingest.test.mjs +52 -0
- package/src/runtime/shared/child-spawn-gate.mjs +17 -0
- package/src/runtime/shared/child-spawn-remote.mjs +69 -7
- package/src/runtime/shared/child-spawn-remote.test.mjs +124 -0
- package/src/runtime/shared/pristine-execution-contract.json +2 -1
- package/src/runtime/shared/provider-api-key.mjs +1 -0
- package/src/runtime/shared/session-runtime-health.mjs +91 -0
- package/src/runtime/shared/session-runtime-health.test.mjs +53 -0
- package/src/runtime/shared/skill-document.mjs +86 -0
- package/src/runtime/shared/skill-document.test.mjs +81 -0
- package/src/runtime/shared/tool-surface.mjs +18 -0
- package/src/runtime/shared/turn-snapshot-store.mjs +1 -0
- package/src/runtime/shared/turn-snapshot.mjs +32 -9
- package/src/runtime/shared/user-cwd.mjs +33 -14
- package/src/runtime/shared/user-cwd.test.mjs +43 -0
- package/src/session-runtime/cwd-plugins.mjs +26 -26
- package/src/session-runtime/cwd-plugins.test.mjs +75 -0
- package/src/session-runtime/cwd-tool-routing.test.mjs +5 -0
- package/src/session-runtime/global-extensions.mjs +26 -0
- package/src/session-runtime/global-extensions.test.mjs +28 -0
- package/src/session-runtime/goal-runtime.mjs +721 -0
- package/src/session-runtime/goal-runtime.test.mjs +108 -0
- package/src/session-runtime/lifecycle-api.mjs +18 -4
- package/src/session-runtime/lifecycle-api.test.mjs +21 -0
- package/src/session-runtime/mcp-glue.mjs +49 -47
- package/src/session-runtime/plugin-mcp-standard.test.mjs +177 -0
- package/src/session-runtime/plugin-mcp.mjs +96 -22
- package/src/session-runtime/prewarm.mjs +1 -10
- package/src/session-runtime/resource-api-global.test.mjs +69 -0
- package/src/session-runtime/resource-api.mjs +173 -58
- package/src/session-runtime/runtime-core.mjs +148 -20
- package/src/session-runtime/runtime-tunables.mjs +6 -8
- package/src/session-runtime/session-turn-api.mjs +22 -11
- package/src/session-runtime/skills-api.mjs +79 -25
- package/src/session-runtime/skills-api.test.mjs +99 -0
- package/src/session-runtime/tool-catalog-schema.mjs +0 -5
- package/src/session-runtime/tool-catalog.mjs +4 -13
- package/src/session-runtime/tool-policy-surface.test.mjs +33 -11
- package/src/session-runtime/tool-surface.mjs +12 -0
- package/src/standalone/agent-dispatch-broker.mjs +5 -9
- package/src/standalone/agent-tool/notify.mjs +6 -8
- package/src/standalone/agent-tool/notify.test.mjs +37 -0
- package/src/standalone/daemon.mjs +31 -15
- package/src/standalone/hook-bus/config.mjs +1 -0
- package/src/standalone/plugin-admin.mjs +17 -0
- package/src/standalone/plugin-admin.test.mjs +7 -0
- package/src/standalone/provider-admin.mjs +1 -0
- package/src/standalone/session-protocol.mjs +5 -0
- package/src/standalone/session-runtime-agent-control-client.mjs +133 -0
- package/src/standalone/session-runtime-agent-control-client.test.mjs +71 -0
- package/src/standalone/session-runtime-dispatch-cancel.test.mjs +139 -0
- package/src/standalone/session-runtime-host-factory.mjs +17 -0
- package/src/standalone/session-runtime-host-factory.test.mjs +31 -0
- package/src/standalone/session-runtime-host-health.test.mjs +5 -0
- package/src/standalone/session-runtime-host.mjs +696 -58
- package/src/standalone/session-runtime-inline-host.mjs +351 -0
- package/src/standalone/session-runtime-inline-host.test.mjs +83 -0
- package/src/standalone/session-runtime-provider-cooldown.mjs +78 -0
- package/src/standalone/session-runtime-provider-cooldown.test.mjs +80 -0
- package/src/standalone/session-runtime-shard-host.test.mjs +569 -0
- package/src/standalone/session-runtime-shard-router.mjs +149 -0
- package/src/standalone/session-runtime-shard-router.test.mjs +101 -0
- package/src/standalone/session-runtime-worker.mjs +476 -18
- package/src/standalone/session-service.mjs +81 -6
- package/src/tui/app/app-view.jsx +1 -0
- package/src/tui/app/core-memory-picker.mjs +2 -0
- package/src/tui/app/extension-pickers.mjs +1 -1
- package/src/tui/app/maintenance-pickers.mjs +3 -3
- package/src/tui/app/model-options.mjs +2 -0
- package/src/tui/app/model-picker.mjs +14 -4
- package/src/tui/app/panel-handoff.test.mjs +311 -0
- package/src/tui/app/project-picker.mjs +1 -0
- package/src/tui/app/route-pickers.mjs +21 -6
- package/src/tui/app/settings-picker.mjs +26 -3
- package/src/tui/app/slash-commands.mjs +1 -0
- package/src/tui/app/slash-dispatch.mjs +73 -22
- package/src/tui/app/theme-effort-pickers.mjs +7 -2
- package/src/tui/components/Picker.jsx +10 -1
- package/src/tui/dist/index.mjs +163 -44
- package/src/tui/session/context-state.mjs +1 -0
- package/src/tui/session/goal-continuation.mjs +110 -0
- package/src/tui/session/goal-continuation.test.mjs +84 -0
- package/src/tui/session/queue-helpers.mjs +5 -2
- package/src/tui/session/session-action-surface.test.mjs +108 -0
- package/src/tui/session/session-api-ext.mjs +39 -8
- package/src/tui/session/session-api.mjs +58 -0
- package/src/tui/session/session-flow.mjs +6 -0
- package/src/tui/session/turn.mjs +1 -0
- package/src/tui/session-local.mjs +15 -1
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +0 -301
|
@@ -1,301 +0,0 @@
|
|
|
1
|
-
// fuzzy-match.mjs — lightweight subsequence fuzzy scorer for filename / path
|
|
2
|
-
// search. Returns a score where a
|
|
3
|
-
// higher value is a better match, or null when the query is not a subsequence
|
|
4
|
-
// of the candidate.
|
|
5
|
-
//
|
|
6
|
-
// Scoring favors, in rough order of weight:
|
|
7
|
-
// - matches at the START of the basename (last path segment)
|
|
8
|
-
// - matches at a word boundary (/, \, _, -, ., space, or camelCase hump)
|
|
9
|
-
// - contiguous runs of matched characters
|
|
10
|
-
// - exact-case hits (small tie-break)
|
|
11
|
-
// - shorter / earlier candidates (mild brevity + earliness pull)
|
|
12
|
-
//
|
|
13
|
-
// Matching is case-insensitive. The query is matched as an ordered subsequence
|
|
14
|
-
// (the chars must appear in order but need not be contiguous), which is what
|
|
15
|
-
// lets partial names surface matching files.
|
|
16
|
-
|
|
17
|
-
function isBoundaryChar(ch) {
|
|
18
|
-
return ch === '/' || ch === '\\' || ch === '_' || ch === '-' || ch === '.' || ch === ' ';
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// A camelCase hump (lower/digit followed by upper) is also a word boundary.
|
|
22
|
-
function isHump(prevCh, ch) {
|
|
23
|
-
return prevCh !== undefined
|
|
24
|
-
&& /[a-z0-9]/.test(prevCh)
|
|
25
|
-
&& /[A-Z]/.test(ch);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function prepareFuzzyScore(query) {
|
|
29
|
-
if (!query) return 0;
|
|
30
|
-
const normalizedQuery = String(query).replace(/[\/\\_.\-\s]+/g, '');
|
|
31
|
-
if (!normalizedQuery) return 0;
|
|
32
|
-
const q = normalizedQuery.toLowerCase();
|
|
33
|
-
const qlen = q.length;
|
|
34
|
-
return (str) => {
|
|
35
|
-
const s = str.toLowerCase();
|
|
36
|
-
const slen = s.length;
|
|
37
|
-
if (qlen === 0) return 0;
|
|
38
|
-
if (qlen > slen) return null;
|
|
39
|
-
|
|
40
|
-
const lastSep = Math.max(str.lastIndexOf('/'), str.lastIndexOf('\\'));
|
|
41
|
-
|
|
42
|
-
let score = 0;
|
|
43
|
-
let si = 0;
|
|
44
|
-
let prevMatch = -2;
|
|
45
|
-
let firstMatchIdx = -1;
|
|
46
|
-
|
|
47
|
-
for (let qi = 0; qi < qlen; qi++) {
|
|
48
|
-
const qc = q[qi];
|
|
49
|
-
let found = -1;
|
|
50
|
-
for (let k = si; k < slen; k++) {
|
|
51
|
-
if (s[k] === qc) { found = k; break; }
|
|
52
|
-
}
|
|
53
|
-
if (found === -1) return null;
|
|
54
|
-
if (firstMatchIdx === -1) firstMatchIdx = found;
|
|
55
|
-
|
|
56
|
-
score += 1; // base point per matched char
|
|
57
|
-
|
|
58
|
-
if (found === prevMatch + 1) score += 5; // contiguous run
|
|
59
|
-
|
|
60
|
-
const prevCh = found > 0 ? str[found - 1] : undefined;
|
|
61
|
-
if (prevCh === undefined || isBoundaryChar(prevCh) || isHump(prevCh, str[found])) {
|
|
62
|
-
score += 8; // word-boundary start
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (str[found] === normalizedQuery[qi]) score += 1; // exact-case tie-break
|
|
66
|
-
|
|
67
|
-
prevMatch = found;
|
|
68
|
-
si = found + 1;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Matches that begin inside the basename (after the last separator) are far
|
|
72
|
-
// more relevant than ones buried in directory components.
|
|
73
|
-
if (firstMatchIdx > lastSep) score += 10;
|
|
74
|
-
|
|
75
|
-
// Mild pulls: shorter candidates and earlier first matches rank higher.
|
|
76
|
-
score -= Math.floor(slen / 16);
|
|
77
|
-
score -= Math.floor(firstMatchIdx / 8);
|
|
78
|
-
|
|
79
|
-
return score;
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* @param {string} query user query (partial name)
|
|
85
|
-
* @param {string} str candidate path (relative)
|
|
86
|
-
* @returns {number|null} score, or null if `query` is not a subsequence
|
|
87
|
-
*/
|
|
88
|
-
function fuzzyScore(query, str) {
|
|
89
|
-
const scorer = prepareFuzzyScore(query);
|
|
90
|
-
return typeof scorer === 'function' ? scorer(str) : scorer;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Below this per-query-char score, a subsequence-only match (query chars in
|
|
94
|
-
// order but scattered mid-word, no contiguity/boundary structure) is treated
|
|
95
|
-
// as noise rather than a real hit. Contiguous substring / basename matches
|
|
96
|
-
// bypass the floor entirely — an exact hit must never be filtered.
|
|
97
|
-
// Measured separation for scattered junk vs real hits ("readme" probe):
|
|
98
|
-
// genuine matches score ≥8/char (boundary + contiguity structure), while
|
|
99
|
-
// incidental subsequences on unrelated paths land at 5–6/char — 6.5 splits
|
|
100
|
-
// them. The floor is tested against the RAW subsequence score (see
|
|
101
|
-
// fuzzyRank), never the +40 basename rank bonus, which otherwise vaults
|
|
102
|
-
// weak basename subsequences straight past any floor.
|
|
103
|
-
const SUBSEQUENCE_MIN_PER_CHAR = 6.5;
|
|
104
|
-
|
|
105
|
-
// Separator/case-insensitive normalization used for the "contiguous substring"
|
|
106
|
-
// strong-match test. Mirrors the query normalization in fuzzyScore so
|
|
107
|
-
// "tool-events.log" matches a "tool-events.log" basename or a ".../tool-events.log"
|
|
108
|
-
// path regardless of separators.
|
|
109
|
-
function normalizeForContains(s) {
|
|
110
|
-
return String(s || '').toLowerCase().replace(/[\/\\_.\-\s]+/g, '');
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function fuzzyAsciiMask(value) {
|
|
114
|
-
let lo = 0;
|
|
115
|
-
let hi = 0;
|
|
116
|
-
const text = String(value || '');
|
|
117
|
-
for (let index = 0; index < text.length; index++) {
|
|
118
|
-
let code = text.charCodeAt(index);
|
|
119
|
-
if (code >= 65 && code <= 90) code += 32;
|
|
120
|
-
let bit = -1;
|
|
121
|
-
if (code >= 48 && code <= 57) bit = code - 48;
|
|
122
|
-
else if (code >= 97 && code <= 122) bit = code - 97 + 10;
|
|
123
|
-
if (bit < 0) continue;
|
|
124
|
-
if (bit < 32) lo |= (1 << bit);
|
|
125
|
-
else hi |= (1 << (bit - 32));
|
|
126
|
-
}
|
|
127
|
-
return { lo: lo >>> 0, hi: hi >>> 0 };
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export function prepareFuzzyItems(paths) {
|
|
131
|
-
return (paths || []).map((path) => {
|
|
132
|
-
const value = String(path || '');
|
|
133
|
-
const mask = fuzzyAsciiMask(value);
|
|
134
|
-
return {
|
|
135
|
-
path: value,
|
|
136
|
-
_fuzzyMaskLo: mask.lo,
|
|
137
|
-
_fuzzyMaskHi: mask.hi,
|
|
138
|
-
_fuzzyLastSep: Math.max(value.lastIndexOf('/'), value.lastIndexOf('\\')),
|
|
139
|
-
};
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function compareRanked(a, b) {
|
|
144
|
-
return (b.score - a.score)
|
|
145
|
-
|| (a.item.path < b.item.path ? -1 : a.item.path > b.item.path ? 1 : 0);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function compareRankedNodes(a, b) {
|
|
149
|
-
return compareRanked(a.entry, b.entry) || (a.ordinal - b.ordinal);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function siftDownRankedHeap(heap, index) {
|
|
153
|
-
for (;;) {
|
|
154
|
-
const left = index * 2 + 1;
|
|
155
|
-
const right = left + 1;
|
|
156
|
-
let worst = index;
|
|
157
|
-
if (left < heap.length && compareRankedNodes(heap[left], heap[worst]) > 0) worst = left;
|
|
158
|
-
if (right < heap.length && compareRankedNodes(heap[right], heap[worst]) > 0) worst = right;
|
|
159
|
-
if (worst === index) return;
|
|
160
|
-
const node = heap[index];
|
|
161
|
-
heap[index] = heap[worst];
|
|
162
|
-
heap[worst] = node;
|
|
163
|
-
index = worst;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Keep the worst retained candidate at index zero. The ordinal completes the
|
|
168
|
-
// public comparator only for heap bookkeeping, preserving stable-sort behavior
|
|
169
|
-
// for duplicate score/path entries.
|
|
170
|
-
function retainTopRanked(heap, entry, ordinal, limit) {
|
|
171
|
-
const node = { entry, ordinal };
|
|
172
|
-
if (heap.length < limit) {
|
|
173
|
-
heap.push(node);
|
|
174
|
-
for (let index = heap.length - 1; index > 0;) {
|
|
175
|
-
const parent = Math.floor((index - 1) / 2);
|
|
176
|
-
if (compareRankedNodes(heap[index], heap[parent]) <= 0) break;
|
|
177
|
-
const parentNode = heap[parent];
|
|
178
|
-
heap[parent] = heap[index];
|
|
179
|
-
heap[index] = parentNode;
|
|
180
|
-
index = parent;
|
|
181
|
-
}
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
if (compareRankedNodes(node, heap[0]) < 0) {
|
|
185
|
-
heap[0] = node;
|
|
186
|
-
siftDownRankedHeap(heap, 0);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Rank candidates by fuzzy score against `query`, dropping non-matches.
|
|
192
|
-
* @param {string} query
|
|
193
|
-
* @param {Array<{path:string}>} items each must expose a `path` string
|
|
194
|
-
* @param {number} [limit]
|
|
195
|
-
* @returns {Array<{item:object, score:number}>} sorted desc, then path asc
|
|
196
|
-
*/
|
|
197
|
-
export function fuzzyRank(query, items, limit = 0) {
|
|
198
|
-
const state = makeTokenScorer(query);
|
|
199
|
-
const scored = [];
|
|
200
|
-
// Preserve slice's legacy coercion behavior for non-integer public callers;
|
|
201
|
-
// normal tool limits are positive integers and take the bounded fast path.
|
|
202
|
-
const bounded = Number.isInteger(limit) && limit > 0;
|
|
203
|
-
for (let ordinal = 0; ordinal < items.length; ordinal++) {
|
|
204
|
-
const sc = scoreItemAgainstToken(state, items[ordinal]);
|
|
205
|
-
if (sc === null) continue;
|
|
206
|
-
const entry = { item: items[ordinal], score: sc };
|
|
207
|
-
if (bounded) retainTopRanked(scored, entry, ordinal, limit);
|
|
208
|
-
else scored.push(entry);
|
|
209
|
-
}
|
|
210
|
-
if (!bounded) {
|
|
211
|
-
scored.sort(compareRanked);
|
|
212
|
-
return limit > 0 ? scored.slice(0, limit) : scored;
|
|
213
|
-
}
|
|
214
|
-
return scored
|
|
215
|
-
.sort(compareRankedNodes)
|
|
216
|
-
.map(({ entry }) => entry);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Prepared per-token scoring state shared by fuzzyRank / fuzzyRankMulti.
|
|
220
|
-
function makeTokenScorer(token) {
|
|
221
|
-
const normQuery = normalizeForContains(token);
|
|
222
|
-
return {
|
|
223
|
-
normQuery,
|
|
224
|
-
queryMask: fuzzyAsciiMask(normQuery),
|
|
225
|
-
score: prepareFuzzyScore(token),
|
|
226
|
-
// Floor scales with query length: a scattered subsequence earns ~1
|
|
227
|
-
// point per char, while any contiguous run (+5/char) or word-boundary
|
|
228
|
-
// hit (+8) pushes a genuine match well past 6.5/char.
|
|
229
|
-
floor: normQuery.length * SUBSEQUENCE_MIN_PER_CHAR,
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Gate + score ONE candidate against ONE prepared token. Returns the rank
|
|
234
|
-
// score (basename bonus included) or null when the candidate fails the ASCII
|
|
235
|
-
// mask prefilter or the subsequence quality floor.
|
|
236
|
-
function scoreItemAgainstToken(state, item) {
|
|
237
|
-
const p = String(item.path || '');
|
|
238
|
-
if (Number.isInteger(item._fuzzyMaskLo)
|
|
239
|
-
&& (((item._fuzzyMaskLo >>> 0) & state.queryMask.lo) >>> 0) !== state.queryMask.lo) return null;
|
|
240
|
-
if (Number.isInteger(item._fuzzyMaskHi)
|
|
241
|
-
&& (((item._fuzzyMaskHi >>> 0) & state.queryMask.hi) >>> 0) !== state.queryMask.hi) return null;
|
|
242
|
-
const lastSep = Number.isInteger(item._fuzzyLastSep)
|
|
243
|
-
? item._fuzzyLastSep
|
|
244
|
-
: Math.max(p.lastIndexOf('/'), p.lastIndexOf('\\'));
|
|
245
|
-
const base = p.slice(lastSep + 1);
|
|
246
|
-
const pathScore = typeof state.score === 'function' ? state.score(p) : state.score;
|
|
247
|
-
const baseScore = typeof state.score === 'function' ? state.score(base) : state.score;
|
|
248
|
-
const sc = Math.max(pathScore ?? -Infinity, baseScore === null ? -Infinity : baseScore + 40);
|
|
249
|
-
if (!Number.isFinite(sc)) return null;
|
|
250
|
-
// Strong match: the query (separators stripped) is a contiguous
|
|
251
|
-
// substring of the basename or the full path. These ALWAYS pass so an
|
|
252
|
-
// exact substring/basename hit can never be starved out as noise.
|
|
253
|
-
const strong = state.normQuery.length > 0 && normalizeForContains(p).includes(state.normQuery);
|
|
254
|
-
// Otherwise it is subsequence-only: keep it only if it clears the
|
|
255
|
-
// per-char floor. Weak scattered matches (the pgAdmin-style junk that
|
|
256
|
-
// merely contains the query chars in order) fall below it and drop out.
|
|
257
|
-
// Compare the RAW subsequence score — the +40 basename rank bonus is
|
|
258
|
-
// for ordering only and must not lift junk over the quality floor.
|
|
259
|
-
const rawSc = Math.max(pathScore ?? -Infinity, baseScore ?? -Infinity);
|
|
260
|
-
if (!strong && rawSc < state.floor) return null;
|
|
261
|
-
return sc;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/** Split a find query into whitespace-separated fragments. */
|
|
265
|
-
export function splitFuzzyQueryTokens(query) {
|
|
266
|
-
return String(query || '').trim().split(/\s+/).filter(Boolean);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Multi-fragment AND ranking: every whitespace-separated fragment of `query`
|
|
271
|
-
* must independently match a candidate (same gate as fuzzyRank); the rank
|
|
272
|
-
* score is the fragment sum. Single-fragment queries delegate to fuzzyRank
|
|
273
|
-
* unchanged.
|
|
274
|
-
*/
|
|
275
|
-
export function fuzzyRankMulti(query, items, limit = 0) {
|
|
276
|
-
const tokens = splitFuzzyQueryTokens(query);
|
|
277
|
-
if (tokens.length <= 1) return fuzzyRank(tokens[0] ?? String(query || ''), items, limit);
|
|
278
|
-
const states = tokens.map(makeTokenScorer);
|
|
279
|
-
const scored = [];
|
|
280
|
-
const bounded = Number.isInteger(limit) && limit > 0;
|
|
281
|
-
for (let ordinal = 0; ordinal < items.length; ordinal++) {
|
|
282
|
-
const item = items[ordinal];
|
|
283
|
-
let total = 0;
|
|
284
|
-
for (const state of states) {
|
|
285
|
-
const sc = scoreItemAgainstToken(state, item);
|
|
286
|
-
if (sc === null) { total = null; break; }
|
|
287
|
-
total += sc;
|
|
288
|
-
}
|
|
289
|
-
if (total === null) continue;
|
|
290
|
-
const entry = { item, score: total };
|
|
291
|
-
if (bounded) retainTopRanked(scored, entry, ordinal, limit);
|
|
292
|
-
else scored.push(entry);
|
|
293
|
-
}
|
|
294
|
-
if (!bounded) {
|
|
295
|
-
scored.sort(compareRanked);
|
|
296
|
-
return limit > 0 ? scored.slice(0, limit) : scored;
|
|
297
|
-
}
|
|
298
|
-
return scored
|
|
299
|
-
.sort(compareRankedNodes)
|
|
300
|
-
.map(({ entry }) => entry);
|
|
301
|
-
}
|