@nebutra/agent-runtime 0.2.1 → 0.2.2
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/LICENSE +21 -676
- package/dist/adapters/index.d.ts +24 -5
- package/dist/adapters/index.js +27 -0
- package/dist/adapters/index.js.map +1 -1
- package/dist/chunk-Q62VKHIT.js +178 -0
- package/dist/chunk-Q62VKHIT.js.map +1 -0
- package/dist/{chunk-MUF7ZZTO.js → chunk-R5HOSQUW.js} +2 -1
- package/dist/{chunk-MUF7ZZTO.js.map → chunk-R5HOSQUW.js.map} +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +14 -17
- package/dist/index.js.map +1 -1
- package/dist/orchestration.d.ts +42 -1
- package/dist/orchestration.js +7 -3
- package/dist/sandbox.js +1 -1
- package/package.json +80 -30
- package/.turbo/turbo-build.log +0 -130
- package/.turbo/turbo-test.log +0 -47
- package/.turbo/turbo-typecheck.log +0 -4
- package/CHANGELOG.md +0 -250
- package/dist/chunk-MX2WL43P.js +0 -90
- package/dist/chunk-MX2WL43P.js.map +0 -1
- package/examples/pulsar-quickstart.ts +0 -35
- package/examples/resume-branch.ts +0 -45
- package/examples/subagent-fanout.ts +0 -20
- package/src/adapters/dispatcher-sse.test.ts +0 -218
- package/src/adapters/dispatcher-sse.ts +0 -222
- package/src/adapters/index.ts +0 -18
- package/src/adapters/mcp-catalog.test.ts +0 -213
- package/src/adapters/mcp-catalog.ts +0 -188
- package/src/adapters/prisma-rollout.test.ts +0 -153
- package/src/adapters/prisma-rollout.ts +0 -104
- package/src/agent-runtime.test.ts +0 -176
- package/src/artifact-stream.test.ts +0 -330
- package/src/artifact-stream.ts +0 -453
- package/src/channel-gateway.test.ts +0 -432
- package/src/channel-gateway.ts +0 -357
- package/src/cli.ts +0 -49
- package/src/code-review.test.ts +0 -501
- package/src/code-review.ts +0 -495
- package/src/command-suggestions.test.ts +0 -251
- package/src/command-suggestions.ts +0 -338
- package/src/commands.test.ts +0 -184
- package/src/commands.ts +0 -140
- package/src/commit-message.test.ts +0 -249
- package/src/commit-message.ts +0 -180
- package/src/context-compaction.test.ts +0 -522
- package/src/context-compaction.ts +0 -438
- package/src/definitions.test.ts +0 -78
- package/src/definitions.ts +0 -190
- package/src/deployment-status.test.ts +0 -215
- package/src/deployment-status.ts +0 -227
- package/src/design-context.test.ts +0 -195
- package/src/design-context.ts +0 -198
- package/src/dispatcher.test.ts +0 -234
- package/src/dispatcher.ts +0 -189
- package/src/durable-turn.test.ts +0 -209
- package/src/durable-turn.ts +0 -135
- package/src/edit-planner.test.ts +0 -204
- package/src/edit-planner.ts +0 -325
- package/src/fuzzy-match.test.ts +0 -311
- package/src/fuzzy-match.ts +0 -444
- package/src/hook-pipeline.test.ts +0 -279
- package/src/hook-pipeline.ts +0 -373
- package/src/inbound-admission.test.ts +0 -394
- package/src/inbound-admission.ts +0 -246
- package/src/index.ts +0 -49
- package/src/loop.test.ts +0 -161
- package/src/loop.ts +0 -211
- package/src/mcp-bridge.test.ts +0 -165
- package/src/mcp-bridge.ts +0 -81
- package/src/memory-provider.test.ts +0 -232
- package/src/memory-provider.ts +0 -257
- package/src/model.ts +0 -168
- package/src/orchestration.test.ts +0 -53
- package/src/orchestration.ts +0 -146
- package/src/permission-ruleset.test.ts +0 -301
- package/src/permission-ruleset.ts +0 -1
- package/src/policy.ts +0 -151
- package/src/project-repo.test.ts +0 -232
- package/src/project-repo.ts +0 -311
- package/src/protocol.ts +0 -159
- package/src/pulsar.test.ts +0 -156
- package/src/pulsar.ts +0 -322
- package/src/rollout-store-persistent.test.ts +0 -217
- package/src/rollout-store-persistent.ts +0 -166
- package/src/rollout.ts +0 -150
- package/src/sandbox.ts +0 -113
- package/src/session-share.test.ts +0 -360
- package/src/session-share.ts +0 -310
- package/src/skill-distillation.test.ts +0 -177
- package/src/skill-distillation.ts +0 -369
- package/src/skills.test.ts +0 -277
- package/src/skills.ts +0 -277
- package/src/subagents.test.ts +0 -290
- package/src/subagents.ts +0 -332
- package/src/tools.test.ts +0 -12
- package/src/tools.ts +0 -129
- package/src/workbench.test.ts +0 -0
- package/src/workbench.ts +0 -0
- package/tsconfig.json +0 -12
- package/tsup.config.ts +0 -36
package/src/fuzzy-match.ts
DELETED
|
@@ -1,444 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* fuzzy-match — pure, deterministic string-matching primitives.
|
|
3
|
-
*
|
|
4
|
-
* A dependency-free re-expression of the classic "smart-case fuzzy +
|
|
5
|
-
* glob-wildcard" matcher found in modern terminals and command palettes.
|
|
6
|
-
* Everything here is a small pure function: no I/O, no shared state, no
|
|
7
|
-
* mutation of inputs, no `console`.
|
|
8
|
-
*
|
|
9
|
-
* Surface:
|
|
10
|
-
* - matchIndices smart-case subsequence match
|
|
11
|
-
* - matchIndicesCaseInsensitive always case-insensitive
|
|
12
|
-
* - matchIndicesCaseInsensitiveIgnoreSpaces ci + spaces ignored
|
|
13
|
-
* - matchWildcardPattern glob (* ? [..] \) full-string
|
|
14
|
-
* - matchWildcardPatternCaseInsensitive glob, case-insensitive
|
|
15
|
-
* - rankByFuzzy generic rank/filter/sort helper
|
|
16
|
-
*
|
|
17
|
-
* --------------------------------------------------------------------------
|
|
18
|
-
* Subsequence scoring heuristic (deterministic; only ORDERING is contractual)
|
|
19
|
-
* --------------------------------------------------------------------------
|
|
20
|
-
* Every char of `query` must appear in `text` in order. Among the many
|
|
21
|
-
* possible subsequences we use a single forward greedy scan with a fixed,
|
|
22
|
-
* documented tie-break so the result is fully deterministic:
|
|
23
|
-
*
|
|
24
|
-
* For each query char, scan `text` forward from the previous match + 1 and
|
|
25
|
-
* collect every candidate position. Pick the candidate that maximises a
|
|
26
|
-
* per-char local score:
|
|
27
|
-
*
|
|
28
|
-
* + WORD_BOUNDARY_BONUS if the candidate is at text start, or the
|
|
29
|
-
* preceding char is a separator (`/ _ - . space`)
|
|
30
|
-
* or a lower→upper camelCase hump.
|
|
31
|
-
* + CONTIGUOUS_BONUS if the candidate immediately follows the
|
|
32
|
-
* previous matched index (a run).
|
|
33
|
-
* - GAP_PENALTY * gap distance skipped since the previous match.
|
|
34
|
-
* - LEADING_PENALTY (first query char only) * its absolute index,
|
|
35
|
-
* so earlier overall placement is preferred.
|
|
36
|
-
*
|
|
37
|
-
* Ties on local score break toward the EARLIEST candidate index — this is
|
|
38
|
-
* what makes the scan deterministic and is exercised by the tests.
|
|
39
|
-
*
|
|
40
|
-
* The aggregate `score` is the sum of per-char local scores. Higher is a
|
|
41
|
-
* better match. Magnitudes are intentionally unspecified; only the relative
|
|
42
|
-
* ordering (boundary/contiguous/at-start outranks scattered/buried/late) is
|
|
43
|
-
* part of the contract.
|
|
44
|
-
*
|
|
45
|
-
* Empty query: defined to match everything with `score === 0` and
|
|
46
|
-
* `indices === []`. Empty text with a non-empty query: `null`.
|
|
47
|
-
*/
|
|
48
|
-
|
|
49
|
-
const SEPARATORS = new Set(["/", "_", "-", ".", " "]);
|
|
50
|
-
|
|
51
|
-
const WORD_BOUNDARY_BONUS = 16;
|
|
52
|
-
const CONTIGUOUS_BONUS = 12;
|
|
53
|
-
const STRING_START_BONUS = 8;
|
|
54
|
-
const GAP_PENALTY = 1;
|
|
55
|
-
const LEADING_PENALTY = 2;
|
|
56
|
-
|
|
57
|
-
/** A successful match: the chosen text positions and an aggregate score. */
|
|
58
|
-
export interface FuzzyMatch {
|
|
59
|
-
readonly score: number;
|
|
60
|
-
readonly indices: readonly number[];
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
interface MatchOptions {
|
|
64
|
-
/** Force case-insensitive matching regardless of query casing. */
|
|
65
|
-
readonly forceCaseInsensitive?: boolean | undefined;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function hasUpperCase(value: string): boolean {
|
|
69
|
-
return value !== value.toLowerCase();
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function isSeparator(ch: string | undefined): boolean {
|
|
73
|
-
return ch !== undefined && SEPARATORS.has(ch);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* True when `text[index]` sits at a "word boundary": string start, right
|
|
78
|
-
* after a separator, or a lower→upper camelCase hump.
|
|
79
|
-
*/
|
|
80
|
-
function isBoundary(text: string, index: number): boolean {
|
|
81
|
-
if (index === 0) {
|
|
82
|
-
return true;
|
|
83
|
-
}
|
|
84
|
-
const prev = text[index - 1];
|
|
85
|
-
if (isSeparator(prev)) {
|
|
86
|
-
return true;
|
|
87
|
-
}
|
|
88
|
-
const cur = text[index];
|
|
89
|
-
if (prev !== undefined && cur !== undefined) {
|
|
90
|
-
const prevLower = prev === prev.toLowerCase() && prev !== prev.toUpperCase();
|
|
91
|
-
const curUpper = cur === cur.toUpperCase() && cur !== cur.toLowerCase();
|
|
92
|
-
if (prevLower && curUpper) {
|
|
93
|
-
return true;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return false;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Core greedy subsequence scan.
|
|
101
|
-
*
|
|
102
|
-
* @param text the haystack
|
|
103
|
-
* @param query the needle
|
|
104
|
-
* @param caseInsensitive compare lower-cased chars
|
|
105
|
-
* @param skipChar optional predicate: text chars for which this returns true
|
|
106
|
-
* are skipped for *matching* but their original index is never
|
|
107
|
-
* emitted (used by the ignore-spaces variant). Query chars for
|
|
108
|
-
* which it returns true are dropped before scanning.
|
|
109
|
-
*/
|
|
110
|
-
function scan(
|
|
111
|
-
text: string,
|
|
112
|
-
query: string,
|
|
113
|
-
caseInsensitive: boolean,
|
|
114
|
-
skipChar?: (ch: string) => boolean,
|
|
115
|
-
): FuzzyMatch | null {
|
|
116
|
-
// Drop skipped chars from the query (e.g. spaces) up front.
|
|
117
|
-
const effectiveQuery = skipChar
|
|
118
|
-
? Array.from(query)
|
|
119
|
-
.filter((c) => !skipChar(c))
|
|
120
|
-
.join("")
|
|
121
|
-
: query;
|
|
122
|
-
|
|
123
|
-
if (effectiveQuery.length === 0) {
|
|
124
|
-
return { score: 0, indices: [] };
|
|
125
|
-
}
|
|
126
|
-
if (text.length === 0) {
|
|
127
|
-
return null;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const fold = (c: string): string => (caseInsensitive ? c.toLowerCase() : c);
|
|
131
|
-
|
|
132
|
-
const indices: number[] = [];
|
|
133
|
-
let score = 0;
|
|
134
|
-
let searchFrom = 0;
|
|
135
|
-
let prevMatched = -1;
|
|
136
|
-
|
|
137
|
-
for (let q = 0; q < effectiveQuery.length; q++) {
|
|
138
|
-
const target = fold(effectiveQuery[q]!);
|
|
139
|
-
|
|
140
|
-
let bestIndex = -1;
|
|
141
|
-
let bestLocal = Number.NEGATIVE_INFINITY;
|
|
142
|
-
|
|
143
|
-
for (let t = searchFrom; t < text.length; t++) {
|
|
144
|
-
const tch = text[t]!;
|
|
145
|
-
if (skipChar && skipChar(tch)) {
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
148
|
-
if (fold(tch) !== target) {
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
let local = 0;
|
|
153
|
-
if (isBoundary(text, t)) {
|
|
154
|
-
local += WORD_BOUNDARY_BONUS;
|
|
155
|
-
}
|
|
156
|
-
if (t === 0) {
|
|
157
|
-
local += STRING_START_BONUS;
|
|
158
|
-
}
|
|
159
|
-
if (prevMatched >= 0 && t === prevMatched + 1) {
|
|
160
|
-
local += CONTIGUOUS_BONUS;
|
|
161
|
-
}
|
|
162
|
-
const gap = prevMatched >= 0 ? t - prevMatched - 1 : 0;
|
|
163
|
-
local -= GAP_PENALTY * gap;
|
|
164
|
-
if (q === 0) {
|
|
165
|
-
local -= LEADING_PENALTY * t;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Deterministic tie-break: strictly-greater wins, so the EARLIEST
|
|
169
|
-
// candidate is retained on ties.
|
|
170
|
-
if (local > bestLocal) {
|
|
171
|
-
bestLocal = local;
|
|
172
|
-
bestIndex = t;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (bestIndex === -1) {
|
|
177
|
-
return null;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
indices.push(bestIndex);
|
|
181
|
-
score += bestLocal;
|
|
182
|
-
prevMatched = bestIndex;
|
|
183
|
-
searchFrom = bestIndex + 1;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return { score, indices };
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Smart-case subsequence fuzzy match.
|
|
191
|
-
*
|
|
192
|
-
* If `query` is all-lowercase the match is case-insensitive; if it contains
|
|
193
|
-
* any uppercase char the *whole* query matches case-sensitively. Returns the
|
|
194
|
-
* chosen 0-based `indices` in `text` plus an aggregate `score`, or `null` if
|
|
195
|
-
* `query` is not a subsequence of `text`.
|
|
196
|
-
*/
|
|
197
|
-
export function matchIndices(
|
|
198
|
-
text: string,
|
|
199
|
-
query: string,
|
|
200
|
-
options: MatchOptions = {},
|
|
201
|
-
): FuzzyMatch | null {
|
|
202
|
-
const caseInsensitive = options.forceCaseInsensitive === true || !hasUpperCase(query);
|
|
203
|
-
return scan(text, query, caseInsensitive);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/** Like {@link matchIndices} but always case-insensitive. */
|
|
207
|
-
export function matchIndicesCaseInsensitive(text: string, query: string): FuzzyMatch | null {
|
|
208
|
-
return scan(text, query, true);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Case-insensitive subsequence match where spaces in BOTH `query` and `text`
|
|
213
|
-
* are ignored for matching. Returned `indices` always point at original
|
|
214
|
-
* `text` positions and never land on a skipped space.
|
|
215
|
-
*/
|
|
216
|
-
export function matchIndicesCaseInsensitiveIgnoreSpaces(
|
|
217
|
-
text: string,
|
|
218
|
-
query: string,
|
|
219
|
-
): FuzzyMatch | null {
|
|
220
|
-
return scan(text, query, true, (c) => c === " ");
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// ---------------------------------------------------------------------------
|
|
224
|
-
// Wildcard / glob matcher
|
|
225
|
-
// ---------------------------------------------------------------------------
|
|
226
|
-
|
|
227
|
-
type GlobToken =
|
|
228
|
-
| { readonly kind: "literal"; readonly ch: string }
|
|
229
|
-
| { readonly kind: "any" } // *
|
|
230
|
-
| { readonly kind: "single" } // ?
|
|
231
|
-
| {
|
|
232
|
-
readonly kind: "class";
|
|
233
|
-
readonly negated: boolean;
|
|
234
|
-
readonly ranges: ReadonlyArray<readonly [number, number]>;
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Compile a glob pattern into a token stream. Supports:
|
|
239
|
-
* `*` any run (including empty)
|
|
240
|
-
* `?` exactly one char
|
|
241
|
-
* `[..]` / `[a-z]` / `[!..]` char class (negation via leading `!` or `^`)
|
|
242
|
-
* `\x` escape — next char is literal
|
|
243
|
-
* `/` is an ordinary literal (path-friendly). Unterminated `[` is treated as
|
|
244
|
-
* a literal `[`.
|
|
245
|
-
*/
|
|
246
|
-
function compileGlob(pattern: string): readonly GlobToken[] {
|
|
247
|
-
const tokens: GlobToken[] = [];
|
|
248
|
-
let i = 0;
|
|
249
|
-
while (i < pattern.length) {
|
|
250
|
-
const ch = pattern[i]!;
|
|
251
|
-
if (ch === "\\") {
|
|
252
|
-
const next = pattern[i + 1];
|
|
253
|
-
if (next !== undefined) {
|
|
254
|
-
tokens.push({ kind: "literal", ch: next });
|
|
255
|
-
i += 2;
|
|
256
|
-
continue;
|
|
257
|
-
}
|
|
258
|
-
tokens.push({ kind: "literal", ch: "\\" });
|
|
259
|
-
i += 1;
|
|
260
|
-
continue;
|
|
261
|
-
}
|
|
262
|
-
if (ch === "*") {
|
|
263
|
-
tokens.push({ kind: "any" });
|
|
264
|
-
i += 1;
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
267
|
-
if (ch === "?") {
|
|
268
|
-
tokens.push({ kind: "single" });
|
|
269
|
-
i += 1;
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
272
|
-
if (ch === "[") {
|
|
273
|
-
const close = pattern.indexOf("]", i + 1);
|
|
274
|
-
if (close === -1) {
|
|
275
|
-
tokens.push({ kind: "literal", ch: "[" });
|
|
276
|
-
i += 1;
|
|
277
|
-
continue;
|
|
278
|
-
}
|
|
279
|
-
let body = pattern.slice(i + 1, close);
|
|
280
|
-
let negated = false;
|
|
281
|
-
if (body.startsWith("!") || body.startsWith("^")) {
|
|
282
|
-
negated = true;
|
|
283
|
-
body = body.slice(1);
|
|
284
|
-
}
|
|
285
|
-
const ranges: Array<readonly [number, number]> = [];
|
|
286
|
-
let b = 0;
|
|
287
|
-
while (b < body.length) {
|
|
288
|
-
const c0 = body[b]!;
|
|
289
|
-
if (body[b + 1] === "-" && b + 2 < body.length) {
|
|
290
|
-
const c1 = body[b + 2]!;
|
|
291
|
-
ranges.push([c0.charCodeAt(0), c1.charCodeAt(0)]);
|
|
292
|
-
b += 3;
|
|
293
|
-
} else {
|
|
294
|
-
ranges.push([c0.charCodeAt(0), c0.charCodeAt(0)]);
|
|
295
|
-
b += 1;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
tokens.push({ kind: "class", negated, ranges });
|
|
299
|
-
i = close + 1;
|
|
300
|
-
continue;
|
|
301
|
-
}
|
|
302
|
-
tokens.push({ kind: "literal", ch });
|
|
303
|
-
i += 1;
|
|
304
|
-
}
|
|
305
|
-
return tokens;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
function classMatches(token: Extract<GlobToken, { kind: "class" }>, code: number): boolean {
|
|
309
|
-
let inRange = false;
|
|
310
|
-
for (const [lo, hi] of token.ranges) {
|
|
311
|
-
if (code >= lo && code <= hi) {
|
|
312
|
-
inRange = true;
|
|
313
|
-
break;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
return token.negated ? !inRange : inRange;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* Linear-time anchored glob match (no RegExp, no catastrophic backtracking).
|
|
321
|
-
*
|
|
322
|
-
* Classic two-pointer wildcard algorithm: on a `*` we record a backtrack
|
|
323
|
-
* point and greedily advance; on any later mismatch we resume the last `*`
|
|
324
|
-
* one char further. This is O(text * pattern) worst case — adversarial
|
|
325
|
-
* `***...*b` inputs stay fast.
|
|
326
|
-
*/
|
|
327
|
-
function globMatch(text: string, tokens: readonly GlobToken[], caseInsensitive: boolean): boolean {
|
|
328
|
-
const fold = (c: string): string => (caseInsensitive ? c.toLowerCase() : c);
|
|
329
|
-
|
|
330
|
-
let ti = 0; // text index
|
|
331
|
-
let pi = 0; // pattern (token) index
|
|
332
|
-
let starPi = -1; // last '*' token index
|
|
333
|
-
let starTi = 0; // text index when last '*' was seen
|
|
334
|
-
|
|
335
|
-
while (ti < text.length) {
|
|
336
|
-
const tok = tokens[pi];
|
|
337
|
-
if (tok !== undefined && tok.kind === "any") {
|
|
338
|
-
starPi = pi;
|
|
339
|
-
starTi = ti;
|
|
340
|
-
pi += 1;
|
|
341
|
-
continue;
|
|
342
|
-
}
|
|
343
|
-
if (tok !== undefined && tok.kind === "single") {
|
|
344
|
-
pi += 1;
|
|
345
|
-
ti += 1;
|
|
346
|
-
continue;
|
|
347
|
-
}
|
|
348
|
-
if (tok !== undefined && tok.kind === "literal") {
|
|
349
|
-
if (fold(text[ti]!) === fold(tok.ch)) {
|
|
350
|
-
pi += 1;
|
|
351
|
-
ti += 1;
|
|
352
|
-
continue;
|
|
353
|
-
}
|
|
354
|
-
} else if (tok !== undefined && tok.kind === "class") {
|
|
355
|
-
const ch = caseInsensitive ? text[ti]!.toLowerCase() : text[ti]!;
|
|
356
|
-
// For case-insensitive class matching, test both casings.
|
|
357
|
-
const matched = caseInsensitive
|
|
358
|
-
? classMatches(tok, ch.charCodeAt(0)) ||
|
|
359
|
-
classMatches(tok, text[ti]!.toUpperCase().charCodeAt(0))
|
|
360
|
-
: classMatches(tok, ch.charCodeAt(0));
|
|
361
|
-
if (matched) {
|
|
362
|
-
pi += 1;
|
|
363
|
-
ti += 1;
|
|
364
|
-
continue;
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// Mismatch: resume from the last '*' if any.
|
|
369
|
-
if (starPi !== -1) {
|
|
370
|
-
pi = starPi + 1;
|
|
371
|
-
starTi += 1;
|
|
372
|
-
ti = starTi;
|
|
373
|
-
continue;
|
|
374
|
-
}
|
|
375
|
-
return false;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// Consume any trailing '*' tokens.
|
|
379
|
-
while (pi < tokens.length && tokens[pi]!.kind === "any") {
|
|
380
|
-
pi += 1;
|
|
381
|
-
}
|
|
382
|
-
return pi === tokens.length;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
/**
|
|
386
|
-
* Anchored (full-string) glob match. `*` = any run incl. empty, `?` = one
|
|
387
|
-
* char, `[..]`/`[a-z]`/`[!..]` char classes, `\` escapes a metachar. `/` is
|
|
388
|
-
* an ordinary char. Case-sensitive.
|
|
389
|
-
*/
|
|
390
|
-
export function matchWildcardPattern(text: string, pattern: string): boolean {
|
|
391
|
-
return globMatch(text, compileGlob(pattern), false);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
/** Case-insensitive variant of {@link matchWildcardPattern}. */
|
|
395
|
-
export function matchWildcardPatternCaseInsensitive(text: string, pattern: string): boolean {
|
|
396
|
-
return globMatch(text, compileGlob(pattern), true);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// ---------------------------------------------------------------------------
|
|
400
|
-
// Ranking helper
|
|
401
|
-
// ---------------------------------------------------------------------------
|
|
402
|
-
|
|
403
|
-
/** One ranked result: the original item plus its match score and indices. */
|
|
404
|
-
export interface RankedFuzzyResult<T> {
|
|
405
|
-
readonly item: T;
|
|
406
|
-
readonly score: number;
|
|
407
|
-
readonly indices: readonly number[];
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
interface RankByFuzzyOptions {
|
|
411
|
-
/** Force case-insensitive matching regardless of query casing. */
|
|
412
|
-
readonly caseInsensitive?: boolean | undefined;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
/**
|
|
416
|
-
* Rank `items` by fuzzy-matching `key(item)` against `query`.
|
|
417
|
-
*
|
|
418
|
-
* - Non-matches are filtered out.
|
|
419
|
-
* - Sorted by `score` descending.
|
|
420
|
-
* - Stable for equal scores (input order preserved).
|
|
421
|
-
* - Pure: neither `items` nor its elements are mutated.
|
|
422
|
-
* - Empty query → every item with `score 0` / empty `indices`, input order.
|
|
423
|
-
*/
|
|
424
|
-
export function rankByFuzzy<T>(
|
|
425
|
-
items: readonly T[],
|
|
426
|
-
query: string,
|
|
427
|
-
key: (item: T) => string,
|
|
428
|
-
options: RankByFuzzyOptions = {},
|
|
429
|
-
): ReadonlyArray<RankedFuzzyResult<T>> {
|
|
430
|
-
const forceCaseInsensitive = options.caseInsensitive === true;
|
|
431
|
-
|
|
432
|
-
const ranked: Array<RankedFuzzyResult<T> & { readonly order: number }> = [];
|
|
433
|
-
items.forEach((item, order) => {
|
|
434
|
-
const match = matchIndices(key(item), query, { forceCaseInsensitive });
|
|
435
|
-
if (match !== null) {
|
|
436
|
-
ranked.push({ item, score: match.score, indices: match.indices, order });
|
|
437
|
-
}
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
// Stable sort: score desc, original order asc for ties.
|
|
441
|
-
ranked.sort((a, b) => b.score - a.score || a.order - b.order);
|
|
442
|
-
|
|
443
|
-
return ranked.map(({ item, score, indices }) => ({ item, score, indices }));
|
|
444
|
-
}
|