sift-light 1.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 +0 -0
- package/CONTRIBUTING.md +19 -0
- package/LICENSE +661 -0
- package/README.md +177 -0
- package/README.zh-CN.md +177 -0
- package/SECURITY.md +25 -0
- package/package.json +142 -0
- package/plugins/sift-light/.claude-plugin/plugin.json +10 -0
- package/plugins/sift-light/.codex-plugin/plugin.json +20 -0
- package/plugins/sift-light/.mcp.json +24 -0
- package/plugins/sift-light/LICENSE +661 -0
- package/plugins/sift-light/concept-worker.mjs +567 -0
- package/plugins/sift-light/hooks/THIRD_PARTY_NOTICES.txt +70 -0
- package/plugins/sift-light/hooks/hooks.json +16 -0
- package/plugins/sift-light/hooks/search-policy.mjs +4113 -0
- package/plugins/sift-light/hooks/tree-sitter-bash.wasm +0 -0
- package/plugins/sift-light/hooks/tree-sitter-powershell.wasm +0 -0
- package/plugins/sift-light/hooks/tree-sitter.wasm +0 -0
- package/plugins/sift-light/kimi.plugin.json +36 -0
- package/plugins/sift-light/mcp-server.mjs +1 -0
- package/plugins/sift-light/omp-extension.mjs +20914 -0
- package/plugins/sift-light/package.json +11 -0
- package/plugins/sift-light/syntax-worker.mjs +280 -0
- package/plugins/sift-light/syntax-worker.toml +2 -0
- package/src/analysis-evidence.ts +43 -0
- package/src/analysis-limits.ts +27 -0
- package/src/analysis-store.ts +559 -0
- package/src/analysis-term-pages.ts +73 -0
- package/src/analysis-types.ts +162 -0
- package/src/audit-search.ts +391 -0
- package/src/capped-lines.ts +112 -0
- package/src/concept-embedding-cache.ts +186 -0
- package/src/concept-inference.ts +218 -0
- package/src/concept-model.ts +96 -0
- package/src/concept-search.ts +577 -0
- package/src/concept-setup.ts +75 -0
- package/src/concept-source-generation.ts +268 -0
- package/src/concept-worker.mjs +567 -0
- package/src/concept-worker.ts +210 -0
- package/src/config-reader.ts +283 -0
- package/src/config.ts +37 -0
- package/src/context-budget.ts +26 -0
- package/src/discovery-errors.ts +2 -0
- package/src/errors.ts +95 -0
- package/src/evidence-candidates.ts +344 -0
- package/src/evidence-ranking.ts +48 -0
- package/src/evidence-service.ts +1047 -0
- package/src/evidence-validation.ts +466 -0
- package/src/evidence-validity.ts +33 -0
- package/src/excerpt.ts +53 -0
- package/src/file-discovery.ts +150 -0
- package/src/file-metadata-filter.ts +46 -0
- package/src/format.ts +407 -0
- package/src/git-diff.ts +195 -0
- package/src/git-process.ts +189 -0
- package/src/git-repository.ts +367 -0
- package/src/git-source.ts +385 -0
- package/src/historical-paths.ts +205 -0
- package/src/hybrid-search.ts +340 -0
- package/src/import-model.ts +611 -0
- package/src/import-navigation.ts +100 -0
- package/src/import-resolution.ts +240 -0
- package/src/index.ts +97 -0
- package/src/inspect-batch.ts +265 -0
- package/src/inspect.ts +271 -0
- package/src/language-capabilities.ts +157 -0
- package/src/language-capability-definitions.ts +204 -0
- package/src/literal-search.ts +25 -0
- package/src/mcp-cli.ts +21 -0
- package/src/mcp-model-output.ts +120 -0
- package/src/mcp-output.ts +9 -0
- package/src/mcp-semantic-judge.ts +39 -0
- package/src/mcp-server.mjs +14064 -0
- package/src/mcp-server.ts +157 -0
- package/src/mcp-stdio.ts +114 -0
- package/src/mcp.ts +631 -0
- package/src/model-error.ts +120 -0
- package/src/multi-term-search.ts +122 -0
- package/src/omp-index.ts +277 -0
- package/src/omp-marketplace-entry.ts +7 -0
- package/src/operation-lifecycle.ts +413 -0
- package/src/operation-output.ts +100 -0
- package/src/owned-parallel.ts +22 -0
- package/src/owned-process.ts +102 -0
- package/src/owned-task-queue.ts +38 -0
- package/src/package-version.ts +3 -0
- package/src/path-policy.ts +203 -0
- package/src/pi-search-policy.ts +33 -0
- package/src/prompt-guidelines.ts +54 -0
- package/src/python-outline.ts +231 -0
- package/src/record-value.ts +3 -0
- package/src/redaction.ts +95 -0
- package/src/request-contract-catalog.ts +215 -0
- package/src/request-contract-error.ts +96 -0
- package/src/request-contract.ts +481 -0
- package/src/request.ts +132 -0
- package/src/result-statistics.ts +178 -0
- package/src/rg.ts +574 -0
- package/src/ripgrep-diagnostics.ts +122 -0
- package/src/ripgrep-executable.ts +43 -0
- package/src/runtime.ts +53 -0
- package/src/scan-revisions.ts +148 -0
- package/src/script-runtime.ts +11 -0
- package/src/search-policy-commands.ts +170 -0
- package/src/search-policy-hook.ts +61 -0
- package/src/search-policy-recovery.ts +388 -0
- package/src/search-policy-shell.ts +280 -0
- package/src/search-policy.ts +102 -0
- package/src/search-retention.ts +77 -0
- package/src/semantic-judge-batches.ts +135 -0
- package/src/semantic-judge.ts +519 -0
- package/src/service.ts +945 -0
- package/src/session-summary.ts +100 -0
- package/src/snapshot-store.ts +203 -0
- package/src/source-access.ts +288 -0
- package/src/source-continuations.ts +168 -0
- package/src/source-document.ts +277 -0
- package/src/source-inspection.ts +731 -0
- package/src/source-pages.ts +112 -0
- package/src/source.ts +286 -0
- package/src/structural-search.ts +125 -0
- package/src/structure.ts +259 -0
- package/src/summary-previews.ts +96 -0
- package/src/summary.ts +79 -0
- package/src/syntax-facts.ts +464 -0
- package/src/syntax-fields.ts +161 -0
- package/src/syntax-search.ts +361 -0
- package/src/syntax-tree.ts +30 -0
- package/src/syntax-types.ts +72 -0
- package/src/syntax-worker.mjs +280 -0
- package/src/syntax-worker.toml +2 -0
- package/src/syntax-worker.ts +143 -0
- package/src/syntax.ts +259 -0
- package/src/test-navigation-facts.ts +261 -0
- package/src/test-navigation.ts +543 -0
- package/src/tool-schema.ts +321 -0
- package/src/tui/dashboard.ts +107 -0
- package/src/tui/layout.ts +291 -0
- package/src/tui/palette.ts +73 -0
- package/src/tui/presentation.ts +240 -0
- package/src/tui/renderers.ts +106 -0
- package/src/types.ts +368 -0
- package/src/validation-output.ts +148 -0
- package/src/validation-types.ts +48 -0
- package/src/workspace-files.ts +142 -0
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
ANALYSIS_TTL_MS,
|
|
4
|
+
ANALYSIS_METADATA_RESERVE_BYTES,
|
|
5
|
+
MAX_ANALYSIS_REASON_BYTES,
|
|
6
|
+
MAX_ANALYSIS_REASONS,
|
|
7
|
+
MAX_ANALYSIS_RESULTS,
|
|
8
|
+
MAX_ANALYSIS_SNAPSHOTS,
|
|
9
|
+
MAX_ANALYSIS_STORAGE_BYTES,
|
|
10
|
+
} from "./analysis-limits.js";
|
|
11
|
+
import {
|
|
12
|
+
SEMANTIC_JUDGE_CLASSIFICATIONS,
|
|
13
|
+
SEMANTIC_JUDGE_NON_PROOF_CLAIM,
|
|
14
|
+
type AnalysisItem,
|
|
15
|
+
type AnalysisResultSet,
|
|
16
|
+
} from "./analysis-types.js";
|
|
17
|
+
import { CursorError, SiftLightError } from "./errors.js";
|
|
18
|
+
import type { SiftLightResult } from "./types.js";
|
|
19
|
+
import { MAX_RESULT_BYTES } from "./types.js";
|
|
20
|
+
import { modificationTimeBoundsText } from "./source.js";
|
|
21
|
+
import {
|
|
22
|
+
analysisTermPage,
|
|
23
|
+
MAX_INLINE_TERM_COUNT_BYTES,
|
|
24
|
+
termCountRequest,
|
|
25
|
+
} from "./analysis-term-pages.js";
|
|
26
|
+
|
|
27
|
+
import { analysisExtraGroups, statisticsForItems } from "./result-statistics.js";
|
|
28
|
+
interface StoredAnalysis {
|
|
29
|
+
id: string;
|
|
30
|
+
result: AnalysisResultSet;
|
|
31
|
+
bytes: number;
|
|
32
|
+
touched: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type RetainedAnalysisSummary = (
|
|
36
|
+
items: readonly AnalysisItem[],
|
|
37
|
+
) => Pick<AnalysisResultSet, "counts" | "termCounts">;
|
|
38
|
+
|
|
39
|
+
function boundedReasons(reasons: readonly string[]): string[] {
|
|
40
|
+
const unsupportedSuffix = ": syntax unsupported; this source remains unclassified";
|
|
41
|
+
const unsupported = reasons
|
|
42
|
+
.filter((reason) => reason.endsWith(unsupportedSuffix))
|
|
43
|
+
.map((reason) => reason.slice(0, -unsupportedSuffix.length));
|
|
44
|
+
const unique = [
|
|
45
|
+
...new Set(reasons.filter((reason) => !reason.endsWith(unsupportedSuffix))),
|
|
46
|
+
...(unsupported.length
|
|
47
|
+
? [
|
|
48
|
+
`${String(unsupported.length)} matching file(s) skipped because syntax is unsupported${unsupported.length ? `; examples: ${unsupported.slice(0, 3).join(", ")}` : ""}`,
|
|
49
|
+
]
|
|
50
|
+
: []),
|
|
51
|
+
];
|
|
52
|
+
const retained: string[] = [];
|
|
53
|
+
let bytes = 2;
|
|
54
|
+
let omitted = 0;
|
|
55
|
+
for (const reason of unique) {
|
|
56
|
+
const reasonBytes = Buffer.byteLength(JSON.stringify(reason)) + 1;
|
|
57
|
+
if (
|
|
58
|
+
retained.length >= MAX_ANALYSIS_REASONS ||
|
|
59
|
+
bytes + reasonBytes > MAX_ANALYSIS_REASON_BYTES
|
|
60
|
+
) {
|
|
61
|
+
omitted += 1;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
retained.push(reason);
|
|
65
|
+
bytes += reasonBytes;
|
|
66
|
+
}
|
|
67
|
+
if (omitted === 0) return retained;
|
|
68
|
+
let notice = `${String(omitted)} additional analysis reasons omitted within the ${String(MAX_ANALYSIS_REASONS)}-reason / ${String(MAX_ANALYSIS_REASON_BYTES)}-byte diagnostic limit`;
|
|
69
|
+
while (
|
|
70
|
+
retained.length > 0 &&
|
|
71
|
+
bytes + Buffer.byteLength(JSON.stringify(notice)) + 1 > MAX_ANALYSIS_REASON_BYTES
|
|
72
|
+
) {
|
|
73
|
+
const removed = retained.pop();
|
|
74
|
+
if (removed === undefined) break;
|
|
75
|
+
bytes -= Buffer.byteLength(JSON.stringify(removed)) + 1;
|
|
76
|
+
omitted += 1;
|
|
77
|
+
notice = `${String(omitted)} additional analysis reasons omitted within the ${String(MAX_ANALYSIS_REASONS)}-reason / ${String(MAX_ANALYSIS_REASON_BYTES)}-byte diagnostic limit`;
|
|
78
|
+
}
|
|
79
|
+
retained.push(notice);
|
|
80
|
+
return retained;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function publicAnalysisLabel(result: AnalysisResultSet): string {
|
|
84
|
+
switch (result.kind) {
|
|
85
|
+
case "files":
|
|
86
|
+
return "file metadata";
|
|
87
|
+
case "outline":
|
|
88
|
+
return "symbol metadata";
|
|
89
|
+
case "imports":
|
|
90
|
+
return "import relationship metadata";
|
|
91
|
+
case "tests":
|
|
92
|
+
return "test candidate metadata";
|
|
93
|
+
case "structure":
|
|
94
|
+
return "structure match metadata";
|
|
95
|
+
case "roles":
|
|
96
|
+
return "role match metadata";
|
|
97
|
+
case "function-and":
|
|
98
|
+
return "function match metadata";
|
|
99
|
+
case "file-and":
|
|
100
|
+
return "file match metadata";
|
|
101
|
+
case "changes":
|
|
102
|
+
return "change metadata";
|
|
103
|
+
case "concept":
|
|
104
|
+
return "semantic candidate metadata";
|
|
105
|
+
case "hybrid":
|
|
106
|
+
return "combined evidence metadata";
|
|
107
|
+
case "any-of":
|
|
108
|
+
return "condition match metadata";
|
|
109
|
+
case "validate":
|
|
110
|
+
return "validation metadata";
|
|
111
|
+
}
|
|
112
|
+
throw new Error("Unknown analysis result kind");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function publicStringArray(value: unknown): string[] | undefined {
|
|
116
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) return undefined;
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function publicStructureDetails(item: AnalysisItem): Record<string, unknown> | undefined {
|
|
121
|
+
if (item.details?.kind !== "symbol") return undefined;
|
|
122
|
+
const details = item.details;
|
|
123
|
+
const scope = publicStringArray(details.scope);
|
|
124
|
+
return {
|
|
125
|
+
kind: "symbol",
|
|
126
|
+
...(typeof details.language === "string" ? { language: details.language } : {}),
|
|
127
|
+
...(typeof details.name === "string" ? { name: details.name } : {}),
|
|
128
|
+
...(scope ? { scope } : {}),
|
|
129
|
+
...(typeof details.hasBody === "boolean" ? { hasBody: details.hasBody } : {}),
|
|
130
|
+
...(typeof details.exported === "boolean" ? { exported: details.exported } : {}),
|
|
131
|
+
...(typeof details.syntax === "string" ? { syntax: details.syntax } : {}),
|
|
132
|
+
...(typeof details.signatureTruncated === "boolean"
|
|
133
|
+
? { signatureTruncated: details.signatureTruncated }
|
|
134
|
+
: {}),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function publicSemanticJudgeDetails(item: AnalysisItem): Record<string, unknown> | undefined {
|
|
139
|
+
const value = item.details?.semanticJudge;
|
|
140
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
|
|
141
|
+
const classification = Reflect.get(value, "classification");
|
|
142
|
+
const probability = Reflect.get(value, "probability");
|
|
143
|
+
const confidence = Reflect.get(value, "confidence");
|
|
144
|
+
const model = Reflect.get(value, "model");
|
|
145
|
+
const claim = Reflect.get(value, "claim");
|
|
146
|
+
if (
|
|
147
|
+
typeof classification !== "string" ||
|
|
148
|
+
!SEMANTIC_JUDGE_CLASSIFICATIONS.some((candidate) => candidate === classification) ||
|
|
149
|
+
typeof probability !== "number" ||
|
|
150
|
+
!Number.isFinite(probability) ||
|
|
151
|
+
probability < 0 ||
|
|
152
|
+
probability > 1 ||
|
|
153
|
+
(confidence !== undefined &&
|
|
154
|
+
(typeof confidence !== "number" ||
|
|
155
|
+
!Number.isFinite(confidence) ||
|
|
156
|
+
confidence < 0 ||
|
|
157
|
+
confidence > 1)) ||
|
|
158
|
+
typeof model !== "string" ||
|
|
159
|
+
model.length === 0 ||
|
|
160
|
+
model.length > 128 ||
|
|
161
|
+
/[\r\n\0]/u.test(model) ||
|
|
162
|
+
claim !== SEMANTIC_JUDGE_NON_PROOF_CLAIM
|
|
163
|
+
)
|
|
164
|
+
return undefined;
|
|
165
|
+
return {
|
|
166
|
+
classification,
|
|
167
|
+
probability,
|
|
168
|
+
...(confidence === undefined ? {} : { confidence }),
|
|
169
|
+
model,
|
|
170
|
+
claim,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function publicItemDetails(item: AnalysisItem): Record<string, unknown> | undefined {
|
|
175
|
+
const structure = publicStructureDetails(item);
|
|
176
|
+
const semanticJudge = publicSemanticJudgeDetails(item);
|
|
177
|
+
const source =
|
|
178
|
+
item.details?.source === "literal" || item.details?.source === "concept"
|
|
179
|
+
? item.details.source
|
|
180
|
+
: undefined;
|
|
181
|
+
if (!structure && !semanticJudge && !source) return undefined;
|
|
182
|
+
return {
|
|
183
|
+
...structure,
|
|
184
|
+
...(source ? { source } : {}),
|
|
185
|
+
...(semanticJudge ? { semanticJudge } : {}),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function publicAnalysisItem(
|
|
190
|
+
result: AnalysisResultSet,
|
|
191
|
+
item: AnalysisItem,
|
|
192
|
+
index: number,
|
|
193
|
+
storedId: string,
|
|
194
|
+
modelOutput: boolean,
|
|
195
|
+
): NonNullable<SiftLightResult["details"]["analysis"]>["items"][number] {
|
|
196
|
+
const inspect =
|
|
197
|
+
item.source && item.range
|
|
198
|
+
? {
|
|
199
|
+
mode: "inspect" as const,
|
|
200
|
+
cursor: `${storedId}.analysis.0`,
|
|
201
|
+
matchIndex: index + 1,
|
|
202
|
+
...(result.redact ? { redact: true } : {}),
|
|
203
|
+
}
|
|
204
|
+
: undefined;
|
|
205
|
+
const publicDetails = publicItemDetails(item);
|
|
206
|
+
return {
|
|
207
|
+
path: item.path,
|
|
208
|
+
line: item.line,
|
|
209
|
+
label: result.kind === "outline" && modelOutput ? item.label : publicAnalysisLabel(result),
|
|
210
|
+
index: index + 1,
|
|
211
|
+
...(inspect ? { inspect } : {}),
|
|
212
|
+
...(publicDetails ? { details: publicDetails } : {}),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function safeTermCounts(
|
|
217
|
+
termCounts: readonly { retainedOccurrences: number }[] | undefined,
|
|
218
|
+
): { term: string; retainedOccurrences: number }[] | undefined {
|
|
219
|
+
return termCounts?.map((entry, index) => ({
|
|
220
|
+
term: `condition #${String(index + 1)}`,
|
|
221
|
+
retainedOccurrences: entry.retainedOccurrences,
|
|
222
|
+
}));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function hybridPreviewIndices(items: readonly AnalysisItem[]): number[] {
|
|
226
|
+
const literal: number[] = [];
|
|
227
|
+
const concept: number[] = [];
|
|
228
|
+
for (const [index, item] of items.entries()) {
|
|
229
|
+
if (item.details?.source === "literal" && literal.length < 3) literal.push(index);
|
|
230
|
+
if (item.details?.source === "concept") concept.push(index);
|
|
231
|
+
}
|
|
232
|
+
return [...literal, ...concept];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Stores only bounded display evidence and version references, never syntax trees. */
|
|
236
|
+
export class AnalysisStore {
|
|
237
|
+
readonly #items = new Map<string, StoredAnalysis>();
|
|
238
|
+
readonly #expired = new Set<string>();
|
|
239
|
+
readonly #now: () => number;
|
|
240
|
+
constructor(now: () => number = Date.now) {
|
|
241
|
+
this.#now = now;
|
|
242
|
+
}
|
|
243
|
+
clear(): void {
|
|
244
|
+
for (const id of this.#items.keys()) this.#rememberExpired(id);
|
|
245
|
+
this.#items.clear();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
create(result: AnalysisResultSet, summarize?: RetainedAnalysisSummary): string {
|
|
249
|
+
this.#expire();
|
|
250
|
+
const bounded: AnalysisResultSet = {
|
|
251
|
+
...result,
|
|
252
|
+
reasons: boundedReasons(result.reasons),
|
|
253
|
+
items: [],
|
|
254
|
+
coverage: {
|
|
255
|
+
...result.coverage,
|
|
256
|
+
retention: result.coverage?.retention ?? "complete",
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
let bytes = Buffer.byteLength(JSON.stringify(bounded));
|
|
260
|
+
const candidates = result.items.map((item, index) => ({ item, index }));
|
|
261
|
+
const retainedIndices: number[] = [];
|
|
262
|
+
const rebuildItems = (): void => {
|
|
263
|
+
const retained = new Set(retainedIndices);
|
|
264
|
+
bounded.items = result.items
|
|
265
|
+
.filter((_item, index) => retained.has(index))
|
|
266
|
+
.map((item) => structuredClone(item));
|
|
267
|
+
};
|
|
268
|
+
for (const candidate of candidates) {
|
|
269
|
+
const { item } = candidate;
|
|
270
|
+
const itemBytes = Buffer.byteLength(JSON.stringify(item)) + 1;
|
|
271
|
+
if (
|
|
272
|
+
retainedIndices.length >= MAX_ANALYSIS_RESULTS ||
|
|
273
|
+
bytes + itemBytes > MAX_ANALYSIS_STORAGE_BYTES - ANALYSIS_METADATA_RESERVE_BYTES
|
|
274
|
+
) {
|
|
275
|
+
bounded.partial = true;
|
|
276
|
+
if (bounded.coverage) bounded.coverage.retention = "partial";
|
|
277
|
+
bounded.reasons.push("Analysis storage limit: 50,000 items / 32 MiB; narrow the query");
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
retainedIndices.push(candidate.index);
|
|
281
|
+
bytes += itemBytes;
|
|
282
|
+
}
|
|
283
|
+
rebuildItems();
|
|
284
|
+
if (summarize) Object.assign(bounded, summarize(bounded.items));
|
|
285
|
+
bytes = Buffer.byteLength(JSON.stringify(bounded));
|
|
286
|
+
while (bytes > MAX_ANALYSIS_STORAGE_BYTES - 1024 && bounded.items.length > 0) {
|
|
287
|
+
retainedIndices.pop();
|
|
288
|
+
rebuildItems();
|
|
289
|
+
bounded.partial = true;
|
|
290
|
+
if (bounded.coverage) bounded.coverage.retention = "partial";
|
|
291
|
+
if (
|
|
292
|
+
!bounded.reasons.includes("Analysis storage limit: 50,000 items / 32 MiB; narrow the query")
|
|
293
|
+
)
|
|
294
|
+
bounded.reasons.push("Analysis storage limit: 50,000 items / 32 MiB; narrow the query");
|
|
295
|
+
if (summarize) Object.assign(bounded, summarize(bounded.items));
|
|
296
|
+
bytes = Buffer.byteLength(JSON.stringify(bounded));
|
|
297
|
+
}
|
|
298
|
+
bounded.reasons = boundedReasons(bounded.reasons);
|
|
299
|
+
bytes = Buffer.byteLength(JSON.stringify(bounded));
|
|
300
|
+
if (bytes > MAX_ANALYSIS_STORAGE_BYTES - 1024)
|
|
301
|
+
throw new SiftLightError("Analysis metadata exceeds the storage budget");
|
|
302
|
+
while (
|
|
303
|
+
this.#items.size >= MAX_ANALYSIS_SNAPSHOTS ||
|
|
304
|
+
this.#totalBytes() + bytes > MAX_ANALYSIS_STORAGE_BYTES ||
|
|
305
|
+
this.#totalItems() + bounded.items.length > MAX_ANALYSIS_RESULTS
|
|
306
|
+
) {
|
|
307
|
+
const oldest = [...this.#items.values()].toSorted((a, b) => a.touched - b.touched)[0];
|
|
308
|
+
if (!oldest) throw new SiftLightError("Analysis metadata exceeds the storage budget");
|
|
309
|
+
this.#items.delete(oldest.id);
|
|
310
|
+
this.#rememberExpired(oldest.id);
|
|
311
|
+
}
|
|
312
|
+
const id = randomUUID();
|
|
313
|
+
this.#items.set(id, { id, result: bounded, bytes, touched: this.#now() });
|
|
314
|
+
return `${id}.${result.kind === "hybrid" ? "analysis-hybrid" : "analysis"}.0`;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
resolve(cursor: string): {
|
|
318
|
+
stored: StoredAnalysis;
|
|
319
|
+
offset: number;
|
|
320
|
+
kind: "analysis" | "analysis-hybrid" | "analysis-terms";
|
|
321
|
+
} {
|
|
322
|
+
this.#expire();
|
|
323
|
+
const match = /^([a-f0-9-]+)\.(analysis|analysis-hybrid|analysis-terms)\.([0-9a-z]+)$/.exec(
|
|
324
|
+
cursor,
|
|
325
|
+
);
|
|
326
|
+
if (!match) throw new CursorError("Invalid analysis cursor");
|
|
327
|
+
const id = match[1];
|
|
328
|
+
const kind = match[2];
|
|
329
|
+
const rawOffset = match[3];
|
|
330
|
+
if (kind !== "analysis" && kind !== "analysis-hybrid" && kind !== "analysis-terms")
|
|
331
|
+
throw new CursorError("Invalid analysis cursor");
|
|
332
|
+
if (!id || !rawOffset) throw new CursorError("Invalid analysis cursor");
|
|
333
|
+
const offset = Number.parseInt(rawOffset, 36);
|
|
334
|
+
const stored = this.#items.get(id);
|
|
335
|
+
if (!stored)
|
|
336
|
+
throw new CursorError(
|
|
337
|
+
this.#expired.has(id)
|
|
338
|
+
? "Analysis cursor expired or was evicted; run the query again"
|
|
339
|
+
: "Analysis cursor was not found; run the query again",
|
|
340
|
+
this.#expired.has(id) ? "E_CURSOR_EXPIRED" : "E_CURSOR_NOT_FOUND",
|
|
341
|
+
);
|
|
342
|
+
if (
|
|
343
|
+
!Number.isSafeInteger(offset) ||
|
|
344
|
+
offset < 0 ||
|
|
345
|
+
offset.toString(36) !== rawOffset ||
|
|
346
|
+
(kind === "analysis"
|
|
347
|
+
? offset > stored.result.items.length
|
|
348
|
+
: kind === "analysis-hybrid"
|
|
349
|
+
? offset !== 0 || stored.result.kind !== "hybrid"
|
|
350
|
+
: offset >= (stored.result.termCounts?.length ?? 0))
|
|
351
|
+
)
|
|
352
|
+
throw new CursorError("Invalid analysis offset", "E_CURSOR_OFFSET_INVALID");
|
|
353
|
+
stored.touched = this.#now();
|
|
354
|
+
return { stored, offset, kind };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
item(cursor: string, index: number): AnalysisItem {
|
|
358
|
+
const { stored, kind } = this.resolve(cursor);
|
|
359
|
+
if (kind !== "analysis")
|
|
360
|
+
throw new CursorError("Term inventories do not contain source items", "E_CURSOR_WRONG_KIND");
|
|
361
|
+
if (!Number.isSafeInteger(index) || index < 1)
|
|
362
|
+
throw new CursorError("matchIndex must be a positive analysis item index");
|
|
363
|
+
const item = stored.result.items[index - 1];
|
|
364
|
+
if (!item) throw new CursorError("Analysis item is outside the retained result");
|
|
365
|
+
return structuredClone(item);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
page(cursor: string, modelOutput = false): SiftLightResult {
|
|
369
|
+
const { stored, offset, kind } = this.resolve(cursor);
|
|
370
|
+
const { result } = stored;
|
|
371
|
+
if (kind === "analysis-terms") return analysisTermPage(result, stored.id, offset);
|
|
372
|
+
const hybridPreview = kind === "analysis-hybrid";
|
|
373
|
+
const hybridMatchesRequest = hybridPreview
|
|
374
|
+
? { cursor: `${stored.id}.analysis.0`, ...(result.redact ? { redact: true } : {}) }
|
|
375
|
+
: undefined;
|
|
376
|
+
const pagedTerms =
|
|
377
|
+
result.termCounts &&
|
|
378
|
+
Buffer.byteLength(JSON.stringify(result.termCounts)) > MAX_INLINE_TERM_COUNT_BYTES;
|
|
379
|
+
const inlineTerms = pagedTerms ? undefined : safeTermCounts(result.termCounts);
|
|
380
|
+
const termsRequest = pagedTerms ? termCountRequest(stored.id, 0, result.redact) : undefined;
|
|
381
|
+
const items: NonNullable<SiftLightResult["details"]["analysis"]>["items"] = [];
|
|
382
|
+
const sources: NonNullable<SiftLightResult["details"]["analysis"]>["sources"] = [];
|
|
383
|
+
const sourceIds = new Map<string, number>();
|
|
384
|
+
const hybridInspectCursor = result.kind === "hybrid" ? `${stored.id}.analysis.0` : undefined;
|
|
385
|
+
const statistics = statisticsForItems(
|
|
386
|
+
result.items,
|
|
387
|
+
result.items.length,
|
|
388
|
+
result.unit,
|
|
389
|
+
analysisExtraGroups(result.counts, result.termCounts, result.items),
|
|
390
|
+
);
|
|
391
|
+
const scope = result.scope
|
|
392
|
+
? ` Scope: ${result.scope.assertion === "project-wide" ? "project root" : "requested path"} ${JSON.stringify(result.scope.path)}${result.scope.expandedToProjectRoot ? `, expanded after ${JSON.stringify(result.scope.requestedPath)} had no matches` : ""}.${modificationTimeBoundsText(result.scope.modifiedAfterMs, result.scope.modifiedBeforeMs)}`
|
|
393
|
+
: "";
|
|
394
|
+
const coverage = result.coverage ? ` Coverage: ${JSON.stringify(result.coverage)}.` : "";
|
|
395
|
+
const stats = result.stats ? ` Stats: ${JSON.stringify(result.stats)}.` : "";
|
|
396
|
+
const semanticJudge = result.semanticJudge
|
|
397
|
+
? ` Semantic judge: ${result.semanticJudge.status}; provider=${result.semanticJudge.provider}; judged ${String(result.semanticJudge.judgedCandidates)} of ${String(result.semanticJudge.candidatesConsidered)} candidates.`
|
|
398
|
+
: "";
|
|
399
|
+
const hasItemDetails = result.items.some((item) => publicItemDetails(item) !== undefined);
|
|
400
|
+
const header = `${result.kind}: ${result.items.length} retained ${result.unit} (${result.partial ? "PARTIAL" : "complete"}). ${publicAnalysisLabel(result)}. ${result.counts ? `Counts: ${JSON.stringify(result.counts)}. ` : ""}${inlineTerms ? `Term counts: ${JSON.stringify(inlineTerms)}. ` : ""}${termsRequest ? `Term counts are paginated: ${JSON.stringify(termsRequest)}. ` : ""}Counts use ${result.unit}; they are not ordinary matching-line counts.${hasItemDetails ? " Structured output retains per-item evidence details." : ""}${semanticJudge}${scope}${coverage}${stats}`;
|
|
401
|
+
const notice = result.reasons.length
|
|
402
|
+
? `\n${result.reasons.map((reason) => `[${reason}]`).join("\n")}`
|
|
403
|
+
: "";
|
|
404
|
+
const rows: string[] = [];
|
|
405
|
+
let bytes = Buffer.byteLength(header + notice) + 1200;
|
|
406
|
+
let next = offset;
|
|
407
|
+
const appendItem = (index: number): boolean => {
|
|
408
|
+
const item = result.items[index];
|
|
409
|
+
if (!item) throw new Error("Analysis item unavailable");
|
|
410
|
+
const publicItem = publicAnalysisItem(result, item, index, stored.id, modelOutput);
|
|
411
|
+
const inspect = publicItem.inspect;
|
|
412
|
+
const exposeExcerpt =
|
|
413
|
+
result.kind === "concept" ||
|
|
414
|
+
(result.kind === "hybrid" && item.details?.source === "concept");
|
|
415
|
+
const row = `#${index + 1} ${item.path}:${item.line} ${publicItem.label}${exposeExcerpt && item.excerpt ? `\n${item.excerpt}` : ""}${inspect && !hybridInspectCursor ? `\nInspect: ${JSON.stringify(inspect)}` : ""}`;
|
|
416
|
+
const rowBytes = Buffer.byteLength(row) + 2;
|
|
417
|
+
if (bytes + rowBytes > MAX_RESULT_BYTES) {
|
|
418
|
+
if (items.length === 0)
|
|
419
|
+
throw new SiftLightError("Analysis item exceeds the response limit; narrow its source");
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
rows.push(row);
|
|
423
|
+
bytes += rowBytes;
|
|
424
|
+
if (hybridInspectCursor) {
|
|
425
|
+
const { inspect: _inspect, ...sharedPublicItem } = publicItem;
|
|
426
|
+
const publicHybridItem = {
|
|
427
|
+
...sharedPublicItem,
|
|
428
|
+
...(item.excerpt ? { excerpt: item.excerpt } : {}),
|
|
429
|
+
};
|
|
430
|
+
if (item.source) {
|
|
431
|
+
const sourceKey = JSON.stringify(item.source);
|
|
432
|
+
let sourceId = sourceIds.get(sourceKey);
|
|
433
|
+
if (sourceId === undefined) {
|
|
434
|
+
sourceId = sources.length;
|
|
435
|
+
sources.push(item.source);
|
|
436
|
+
sourceIds.set(sourceKey, sourceId);
|
|
437
|
+
}
|
|
438
|
+
items.push({ ...publicHybridItem, sourceId });
|
|
439
|
+
} else {
|
|
440
|
+
items.push(publicHybridItem);
|
|
441
|
+
}
|
|
442
|
+
} else {
|
|
443
|
+
items.push({
|
|
444
|
+
...item,
|
|
445
|
+
label: publicItem.label,
|
|
446
|
+
index: index + 1,
|
|
447
|
+
...(inspect ? { inspect } : {}),
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
if (!hybridPreview) next = index + 1;
|
|
451
|
+
return true;
|
|
452
|
+
};
|
|
453
|
+
if (hybridPreview) {
|
|
454
|
+
for (const index of hybridPreviewIndices(result.items)) {
|
|
455
|
+
if (items.length >= 30 || !appendItem(index)) break;
|
|
456
|
+
}
|
|
457
|
+
} else {
|
|
458
|
+
for (let index = offset; index < result.items.length && items.length < 30; index += 1) {
|
|
459
|
+
if (!appendItem(index)) break;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
const nextRequest =
|
|
463
|
+
hybridMatchesRequest ??
|
|
464
|
+
(next < result.items.length
|
|
465
|
+
? {
|
|
466
|
+
cursor: `${stored.id}.analysis.${next.toString(36)}`,
|
|
467
|
+
...(result.redact ? { redact: true } : {}),
|
|
468
|
+
}
|
|
469
|
+
: undefined);
|
|
470
|
+
const text = [
|
|
471
|
+
header + notice,
|
|
472
|
+
...rows,
|
|
473
|
+
...(hybridInspectCursor
|
|
474
|
+
? [
|
|
475
|
+
`Inspect item #N: ${JSON.stringify({ mode: "inspect", cursor: hybridInspectCursor, matchIndex: "N" })}`,
|
|
476
|
+
]
|
|
477
|
+
: []),
|
|
478
|
+
...(nextRequest ? [`Next request: ${JSON.stringify(nextRequest)}`] : []),
|
|
479
|
+
].join("\n\n");
|
|
480
|
+
if (Buffer.byteLength(text) > MAX_RESULT_BYTES)
|
|
481
|
+
throw new SiftLightError("Analysis metadata exceeds the output limit");
|
|
482
|
+
return {
|
|
483
|
+
text,
|
|
484
|
+
details: {
|
|
485
|
+
version: 1,
|
|
486
|
+
mode:
|
|
487
|
+
result.kind === "concept" ||
|
|
488
|
+
result.kind === "hybrid" ||
|
|
489
|
+
result.kind === "structure" ||
|
|
490
|
+
result.kind === "files" ||
|
|
491
|
+
result.kind === "outline" ||
|
|
492
|
+
result.kind === "imports" ||
|
|
493
|
+
result.kind === "tests"
|
|
494
|
+
? result.kind
|
|
495
|
+
: "matches",
|
|
496
|
+
status: result.partial ? "partial" : "complete",
|
|
497
|
+
snapshotComplete: !result.partial,
|
|
498
|
+
totalMatches: result.items.length,
|
|
499
|
+
storedMatches: result.items.length,
|
|
500
|
+
returnedMatches: items.length,
|
|
501
|
+
totalFiles: statistics.files,
|
|
502
|
+
statistics,
|
|
503
|
+
cursor: nextRequest?.cursor ?? `${stored.id}.analysis.0`,
|
|
504
|
+
...(nextRequest ? { nextRequest } : {}),
|
|
505
|
+
analysis: {
|
|
506
|
+
kind: result.kind,
|
|
507
|
+
unit: result.unit,
|
|
508
|
+
totalItems: result.items.length,
|
|
509
|
+
returnedItems: items.length,
|
|
510
|
+
...(modelOutput ? { modelOutput: true } : {}),
|
|
511
|
+
statistics,
|
|
512
|
+
items,
|
|
513
|
+
...(sources.length ? { sources } : {}),
|
|
514
|
+
reasons: result.reasons,
|
|
515
|
+
...(result.semanticJudge ? { semanticJudge: result.semanticJudge } : {}),
|
|
516
|
+
...(result.filesRead !== undefined ? { filesRead: result.filesRead } : {}),
|
|
517
|
+
...(result.bytesRead !== undefined ? { bytesRead: result.bytesRead } : {}),
|
|
518
|
+
...(result.changes ? { changes: result.changes } : {}),
|
|
519
|
+
...(result.counts ? { counts: result.counts } : {}),
|
|
520
|
+
...(inlineTerms ? { termCounts: inlineTerms } : {}),
|
|
521
|
+
...(termsRequest
|
|
522
|
+
? { totalTerms: result.termCounts?.length ?? 0, termCountsNextRequest: termsRequest }
|
|
523
|
+
: {}),
|
|
524
|
+
...(result.scope ? { scope: result.scope } : {}),
|
|
525
|
+
...(result.chunks !== undefined ? { chunks: result.chunks } : {}),
|
|
526
|
+
...(result.coverage ? { coverage: result.coverage } : {}),
|
|
527
|
+
...(result.stats ? { stats: result.stats } : {}),
|
|
528
|
+
...(result.sourceGeneration ? { sourceGeneration: result.sourceGeneration } : {}),
|
|
529
|
+
...(hybridMatchesRequest ? { matchesRequest: hybridMatchesRequest } : {}),
|
|
530
|
+
...(hybridInspectCursor ? { inspectCursor: hybridInspectCursor } : {}),
|
|
531
|
+
},
|
|
532
|
+
...(result.scope ? { scope: result.scope } : {}),
|
|
533
|
+
...(result.redact ? { redactionRequested: true } : {}),
|
|
534
|
+
},
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
#expire(): void {
|
|
539
|
+
for (const [id, item] of this.#items)
|
|
540
|
+
if (this.#now() - item.touched >= ANALYSIS_TTL_MS) {
|
|
541
|
+
this.#items.delete(id);
|
|
542
|
+
this.#rememberExpired(id);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
#totalBytes(): number {
|
|
546
|
+
return [...this.#items.values()].reduce((n, item) => n + item.bytes, 0);
|
|
547
|
+
}
|
|
548
|
+
#totalItems(): number {
|
|
549
|
+
return [...this.#items.values()].reduce((n, item) => n + item.result.items.length, 0);
|
|
550
|
+
}
|
|
551
|
+
#rememberExpired(id: string): void {
|
|
552
|
+
this.#expired.add(id);
|
|
553
|
+
while (this.#expired.size > MAX_ANALYSIS_SNAPSHOTS * 4) {
|
|
554
|
+
const oldest = this.#expired.values().next().value;
|
|
555
|
+
if (oldest === undefined) break;
|
|
556
|
+
this.#expired.delete(oldest);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { AnalysisResultSet } from "./analysis-types.js";
|
|
2
|
+
import type { SiftLightResult } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export const MAX_INLINE_TERM_COUNT_BYTES = 8 * 1024;
|
|
5
|
+
|
|
6
|
+
export function termCountRequest(id: string, offset: number, redact = false) {
|
|
7
|
+
return {
|
|
8
|
+
cursor: `${id}.analysis-terms.${offset.toString(36)}`,
|
|
9
|
+
...(redact ? { redact: true } : {}),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Term metadata has its own pages so even a complete empty result remains readable. */
|
|
14
|
+
export function analysisTermPage(
|
|
15
|
+
result: AnalysisResultSet,
|
|
16
|
+
id: string,
|
|
17
|
+
offset: number,
|
|
18
|
+
): SiftLightResult {
|
|
19
|
+
const all = result.termCounts ?? [];
|
|
20
|
+
const terms: { term: string; retainedOccurrences: number }[] = [];
|
|
21
|
+
const rows: string[] = [];
|
|
22
|
+
let bytes = 0;
|
|
23
|
+
for (let index = offset; index < all.length; index++) {
|
|
24
|
+
const term = all[index];
|
|
25
|
+
if (!term) throw new Error("Term inventory index unavailable");
|
|
26
|
+
const label = `condition #${String(index + 1)}`;
|
|
27
|
+
const row = `${label}: ${String(term.retainedOccurrences)} retained occurrences`;
|
|
28
|
+
const size = Buffer.byteLength(row) + 2;
|
|
29
|
+
if (bytes + size > MAX_INLINE_TERM_COUNT_BYTES) break;
|
|
30
|
+
rows.push(row);
|
|
31
|
+
terms.push({ term: label, retainedOccurrences: term.retainedOccurrences });
|
|
32
|
+
bytes += size;
|
|
33
|
+
}
|
|
34
|
+
const nextOffset = offset + terms.length;
|
|
35
|
+
const nextRequest =
|
|
36
|
+
nextOffset < all.length ? termCountRequest(id, nextOffset, result.redact) : undefined;
|
|
37
|
+
const matchesRequest = { cursor: `${id}.analysis.0`, ...(result.redact ? { redact: true } : {}) };
|
|
38
|
+
return {
|
|
39
|
+
text: [
|
|
40
|
+
`Condition inventory ${String(offset + 1)}-${String(nextOffset)} of ${String(all.length)} (${result.partial ? "PARTIAL evidence" : "complete evidence"}). Counts refer to retained occurrences.`,
|
|
41
|
+
...rows,
|
|
42
|
+
...(nextRequest ? [`Next request: ${JSON.stringify(nextRequest)}`] : []),
|
|
43
|
+
`Match metadata request: ${JSON.stringify(matchesRequest)}`,
|
|
44
|
+
].join("\n\n"),
|
|
45
|
+
details: {
|
|
46
|
+
version: 1,
|
|
47
|
+
mode: "matches",
|
|
48
|
+
status: result.partial ? "partial" : "complete",
|
|
49
|
+
snapshotComplete: !result.partial,
|
|
50
|
+
totalMatches: result.items.length,
|
|
51
|
+
storedMatches: result.items.length,
|
|
52
|
+
returnedMatches: 0,
|
|
53
|
+
totalFiles: new Set(result.items.map((item) => item.path)).size,
|
|
54
|
+
cursor: nextRequest?.cursor ?? matchesRequest.cursor,
|
|
55
|
+
...(nextRequest ? { nextRequest } : {}),
|
|
56
|
+
...(result.redact ? { redactionRequested: true } : {}),
|
|
57
|
+
analysis: {
|
|
58
|
+
kind: result.kind,
|
|
59
|
+
unit: result.unit,
|
|
60
|
+
totalItems: result.items.length,
|
|
61
|
+
returnedItems: 0,
|
|
62
|
+
items: [],
|
|
63
|
+
reasons: result.reasons,
|
|
64
|
+
termCounts: terms,
|
|
65
|
+
termCountsOffset: offset,
|
|
66
|
+
totalTerms: all.length,
|
|
67
|
+
...(nextRequest ? { termCountsNextRequest: nextRequest } : {}),
|
|
68
|
+
matchesRequest,
|
|
69
|
+
...(result.coverage ? { coverage: result.coverage } : {}),
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|