@untangle-code/search 0.0.0-stage → 0.1.1
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 +661 -0
- package/NOTICE +90 -0
- package/README.md +433 -2
- package/THIRD_PARTY_LICENSES.md +29 -0
- package/dist/benchmark-integrity.d.ts +12 -0
- package/dist/benchmark-integrity.d.ts.map +1 -0
- package/dist/benchmark-integrity.js +10 -0
- package/dist/benchmark-integrity.js.map +1 -0
- package/dist/bm25.d.ts +46 -0
- package/dist/bm25.d.ts.map +1 -0
- package/dist/bm25.js +105 -0
- package/dist/bm25.js.map +1 -0
- package/dist/deadline.d.ts +4 -0
- package/dist/deadline.d.ts.map +1 -0
- package/dist/deadline.js +54 -0
- package/dist/deadline.js.map +1 -0
- package/dist/dedupe.d.ts +48 -0
- package/dist/dedupe.d.ts.map +1 -0
- package/dist/dedupe.js +58 -0
- package/dist/dedupe.js.map +1 -0
- package/dist/diversify.d.ts +45 -0
- package/dist/diversify.d.ts.map +1 -0
- package/dist/diversify.js +30 -0
- package/dist/diversify.js.map +1 -0
- package/dist/engine.d.ts +157 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +1498 -0
- package/dist/engine.js.map +1 -0
- package/dist/enrichment.d.ts +16 -0
- package/dist/enrichment.d.ts.map +1 -0
- package/dist/enrichment.js +92 -0
- package/dist/enrichment.js.map +1 -0
- package/dist/filter.d.ts +86 -0
- package/dist/filter.d.ts.map +1 -0
- package/dist/filter.js +166 -0
- package/dist/filter.js.map +1 -0
- package/dist/findability.d.ts +144 -0
- package/dist/findability.d.ts.map +1 -0
- package/dist/findability.js +393 -0
- package/dist/findability.js.map +1 -0
- package/dist/format.d.ts +28 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +172 -0
- package/dist/format.js.map +1 -0
- package/dist/fusion.d.ts +47 -0
- package/dist/fusion.d.ts.map +1 -0
- package/dist/fusion.js +84 -0
- package/dist/fusion.js.map +1 -0
- package/dist/glob.d.ts +50 -0
- package/dist/glob.d.ts.map +1 -0
- package/dist/glob.js +144 -0
- package/dist/glob.js.map +1 -0
- package/dist/grep.d.ts +76 -0
- package/dist/grep.d.ts.map +1 -0
- package/dist/grep.js +475 -0
- package/dist/grep.js.map +1 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +63 -0
- package/dist/index.js.map +1 -0
- package/dist/ledger.d.ts +45 -0
- package/dist/ledger.d.ts.map +1 -0
- package/dist/ledger.js +39 -0
- package/dist/ledger.js.map +1 -0
- package/dist/payload.d.ts +108 -0
- package/dist/payload.d.ts.map +1 -0
- package/dist/payload.js +263 -0
- package/dist/payload.js.map +1 -0
- package/dist/port.d.ts +112 -0
- package/dist/port.d.ts.map +1 -0
- package/dist/port.js +2 -0
- package/dist/port.js.map +1 -0
- package/dist/product-complexity.d.ts +89 -0
- package/dist/product-complexity.d.ts.map +1 -0
- package/dist/product-complexity.js +1367 -0
- package/dist/product-complexity.js.map +1 -0
- package/dist/regex-safety.d.ts +75 -0
- package/dist/regex-safety.d.ts.map +1 -0
- package/dist/regex-safety.js +902 -0
- package/dist/regex-safety.js.map +1 -0
- package/dist/relevance-prerequisites.d.ts +21 -0
- package/dist/relevance-prerequisites.d.ts.map +1 -0
- package/dist/relevance-prerequisites.js +47 -0
- package/dist/relevance-prerequisites.js.map +1 -0
- package/dist/relevance.d.ts +107 -0
- package/dist/relevance.d.ts.map +1 -0
- package/dist/relevance.js +199 -0
- package/dist/relevance.js.map +1 -0
- package/dist/retrieval.d.ts +46 -0
- package/dist/retrieval.d.ts.map +1 -0
- package/dist/retrieval.js +182 -0
- package/dist/retrieval.js.map +1 -0
- package/dist/scoring.d.ts +102 -0
- package/dist/scoring.d.ts.map +1 -0
- package/dist/scoring.js +175 -0
- package/dist/scoring.js.map +1 -0
- package/dist/sort.d.ts +21 -0
- package/dist/sort.d.ts.map +1 -0
- package/dist/sort.js +53 -0
- package/dist/sort.js.map +1 -0
- package/dist/symbol-match.d.ts +21 -0
- package/dist/symbol-match.d.ts.map +1 -0
- package/dist/symbol-match.js +72 -0
- package/dist/symbol-match.js.map +1 -0
- package/dist/tokenize.d.ts +41 -0
- package/dist/tokenize.d.ts.map +1 -0
- package/dist/tokenize.js +202 -0
- package/dist/tokenize.js.map +1 -0
- package/dist/types.d.ts +173 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +42 -0
- package/dist/types.js.map +1 -0
- package/package.json +61 -3
- package/src/benchmark-integrity.ts +25 -0
- package/src/bm25.ts +135 -0
- package/src/deadline.ts +59 -0
- package/src/dedupe.ts +99 -0
- package/src/diversify.ts +71 -0
- package/src/engine.ts +1957 -0
- package/src/enrichment.ts +115 -0
- package/src/filter.ts +267 -0
- package/src/findability.ts +580 -0
- package/src/format.ts +228 -0
- package/src/fusion.ts +121 -0
- package/src/glob.ts +151 -0
- package/src/grep.ts +618 -0
- package/src/index.ts +83 -0
- package/src/ledger.ts +72 -0
- package/src/payload.ts +382 -0
- package/src/port.ts +133 -0
- package/src/product-complexity.ts +2168 -0
- package/src/regex-safety.ts +1002 -0
- package/src/relevance-prerequisites.ts +83 -0
- package/src/relevance.ts +328 -0
- package/src/retrieval.ts +299 -0
- package/src/scoring.ts +321 -0
- package/src/sort.ts +86 -0
- package/src/symbol-match.ts +87 -0
- package/src/tokenize.ts +213 -0
- package/src/types.ts +206 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SearchGraphOmissionReason,
|
|
3
|
+
SearchGraphOmissionV1,
|
|
4
|
+
SearchGraphRelationsV1,
|
|
5
|
+
SearchGraphRelationV1,
|
|
6
|
+
} from "@untangle-code/domain";
|
|
7
|
+
import { compareCodepoints } from "@untangle-code/domain";
|
|
8
|
+
|
|
9
|
+
/** Harte Obergrenze je Treffer UND Graphart. */
|
|
10
|
+
export const SEARCH_GRAPH_RELATION_LIMIT = 32;
|
|
11
|
+
/** Maximale Textlaenge eines einzelnen Auslassungsdetails. */
|
|
12
|
+
export const SEARCH_GRAPH_OMISSION_DETAIL_LIMIT = 512;
|
|
13
|
+
|
|
14
|
+
/** Begrenzt freie Providerdiagnostik mit einem sichtbaren Restzaehler. */
|
|
15
|
+
export function boundSearchGraphDetail(detail: string): string {
|
|
16
|
+
if (detail.length <= SEARCH_GRAPH_OMISSION_DETAIL_LIMIT) return detail;
|
|
17
|
+
const previewLength = 440;
|
|
18
|
+
const suffix = `… [${String(detail.length - previewLength)} further character(s) omitted]`;
|
|
19
|
+
return `${detail.slice(0, previewLength)}${suffix}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function compareRelation(left: SearchGraphRelationV1, right: SearchGraphRelationV1): number {
|
|
23
|
+
const fields: Array<[string, string]> = [
|
|
24
|
+
[left.source, right.source],
|
|
25
|
+
[left.direction, right.direction],
|
|
26
|
+
[left.repoId, right.repoId],
|
|
27
|
+
[left.refId, right.refId],
|
|
28
|
+
[left.path, right.path],
|
|
29
|
+
[left.fileId, right.fileId],
|
|
30
|
+
[left.edgeKind, right.edgeKind],
|
|
31
|
+
[left.edgeId, right.edgeId],
|
|
32
|
+
[left.symbol?.id ?? "", right.symbol?.id ?? ""],
|
|
33
|
+
];
|
|
34
|
+
for (const [leftValue, rightValue] of fields) {
|
|
35
|
+
const compared = compareCodepoints(leftValue, rightValue);
|
|
36
|
+
if (compared !== 0) return compared;
|
|
37
|
+
}
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizeOmissions(
|
|
42
|
+
input: readonly SearchGraphOmissionV1[],
|
|
43
|
+
truncatedRelationCount: number,
|
|
44
|
+
): SearchGraphOmissionV1[] {
|
|
45
|
+
const counts = new Map<SearchGraphOmissionReason, { count: number; details: Set<string> }>();
|
|
46
|
+
const add = (omission: SearchGraphOmissionV1): void => {
|
|
47
|
+
if (!Number.isInteger(omission.count) || omission.count <= 0 || omission.detail.trim() === "") {
|
|
48
|
+
throw new TypeError(
|
|
49
|
+
"search graph omission requires a positive integer count and non-empty detail",
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
const previous = counts.get(omission.reason) ?? { count: 0, details: new Set<string>() };
|
|
53
|
+
previous.count += omission.count;
|
|
54
|
+
previous.details.add(omission.detail);
|
|
55
|
+
counts.set(omission.reason, previous);
|
|
56
|
+
};
|
|
57
|
+
for (const omission of input) add(omission);
|
|
58
|
+
if (truncatedRelationCount > 0) {
|
|
59
|
+
add({
|
|
60
|
+
reason: "relation_limit",
|
|
61
|
+
count: truncatedRelationCount,
|
|
62
|
+
detail: `${String(truncatedRelationCount)} direct persisted relation(s) were omitted by the per-hit limit`,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return [...counts.entries()]
|
|
66
|
+
.sort(([left], [right]) => compareCodepoints(left, right))
|
|
67
|
+
.map(([reason, aggregate]) => {
|
|
68
|
+
const details = [...aggregate.details].sort(compareCodepoints);
|
|
69
|
+
const first = details[0] as string;
|
|
70
|
+
return {
|
|
71
|
+
reason,
|
|
72
|
+
count: aggregate.count,
|
|
73
|
+
detail: boundSearchGraphDetail(
|
|
74
|
+
details.length === 1
|
|
75
|
+
? first
|
|
76
|
+
: `${first} (+${String(details.length - 1)} further distinct detail(s), aggregated deterministically)`,
|
|
77
|
+
),
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Macht ein Provider-Ergebnis deterministisch und erzwingt die Grenze erneut
|
|
84
|
+
* an der Search-Paketgrenze. Ein Adapter darf also nicht versehentlich eine
|
|
85
|
+
* unbeschraenkte Nachbarschaft an einen Treffer haengen.
|
|
86
|
+
*/
|
|
87
|
+
export function createSearchGraphRelations(
|
|
88
|
+
relations: readonly SearchGraphRelationV1[],
|
|
89
|
+
omissions: readonly SearchGraphOmissionV1[] = [],
|
|
90
|
+
relationLimit = SEARCH_GRAPH_RELATION_LIMIT,
|
|
91
|
+
): SearchGraphRelationsV1 {
|
|
92
|
+
if (!Number.isInteger(relationLimit) || relationLimit <= 0) {
|
|
93
|
+
throw new TypeError("search graph relationLimit must be a positive integer");
|
|
94
|
+
}
|
|
95
|
+
const sorted = [...relations].sort(compareRelation);
|
|
96
|
+
const kept = sorted.slice(0, relationLimit);
|
|
97
|
+
const normalizedOmissions = normalizeOmissions(omissions, sorted.length - kept.length);
|
|
98
|
+
return {
|
|
99
|
+
schemaVersion: 1,
|
|
100
|
+
relations: kept,
|
|
101
|
+
relationLimit,
|
|
102
|
+
truncated: normalizedOmissions.some((omission) => omission.reason === "relation_limit"),
|
|
103
|
+
omittedCount: normalizedOmissions.reduce((sum, omission) => sum + omission.count, 0),
|
|
104
|
+
omissions: normalizedOmissions,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Eine leere, aber erklaerte Facette fuer einen sichtbaren Degradationsfall. */
|
|
109
|
+
export function unavailableSearchGraphRelations(
|
|
110
|
+
reason: SearchGraphOmissionReason,
|
|
111
|
+
detail: string,
|
|
112
|
+
relationLimit = SEARCH_GRAPH_RELATION_LIMIT,
|
|
113
|
+
): SearchGraphRelationsV1 {
|
|
114
|
+
return createSearchGraphRelations([], [{ reason, count: 1, detail }], relationLimit);
|
|
115
|
+
}
|
package/src/filter.ts
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Abbildung der normalisierten Suchfilter auf einen `PayloadFilter`
|
|
3
|
+
* (spec.md §13.2 Schritt 7: „Payloadfilter direkt in Qdrant anwenden").
|
|
4
|
+
*
|
|
5
|
+
* Zentrale Entscheidung dieses Moduls: Es sagt ausdruecklich, WELCHE Filter
|
|
6
|
+
* es NICHT in den Speicher schieben konnte. Ein Filter, den der Speicher
|
|
7
|
+
* nicht kennt (Pfad-Globs, Findability), muss im Prozess nachgezogen werden —
|
|
8
|
+
* und wenn das niemand tut, faellt es hier auf, statt dass die Suche
|
|
9
|
+
* stillschweigend zu viele Treffer liefert.
|
|
10
|
+
*/
|
|
11
|
+
import type { FieldCondition, PayloadFilter, SearchFiltersV1 } from "@untangle-code/domain";
|
|
12
|
+
import { sortByCodepoints } from "@untangle-code/domain";
|
|
13
|
+
|
|
14
|
+
/** Ein Filter, der nicht in den Speicher geschoben werden konnte. */
|
|
15
|
+
export interface ResidualFilter {
|
|
16
|
+
/** Name des Filters, z. B. "pathGlobs". */
|
|
17
|
+
name: string;
|
|
18
|
+
/** Warum er im Speicher nicht ausdrueckbar ist. */
|
|
19
|
+
reason: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Der GELTUNGSBEREICH einer Suche — Workspace und die Repositories, die das
|
|
24
|
+
* angefragte Ziel ueberhaupt enthaelt.
|
|
25
|
+
*
|
|
26
|
+
* Er steht bewusst NICHT in `SearchFiltersV1`: `workspaceId` ist kein Filter,
|
|
27
|
+
* den ein Nutzer setzt oder weglaesst, sondern die Grenze, innerhalb derer die
|
|
28
|
+
* Anfrage ueberhaupt gilt (spec.md §10.4 fuehrt `workspace_id` als
|
|
29
|
+
* Pflicht-Keyword-Index genau dafuer). Weil er ein PFLICHTPARAMETER dieser
|
|
30
|
+
* Funktion ist, kann ihn kein Aufrufer vergessen — das ist der eigentliche
|
|
31
|
+
* Grund fuer diese Form.
|
|
32
|
+
*/
|
|
33
|
+
export interface SearchScopeV1 {
|
|
34
|
+
/** Workspace der normalisierten Anfrage; darf nicht leer sein. */
|
|
35
|
+
workspaceId: string;
|
|
36
|
+
/**
|
|
37
|
+
* Repositories, die das Ziel (die Collection) enthaelt
|
|
38
|
+
* (`ProfileTargetV1.repoIds`). Leer heisst „nicht deklariert" und schraenkt
|
|
39
|
+
* dann nichts ein — wie eine leere Filterliste in `SearchFiltersV1`.
|
|
40
|
+
*/
|
|
41
|
+
targetRepoIds?: readonly string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface BuiltPayloadFilter {
|
|
45
|
+
filter: PayloadFilter;
|
|
46
|
+
/** Filter, die der Aufrufer NACH dem Abruf selbst anwenden muss. */
|
|
47
|
+
residual: ResidualFilter[];
|
|
48
|
+
/**
|
|
49
|
+
* Die Repositories, aus denen dieses Ziel Treffer liefern KANN: der
|
|
50
|
+
* Durchschnitt aus Anfragefilter und Ziel-Repositories. Leer heisst „nicht
|
|
51
|
+
* eingeschraenkt". Der Aufrufer meldet damit die Herkunft der Ergebnisgruppe,
|
|
52
|
+
* statt die deklarierten Ziel-Repositories ungeprueft zu wiederholen.
|
|
53
|
+
*/
|
|
54
|
+
scopeRepoIds: string[];
|
|
55
|
+
/**
|
|
56
|
+
* Gesetzt, wenn der Geltungsbereich LEER ist (die angefragten Repositories
|
|
57
|
+
* liegen saemtlich ausserhalb des Ziels). Dann darf der Aufrufer den Speicher
|
|
58
|
+
* NICHT fragen: eine leere `in`-Liste waere kein Filter, sondern gar keiner —
|
|
59
|
+
* die Suche liefe ueber die ganze Collection. Der Text ist die Begruendung
|
|
60
|
+
* fuer den Skip-Ledger.
|
|
61
|
+
*/
|
|
62
|
+
emptyScopeReason?: string;
|
|
63
|
+
/**
|
|
64
|
+
* WP-176: die ANGEFRAGTEN Repositories, die dieses Ziel nicht enthaelt und
|
|
65
|
+
* die deshalb hier nicht durchsucht wurden. Leer heisst „alles Angefragte
|
|
66
|
+
* lag im Ziel". Der Aufrufer MUSS sie melden — sie sind der Unterschied
|
|
67
|
+
* zwischen „in a und b gesucht" und „nur in a gesucht".
|
|
68
|
+
*/
|
|
69
|
+
outOfScopeRepoIds: string[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Durchschnitt zweier Repo-Listen; leere Liste heisst „keine Einschraenkung".
|
|
74
|
+
*
|
|
75
|
+
* WP-176: `dropped` nennt die ANGEFRAGTEN Repositories, die das Ziel gar nicht
|
|
76
|
+
* enthaelt. Frueher fiel diese Menge lautlos aus dem Durchschnitt heraus und
|
|
77
|
+
* nur der VOLLSTAENDIG leere Fall kam in den Ledger — wer `--repo=a,b` fragte
|
|
78
|
+
* und ein Ziel nur `a` fuehrt, bekam Treffer aus `a` und kein Wort darueber,
|
|
79
|
+
* dass `b` nie durchsucht wurde. Das ist derselbe Fehler wie ein leeres
|
|
80
|
+
* Ergebnis ohne Grund, nur schwerer zu bemerken: er sieht wie ein Erfolg aus.
|
|
81
|
+
*/
|
|
82
|
+
function intersectRepoIds(
|
|
83
|
+
requested: readonly string[],
|
|
84
|
+
target: readonly string[],
|
|
85
|
+
): { repoIds: string[]; dropped: string[]; empty: boolean } {
|
|
86
|
+
if (requested.length === 0) {
|
|
87
|
+
return { repoIds: sortByCodepoints([...target]), dropped: [], empty: false };
|
|
88
|
+
}
|
|
89
|
+
if (target.length === 0) {
|
|
90
|
+
return { repoIds: sortByCodepoints([...requested]), dropped: [], empty: false };
|
|
91
|
+
}
|
|
92
|
+
const targetSet = new Set(target);
|
|
93
|
+
const shared = requested.filter((repoId) => targetSet.has(repoId));
|
|
94
|
+
const dropped = requested.filter((repoId) => !targetSet.has(repoId));
|
|
95
|
+
return {
|
|
96
|
+
repoIds: sortByCodepoints(shared),
|
|
97
|
+
dropped: sortByCodepoints(dropped),
|
|
98
|
+
empty: shared.length === 0,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function keywordIn(field: string, values: readonly string[]): FieldCondition | undefined {
|
|
103
|
+
if (values.length === 0) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
if (values.length === 1) {
|
|
107
|
+
return { field, match: { eq: values[0] as string } };
|
|
108
|
+
}
|
|
109
|
+
return { field, match: { in: [...values] } };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function range(
|
|
113
|
+
field: string,
|
|
114
|
+
min: number | undefined,
|
|
115
|
+
max: number | undefined,
|
|
116
|
+
): FieldCondition | undefined {
|
|
117
|
+
if (min === undefined && max === undefined) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
field,
|
|
122
|
+
range: {
|
|
123
|
+
...(min === undefined ? {} : { gte: min }),
|
|
124
|
+
...(max === undefined ? {} : { lte: max }),
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Baut den Speicherfilter aus dem Geltungsbereich und den normalisierten Filtern.
|
|
131
|
+
*
|
|
132
|
+
* `point_type = "code_chunk"` steht IMMER drin: eine Code-Collection kann
|
|
133
|
+
* neben Chunks auch andere Punktarten enthalten, und ein Treffer, der kein
|
|
134
|
+
* Chunk ist, hat in einer Trefferliste nichts verloren (spec.md §10.3/§10.4).
|
|
135
|
+
*
|
|
136
|
+
* `workspace_id` steht EBENSO IMMER drin. Eine Collection kann Chunks mehrerer
|
|
137
|
+
* Workspaces tragen — der Dedupe-Schluessel dieses Pakets rechnet selbst damit —
|
|
138
|
+
* und ein Treffer aus einem fremden Workspace waere kein ungenauer Treffer,
|
|
139
|
+
* sondern ein fremder Inhalt, ausgegeben als eigener. Deshalb ist der
|
|
140
|
+
* Geltungsbereich ein Pflichtparameter und keine Option.
|
|
141
|
+
*/
|
|
142
|
+
export function buildPayloadFilter(
|
|
143
|
+
filters: SearchFiltersV1,
|
|
144
|
+
scope: SearchScopeV1,
|
|
145
|
+
): BuiltPayloadFilter {
|
|
146
|
+
const workspaceId = scope.workspaceId.trim();
|
|
147
|
+
if (workspaceId === "") {
|
|
148
|
+
throw new Error(
|
|
149
|
+
"buildPayloadFilter requires a non-empty workspaceId: without it the store filter would span every workspace in the collection",
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
const must: FieldCondition[] = [
|
|
153
|
+
{ field: "point_type", match: { eq: "code_chunk" } },
|
|
154
|
+
{ field: "workspace_id", match: { eq: workspaceId } },
|
|
155
|
+
];
|
|
156
|
+
const residual: ResidualFilter[] = [];
|
|
157
|
+
|
|
158
|
+
const scoped = intersectRepoIds(filters.repoIds, scope.targetRepoIds ?? []);
|
|
159
|
+
if (scoped.empty) {
|
|
160
|
+
return {
|
|
161
|
+
filter: { must },
|
|
162
|
+
residual,
|
|
163
|
+
scopeRepoIds: [],
|
|
164
|
+
outOfScopeRepoIds: scoped.dropped,
|
|
165
|
+
emptyScopeReason: `the requested repositories [${filters.repoIds.join(", ")}] are not part of this target's repositories [${sortByCodepoints([...(scope.targetRepoIds ?? [])]).join(", ")}]; this target was not searched`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const conditions = [
|
|
170
|
+
keywordIn("repo_id", scoped.repoIds),
|
|
171
|
+
keywordIn("ref_id", filters.refIds),
|
|
172
|
+
keywordIn("language", filters.languages),
|
|
173
|
+
keywordIn("symbol_name", filters.symbolNames),
|
|
174
|
+
keywordIn("symbol_kind", filters.symbolKinds),
|
|
175
|
+
keywordIn("chunk_kind", filters.chunkKinds),
|
|
176
|
+
filters.commitSha === undefined
|
|
177
|
+
? undefined
|
|
178
|
+
: ({ field: "commit_sha", match: { eq: filters.commitSha } } satisfies FieldCondition),
|
|
179
|
+
];
|
|
180
|
+
for (const condition of conditions) {
|
|
181
|
+
if (condition !== undefined) {
|
|
182
|
+
must.push(condition);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const complexity = filters.complexity;
|
|
187
|
+
const complexityConditions =
|
|
188
|
+
complexity === undefined
|
|
189
|
+
? []
|
|
190
|
+
: [
|
|
191
|
+
range("cyclomatic_extended", complexity.cyclomaticMin, complexity.cyclomaticMax),
|
|
192
|
+
range("cognitive", complexity.cognitiveMin, complexity.cognitiveMax),
|
|
193
|
+
range("risk_score", complexity.riskMin, complexity.riskMax),
|
|
194
|
+
];
|
|
195
|
+
for (const condition of complexityConditions) {
|
|
196
|
+
if (condition !== undefined) {
|
|
197
|
+
must.push(condition);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (filters.changed !== undefined) {
|
|
202
|
+
const since = Date.parse(filters.changed.since);
|
|
203
|
+
const until =
|
|
204
|
+
filters.changed.until === undefined ? undefined : Date.parse(filters.changed.until);
|
|
205
|
+
// Ein unlesbarer Zeitstempel wuerde als NaN in die Bereichsbedingung
|
|
206
|
+
// wandern; je nach Speicher filtert das dann alles oder nichts, in beiden
|
|
207
|
+
// Faellen ohne Hinweis. Lieber ablehnen (E4).
|
|
208
|
+
if (Number.isNaN(since)) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
`search query filter "changed.since" is not a valid ISO-8601 timestamp: "${filters.changed.since}"`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
if (until !== undefined && Number.isNaN(until)) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
`search query filter "changed.until" is not a valid ISO-8601 timestamp: "${String(filters.changed.until)}"`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
// `changed_at_epoch_ms` ist der numerische Index aus spec.md §10.4. Punkte
|
|
219
|
+
// ohne dieses Feld erfuellen eine Bereichsbedingung nicht und fallen damit
|
|
220
|
+
// heraus — das ist gewollt und in der README vermerkt: „seit X geaendert"
|
|
221
|
+
// ist ohne Aenderungsdatum nicht beantwortbar, und ein stilles Durchwinken
|
|
222
|
+
// waere eine falsche Antwort.
|
|
223
|
+
must.push({
|
|
224
|
+
field: "changed_at_epoch_ms",
|
|
225
|
+
range: {
|
|
226
|
+
gte: since,
|
|
227
|
+
...(until === undefined ? {} : { lt: until }),
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (filters.pathGlobs.length > 0) {
|
|
233
|
+
residual.push({
|
|
234
|
+
name: "pathGlobs",
|
|
235
|
+
reason:
|
|
236
|
+
"the vector store's payload filter matches exact values and ranges only, not glob patterns",
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
if (filters.findability !== undefined) {
|
|
240
|
+
residual.push({
|
|
241
|
+
name: "findability",
|
|
242
|
+
reason: "findability is derived per symbol and is not part of the stored chunk payload",
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
filter: { must },
|
|
248
|
+
residual,
|
|
249
|
+
scopeRepoIds: scoped.repoIds,
|
|
250
|
+
outOfScopeRepoIds: scoped.dropped,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Zusaetzlicher Filter fuer den Zweig der exakten Symbolnamen: nur Punkte,
|
|
256
|
+
* deren `symbol_name` einem der Kandidaten entspricht.
|
|
257
|
+
*/
|
|
258
|
+
export function buildSymbolNameFilter(
|
|
259
|
+
base: PayloadFilter,
|
|
260
|
+
symbolNames: readonly string[],
|
|
261
|
+
): PayloadFilter | undefined {
|
|
262
|
+
const condition = keywordIn("symbol_name", symbolNames);
|
|
263
|
+
if (condition === undefined) {
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
return { ...base, must: [...(base.must ?? []), condition] };
|
|
267
|
+
}
|