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.
Files changed (145) hide show
  1. package/CHANGELOG.md +0 -0
  2. package/CONTRIBUTING.md +19 -0
  3. package/LICENSE +661 -0
  4. package/README.md +177 -0
  5. package/README.zh-CN.md +177 -0
  6. package/SECURITY.md +25 -0
  7. package/package.json +142 -0
  8. package/plugins/sift-light/.claude-plugin/plugin.json +10 -0
  9. package/plugins/sift-light/.codex-plugin/plugin.json +20 -0
  10. package/plugins/sift-light/.mcp.json +24 -0
  11. package/plugins/sift-light/LICENSE +661 -0
  12. package/plugins/sift-light/concept-worker.mjs +567 -0
  13. package/plugins/sift-light/hooks/THIRD_PARTY_NOTICES.txt +70 -0
  14. package/plugins/sift-light/hooks/hooks.json +16 -0
  15. package/plugins/sift-light/hooks/search-policy.mjs +4113 -0
  16. package/plugins/sift-light/hooks/tree-sitter-bash.wasm +0 -0
  17. package/plugins/sift-light/hooks/tree-sitter-powershell.wasm +0 -0
  18. package/plugins/sift-light/hooks/tree-sitter.wasm +0 -0
  19. package/plugins/sift-light/kimi.plugin.json +36 -0
  20. package/plugins/sift-light/mcp-server.mjs +1 -0
  21. package/plugins/sift-light/omp-extension.mjs +20914 -0
  22. package/plugins/sift-light/package.json +11 -0
  23. package/plugins/sift-light/syntax-worker.mjs +280 -0
  24. package/plugins/sift-light/syntax-worker.toml +2 -0
  25. package/src/analysis-evidence.ts +43 -0
  26. package/src/analysis-limits.ts +27 -0
  27. package/src/analysis-store.ts +559 -0
  28. package/src/analysis-term-pages.ts +73 -0
  29. package/src/analysis-types.ts +162 -0
  30. package/src/audit-search.ts +391 -0
  31. package/src/capped-lines.ts +112 -0
  32. package/src/concept-embedding-cache.ts +186 -0
  33. package/src/concept-inference.ts +218 -0
  34. package/src/concept-model.ts +96 -0
  35. package/src/concept-search.ts +577 -0
  36. package/src/concept-setup.ts +75 -0
  37. package/src/concept-source-generation.ts +268 -0
  38. package/src/concept-worker.mjs +567 -0
  39. package/src/concept-worker.ts +210 -0
  40. package/src/config-reader.ts +283 -0
  41. package/src/config.ts +37 -0
  42. package/src/context-budget.ts +26 -0
  43. package/src/discovery-errors.ts +2 -0
  44. package/src/errors.ts +95 -0
  45. package/src/evidence-candidates.ts +344 -0
  46. package/src/evidence-ranking.ts +48 -0
  47. package/src/evidence-service.ts +1047 -0
  48. package/src/evidence-validation.ts +466 -0
  49. package/src/evidence-validity.ts +33 -0
  50. package/src/excerpt.ts +53 -0
  51. package/src/file-discovery.ts +150 -0
  52. package/src/file-metadata-filter.ts +46 -0
  53. package/src/format.ts +407 -0
  54. package/src/git-diff.ts +195 -0
  55. package/src/git-process.ts +189 -0
  56. package/src/git-repository.ts +367 -0
  57. package/src/git-source.ts +385 -0
  58. package/src/historical-paths.ts +205 -0
  59. package/src/hybrid-search.ts +340 -0
  60. package/src/import-model.ts +611 -0
  61. package/src/import-navigation.ts +100 -0
  62. package/src/import-resolution.ts +240 -0
  63. package/src/index.ts +97 -0
  64. package/src/inspect-batch.ts +265 -0
  65. package/src/inspect.ts +271 -0
  66. package/src/language-capabilities.ts +157 -0
  67. package/src/language-capability-definitions.ts +204 -0
  68. package/src/literal-search.ts +25 -0
  69. package/src/mcp-cli.ts +21 -0
  70. package/src/mcp-model-output.ts +120 -0
  71. package/src/mcp-output.ts +9 -0
  72. package/src/mcp-semantic-judge.ts +39 -0
  73. package/src/mcp-server.mjs +14064 -0
  74. package/src/mcp-server.ts +157 -0
  75. package/src/mcp-stdio.ts +114 -0
  76. package/src/mcp.ts +631 -0
  77. package/src/model-error.ts +120 -0
  78. package/src/multi-term-search.ts +122 -0
  79. package/src/omp-index.ts +277 -0
  80. package/src/omp-marketplace-entry.ts +7 -0
  81. package/src/operation-lifecycle.ts +413 -0
  82. package/src/operation-output.ts +100 -0
  83. package/src/owned-parallel.ts +22 -0
  84. package/src/owned-process.ts +102 -0
  85. package/src/owned-task-queue.ts +38 -0
  86. package/src/package-version.ts +3 -0
  87. package/src/path-policy.ts +203 -0
  88. package/src/pi-search-policy.ts +33 -0
  89. package/src/prompt-guidelines.ts +54 -0
  90. package/src/python-outline.ts +231 -0
  91. package/src/record-value.ts +3 -0
  92. package/src/redaction.ts +95 -0
  93. package/src/request-contract-catalog.ts +215 -0
  94. package/src/request-contract-error.ts +96 -0
  95. package/src/request-contract.ts +481 -0
  96. package/src/request.ts +132 -0
  97. package/src/result-statistics.ts +178 -0
  98. package/src/rg.ts +574 -0
  99. package/src/ripgrep-diagnostics.ts +122 -0
  100. package/src/ripgrep-executable.ts +43 -0
  101. package/src/runtime.ts +53 -0
  102. package/src/scan-revisions.ts +148 -0
  103. package/src/script-runtime.ts +11 -0
  104. package/src/search-policy-commands.ts +170 -0
  105. package/src/search-policy-hook.ts +61 -0
  106. package/src/search-policy-recovery.ts +388 -0
  107. package/src/search-policy-shell.ts +280 -0
  108. package/src/search-policy.ts +102 -0
  109. package/src/search-retention.ts +77 -0
  110. package/src/semantic-judge-batches.ts +135 -0
  111. package/src/semantic-judge.ts +519 -0
  112. package/src/service.ts +945 -0
  113. package/src/session-summary.ts +100 -0
  114. package/src/snapshot-store.ts +203 -0
  115. package/src/source-access.ts +288 -0
  116. package/src/source-continuations.ts +168 -0
  117. package/src/source-document.ts +277 -0
  118. package/src/source-inspection.ts +731 -0
  119. package/src/source-pages.ts +112 -0
  120. package/src/source.ts +286 -0
  121. package/src/structural-search.ts +125 -0
  122. package/src/structure.ts +259 -0
  123. package/src/summary-previews.ts +96 -0
  124. package/src/summary.ts +79 -0
  125. package/src/syntax-facts.ts +464 -0
  126. package/src/syntax-fields.ts +161 -0
  127. package/src/syntax-search.ts +361 -0
  128. package/src/syntax-tree.ts +30 -0
  129. package/src/syntax-types.ts +72 -0
  130. package/src/syntax-worker.mjs +280 -0
  131. package/src/syntax-worker.toml +2 -0
  132. package/src/syntax-worker.ts +143 -0
  133. package/src/syntax.ts +259 -0
  134. package/src/test-navigation-facts.ts +261 -0
  135. package/src/test-navigation.ts +543 -0
  136. package/src/tool-schema.ts +321 -0
  137. package/src/tui/dashboard.ts +107 -0
  138. package/src/tui/layout.ts +291 -0
  139. package/src/tui/palette.ts +73 -0
  140. package/src/tui/presentation.ts +240 -0
  141. package/src/tui/renderers.ts +106 -0
  142. package/src/types.ts +368 -0
  143. package/src/validation-output.ts +148 -0
  144. package/src/validation-types.ts +48 -0
  145. package/src/workspace-files.ts +142 -0
@@ -0,0 +1,519 @@
1
+ import type { SemanticJudgeConfig } from "./config-reader.js";
2
+ import {
3
+ SEMANTIC_JUDGE_CLASSIFICATIONS,
4
+ SEMANTIC_JUDGE_NON_PROOF_CLAIM,
5
+ type AnalysisResultSet,
6
+ type SemanticJudgeClassification,
7
+ type SemanticJudgeDetails,
8
+ type SemanticJudgeJudgment,
9
+ } from "./analysis-types.js";
10
+ import { SiftLightError } from "./errors.js";
11
+ import {
12
+ createSemanticJudgeBatches,
13
+ judgeSemanticCandidateBatches,
14
+ MAX_SEMANTIC_JUDGE_REQUEST_BYTES,
15
+ SemanticJudgePayloadTooLargeError,
16
+ } from "./semantic-judge-batches.js";
17
+
18
+ const MAX_CANDIDATE_CHARS = 4_000;
19
+ const MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
20
+ const MAX_ERROR_CHARS = 512;
21
+
22
+ const CLASSIFICATION_PRIORITY: Readonly<Record<SemanticJudgeClassification, number>> = {
23
+ "implementation-candidate": 0,
24
+ "caller-candidate": 1,
25
+ "mention-only": 2,
26
+ documentation: 3,
27
+ "test-only": 4,
28
+ uncertain: 5,
29
+ irrelevant: 6,
30
+ };
31
+
32
+ export interface SemanticJudgeCandidate {
33
+ id: string;
34
+ excerpt: string;
35
+ }
36
+
37
+ export interface SemanticJudgeResult {
38
+ model: string;
39
+ judgments: SemanticJudgeJudgment[];
40
+ inputTokens?: number;
41
+ outputTokens?: number;
42
+ elapsedMs: number;
43
+ }
44
+
45
+ export type SemanticJudgeRunner = (
46
+ query: string,
47
+ candidates: readonly SemanticJudgeCandidate[],
48
+ signal?: AbortSignal,
49
+ ) => Promise<SemanticJudgeResult>;
50
+
51
+ export type SemanticJudgeFetcher = (
52
+ input: string | URL | Request,
53
+ init?: RequestInit,
54
+ ) => Promise<Response>;
55
+
56
+ export interface SemanticJudgeIntegration {
57
+ config: SemanticJudgeConfig;
58
+ runner?: SemanticJudgeRunner;
59
+ }
60
+
61
+ export class SemanticJudgeConfigurationError extends SiftLightError {
62
+ constructor(message: string, options?: ErrorOptions) {
63
+ super(message, options);
64
+ this.name = "SemanticJudgeConfigurationError";
65
+ }
66
+ }
67
+
68
+ class SemanticJudgeRequestError extends SiftLightError {
69
+ readonly retryable: boolean;
70
+
71
+ constructor(message: string, retryable: boolean, options?: ErrorOptions) {
72
+ super(message, options);
73
+ this.name = "SemanticJudgeRequestError";
74
+ this.retryable = retryable;
75
+ }
76
+ }
77
+
78
+ interface RawRecord {
79
+ [key: string]: unknown;
80
+ }
81
+
82
+ function isRecord(value: unknown): value is RawRecord {
83
+ return typeof value === "object" && value !== null && !Array.isArray(value);
84
+ }
85
+
86
+ function boundedError(error: unknown): string {
87
+ const message = error instanceof Error ? error.message : String(error);
88
+ return message.slice(0, MAX_ERROR_CHARS);
89
+ }
90
+
91
+ function isClassification(value: unknown): value is SemanticJudgeClassification {
92
+ return (
93
+ typeof value === "string" &&
94
+ SEMANTIC_JUDGE_CLASSIFICATIONS.some((classification) => classification === value)
95
+ );
96
+ }
97
+
98
+ function probability(value: unknown, field: string): number {
99
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1) {
100
+ throw new SemanticJudgeRequestError(`Semantic judge response has an invalid ${field}`, false);
101
+ }
102
+ return value;
103
+ }
104
+
105
+ function choiceProbability(answer: RawRecord, choice: SemanticJudgeClassification): number {
106
+ const probabilities = answer.probabilities;
107
+ if (!isRecord(probabilities)) return probability(answer.confidence, "confidence");
108
+ return probability(probabilities[choice], `probabilities.${choice}`);
109
+ }
110
+
111
+ function parseJudgment(value: unknown, candidateIndex: number): SemanticJudgeJudgment {
112
+ if (!isRecord(value) || value.type !== "choice" || !isClassification(value.choice)) {
113
+ throw new SemanticJudgeRequestError(
114
+ `Semantic judge response is missing choice answer ${String(candidateIndex + 1)}`,
115
+ false,
116
+ );
117
+ }
118
+ const confidence =
119
+ value.confidence === undefined ? undefined : probability(value.confidence, "confidence");
120
+ return {
121
+ candidateIndex,
122
+ classification: value.choice,
123
+ probability: choiceProbability(value, value.choice),
124
+ ...(confidence === undefined ? {} : { confidence }),
125
+ };
126
+ }
127
+
128
+ function requestBody(
129
+ query: string,
130
+ candidates: readonly SemanticJudgeCandidate[],
131
+ model: string,
132
+ ): RawRecord {
133
+ const questions: RawRecord = {};
134
+ for (const candidate of candidates) {
135
+ questions[candidate.id] = {
136
+ type: "choice",
137
+ instructions:
138
+ "Classify the candidate by what it actually does for the requested behavior. Judge the code excerpt, not just matching words.",
139
+ criteria: {
140
+ "implementation-candidate":
141
+ "The excerpt appears to implement the requested behavior or its core decision/side effect.",
142
+ "caller-candidate":
143
+ "The excerpt invokes or wires an implementation but does not implement the behavior itself.",
144
+ "mention-only":
145
+ "The excerpt mentions the topic without implementing or invoking the behavior.",
146
+ documentation:
147
+ "The excerpt is documentation or explanatory prose rather than executable behavior.",
148
+ "test-only": "The excerpt is test or fixture code that describes or checks the behavior.",
149
+ irrelevant: "The excerpt is not meaningfully related to the requested behavior.",
150
+ uncertain: "The excerpt is too incomplete or ambiguous to classify confidently.",
151
+ },
152
+ };
153
+ }
154
+ return {
155
+ state: {
156
+ query,
157
+ candidates: candidates.map(({ id, excerpt }) => ({
158
+ id,
159
+ excerpt,
160
+ })),
161
+ },
162
+ model,
163
+ questions,
164
+ };
165
+ }
166
+
167
+ async function responseText(response: Response): Promise<string> {
168
+ if (!response.body) {
169
+ const contentLength = response.headers.get("content-length");
170
+ if (contentLength !== null && Number(contentLength) > MAX_RESPONSE_BYTES) {
171
+ throw new SemanticJudgeRequestError(
172
+ "Semantic judge response exceeded the 4 MiB limit",
173
+ false,
174
+ );
175
+ }
176
+ const text = await response.text();
177
+ if (new TextEncoder().encode(text).byteLength > MAX_RESPONSE_BYTES) {
178
+ throw new SemanticJudgeRequestError(
179
+ "Semantic judge response exceeded the 4 MiB limit",
180
+ false,
181
+ );
182
+ }
183
+ return text;
184
+ }
185
+ const reader = response.body.getReader();
186
+ const chunks: Uint8Array[] = [];
187
+ let bytes = 0;
188
+ try {
189
+ while (true) {
190
+ // oxlint-disable-next-line no-await-in-loop -- streaming response chunks must be read in order.
191
+ const result = await reader.read();
192
+ if (result.done) break;
193
+ const chunk = result.value;
194
+ bytes += chunk.byteLength;
195
+ if (bytes > MAX_RESPONSE_BYTES) {
196
+ // oxlint-disable-next-line no-await-in-loop -- cancel the owned stream before reporting the bounded-response failure.
197
+ await reader.cancel();
198
+ throw new SemanticJudgeRequestError(
199
+ "Semantic judge response exceeded the 4 MiB limit",
200
+ false,
201
+ );
202
+ }
203
+ chunks.push(chunk);
204
+ }
205
+ } finally {
206
+ reader.releaseLock();
207
+ }
208
+ const all = new Uint8Array(bytes);
209
+ let offset = 0;
210
+ for (const chunk of chunks) {
211
+ all.set(chunk, offset);
212
+ offset += chunk.byteLength;
213
+ }
214
+ return new TextDecoder("utf-8", { fatal: true }).decode(all);
215
+ }
216
+
217
+ function retryDelay(attempt: number): number {
218
+ return Math.min(20_000, 400 * 2 ** attempt);
219
+ }
220
+
221
+ async function waitForRetry(delayMs: number, signal?: AbortSignal): Promise<void> {
222
+ if (signal?.aborted)
223
+ throw signal.reason instanceof Error ? signal.reason : new Error("Operation aborted");
224
+ await new Promise<void>((resolve, reject) => {
225
+ const finish = (): void => {
226
+ clearTimeout(timer);
227
+ signal?.removeEventListener("abort", onAbort);
228
+ resolve();
229
+ };
230
+ const timer = setTimeout(finish, delayMs);
231
+ const onAbort = (): void => {
232
+ clearTimeout(timer);
233
+ signal?.removeEventListener("abort", onAbort);
234
+ reject(signal?.reason instanceof Error ? signal.reason : new Error("Operation aborted"));
235
+ };
236
+ signal?.addEventListener("abort", onAbort, { once: true });
237
+ });
238
+ }
239
+
240
+ function createJevRunner(
241
+ config: SemanticJudgeConfig,
242
+ key: string,
243
+ fetcher: SemanticJudgeFetcher,
244
+ ): SemanticJudgeRunner {
245
+ return async (query, candidates, parentSignal) => {
246
+ const startedAt = performance.now();
247
+ const body = JSON.stringify(requestBody(query, candidates, config.model));
248
+ if (new TextEncoder().encode(body).byteLength > MAX_SEMANTIC_JUDGE_REQUEST_BYTES) {
249
+ throw new SemanticJudgePayloadTooLargeError(
250
+ "Semantic judge request exceeded the 64 KiB request limit",
251
+ );
252
+ }
253
+ for (let attempt = 0; attempt <= config.maxRetries; attempt += 1) {
254
+ const controller = new AbortController();
255
+ const onParentAbort = (): void => controller.abort(parentSignal?.reason);
256
+ parentSignal?.addEventListener("abort", onParentAbort, { once: true });
257
+ const timeout = setTimeout(
258
+ () => controller.abort(new Error("Semantic judge request timed out")),
259
+ config.timeoutMs,
260
+ );
261
+ try {
262
+ // oxlint-disable-next-line no-await-in-loop -- retries intentionally serialize attempts under one request budget.
263
+ const response = await fetcher(config.endpoint, {
264
+ method: "POST",
265
+ headers: {
266
+ "content-type": "application/json",
267
+ authorization: `Bearer ${key}`,
268
+ },
269
+ body,
270
+ signal: controller.signal,
271
+ });
272
+ if (!response.ok) {
273
+ if (response.status === 413) {
274
+ throw new SemanticJudgePayloadTooLargeError(
275
+ "Semantic judge rejected an oversized request with HTTP 413",
276
+ );
277
+ }
278
+ const retryable =
279
+ response.status === 408 ||
280
+ response.status === 429 ||
281
+ response.status === 529 ||
282
+ response.status >= 500;
283
+ throw new SemanticJudgeRequestError(
284
+ `Semantic judge returned HTTP ${String(response.status)}`,
285
+ retryable,
286
+ );
287
+ }
288
+ // oxlint-disable-next-line no-await-in-loop -- parse the current bounded response before deciding whether to retry.
289
+ const raw: unknown = JSON.parse(await responseText(response));
290
+ if (!isRecord(raw) || !isRecord(raw.answers)) {
291
+ throw new SemanticJudgeRequestError(
292
+ "Semantic judge response did not contain answers",
293
+ false,
294
+ );
295
+ }
296
+ const answers = raw.answers;
297
+ const judgments = candidates.map((candidate, index) =>
298
+ parseJudgment(answers[candidate.id], index),
299
+ );
300
+ const model = raw.model ?? config.model;
301
+ if (
302
+ typeof model !== "string" ||
303
+ model.length === 0 ||
304
+ model.length > 128 ||
305
+ /[\r\n\0]/u.test(model)
306
+ ) {
307
+ throw new SemanticJudgeRequestError(
308
+ "Semantic judge response model must be bounded single-line text",
309
+ false,
310
+ );
311
+ }
312
+ const usage = isRecord(raw.usage) ? raw.usage : undefined;
313
+ const inputTokens =
314
+ typeof usage?.input_tokens === "number" &&
315
+ Number.isSafeInteger(usage.input_tokens) &&
316
+ usage.input_tokens >= 0
317
+ ? usage.input_tokens
318
+ : undefined;
319
+ const outputTokens =
320
+ typeof usage?.output_tokens === "number" &&
321
+ Number.isSafeInteger(usage.output_tokens) &&
322
+ usage.output_tokens >= 0
323
+ ? usage.output_tokens
324
+ : undefined;
325
+ return {
326
+ model,
327
+ judgments,
328
+ ...(inputTokens === undefined ? {} : { inputTokens }),
329
+ ...(outputTokens === undefined ? {} : { outputTokens }),
330
+ elapsedMs: Math.round(performance.now() - startedAt),
331
+ };
332
+ } catch (error) {
333
+ if (parentSignal?.aborted) throw error;
334
+ const retryable =
335
+ error instanceof SemanticJudgePayloadTooLargeError
336
+ ? false
337
+ : error instanceof SemanticJudgeRequestError
338
+ ? error.retryable
339
+ : true;
340
+ if (!retryable || attempt >= config.maxRetries) throw error;
341
+ // oxlint-disable-next-line no-await-in-loop -- provider retries share one bounded request budget.
342
+ await waitForRetry(retryDelay(attempt), parentSignal);
343
+ } finally {
344
+ clearTimeout(timeout);
345
+ parentSignal?.removeEventListener("abort", onParentAbort);
346
+ }
347
+ }
348
+ throw new SemanticJudgeRequestError("Semantic judge request failed", false);
349
+ };
350
+ }
351
+
352
+ export function createDisabledSemanticJudgeIntegration(
353
+ config: SemanticJudgeConfig,
354
+ ): SemanticJudgeIntegration {
355
+ return { config: { ...config, enabled: false } };
356
+ }
357
+
358
+ export function createSemanticJudgeIntegration(
359
+ config: SemanticJudgeConfig,
360
+ environment: NodeJS.ProcessEnv = process.env,
361
+ fetcher: SemanticJudgeFetcher = fetch,
362
+ ): SemanticJudgeIntegration {
363
+ if (!config.enabled) return createDisabledSemanticJudgeIntegration(config);
364
+ const key = environment[config.apiKeyEnv]?.trim();
365
+ if (!key) {
366
+ throw new SemanticJudgeConfigurationError(
367
+ `Semantic judge is enabled, but environment variable ${config.apiKeyEnv} is missing or empty`,
368
+ );
369
+ }
370
+ if (config.provider !== "jev") {
371
+ throw new SemanticJudgeConfigurationError(
372
+ `Unsupported semantic judge provider: ${String(config.provider)}`,
373
+ );
374
+ }
375
+ return { config, runner: createJevRunner(config, key, fetcher) };
376
+ }
377
+ function baseDetails(config: SemanticJudgeConfig): SemanticJudgeDetails {
378
+ return {
379
+ enabled: config.enabled,
380
+ provider: config.provider,
381
+ status: config.enabled ? "partial" : "disabled",
382
+ maxCandidates: config.maxCandidates,
383
+ candidatesConsidered: 0,
384
+ judgedCandidates: 0,
385
+ candidatesUnjudged: 0,
386
+ batchesAttempted: 0,
387
+ batchesCompleted: 0,
388
+ batchesFailed: 0,
389
+ batchesSplit: 0,
390
+ classificationCounts: {},
391
+ };
392
+ }
393
+
394
+ function optionalUsageTotal(
395
+ results: readonly SemanticJudgeResult[],
396
+ select: (result: SemanticJudgeResult) => number | undefined,
397
+ ): number | undefined {
398
+ let total = 0;
399
+ for (const result of results) {
400
+ const value = select(result);
401
+ if (value === undefined) return undefined;
402
+ total += value;
403
+ }
404
+ return total;
405
+ }
406
+
407
+ export async function applySemanticJudge(
408
+ result: AnalysisResultSet,
409
+ query: string,
410
+ integration: SemanticJudgeIntegration | undefined,
411
+ signal?: AbortSignal,
412
+ ): Promise<AnalysisResultSet> {
413
+ const config = integration?.config;
414
+ if (!config || !config.enabled || !integration.runner) {
415
+ return { ...result, ...(config ? { semanticJudge: baseDetails(config) } : {}) };
416
+ }
417
+ const candidates = result.items.slice(0, config.maxCandidates).map((item, index) => ({
418
+ id: `candidate-${String(index + 1)}`,
419
+ excerpt: (item.excerpt ?? "").slice(0, MAX_CANDIDATE_CHARS),
420
+ }));
421
+ const initial = baseDetails(config);
422
+ initial.candidatesConsidered = candidates.length;
423
+ if (candidates.length === 0) {
424
+ return { ...result, semanticJudge: { ...initial, status: "complete", model: config.model } };
425
+ }
426
+ const startedAt = performance.now();
427
+ const execution = await judgeSemanticCandidateBatches(
428
+ integration.runner,
429
+ query,
430
+ createSemanticJudgeBatches(
431
+ candidates,
432
+ (batch) =>
433
+ new TextEncoder().encode(JSON.stringify(requestBody(query, batch, config.model)))
434
+ .byteLength,
435
+ ),
436
+ signal,
437
+ );
438
+ const judgments = new Map<number, SemanticJudgeJudgment>();
439
+ const judgmentModels = new Map<number, string>();
440
+ for (const success of execution.successes) {
441
+ for (const judgment of success.result.judgments) {
442
+ const candidateIndex = success.startIndex + judgment.candidateIndex;
443
+ judgments.set(candidateIndex, { ...judgment, candidateIndex });
444
+ judgmentModels.set(candidateIndex, success.result.model);
445
+ }
446
+ }
447
+ const counts: Record<string, number> = {};
448
+ for (const judgment of judgments.values())
449
+ counts[judgment.classification] = (counts[judgment.classification] ?? 0) + 1;
450
+ const rankedJudgedIndices = [...judgments.keys()].toSorted((left, right) => {
451
+ const leftJudgment = judgments.get(left)!;
452
+ const rightJudgment = judgments.get(right)!;
453
+ return (
454
+ CLASSIFICATION_PRIORITY[leftJudgment.classification] -
455
+ CLASSIFICATION_PRIORITY[rightJudgment.classification] ||
456
+ rightJudgment.probability - leftJudgment.probability ||
457
+ left - right
458
+ );
459
+ });
460
+ let rankedOffset = 0;
461
+ const reordered = result.items.map((original, index) => {
462
+ if (!judgments.has(index)) return original;
463
+ const rankedIndex = rankedJudgedIndices[rankedOffset++];
464
+ const item = rankedIndex === undefined ? undefined : result.items[rankedIndex];
465
+ const judgment = rankedIndex === undefined ? undefined : judgments.get(rankedIndex);
466
+ const model = rankedIndex === undefined ? undefined : judgmentModels.get(rankedIndex);
467
+ if (!item || !judgment || !model) return original;
468
+ return Object.assign({}, item, {
469
+ details: Object.assign({}, item.details, {
470
+ semanticJudge: {
471
+ classification: judgment.classification,
472
+ probability: judgment.probability,
473
+ ...(judgment.confidence === undefined ? {} : { confidence: judgment.confidence }),
474
+ model,
475
+ claim: SEMANTIC_JUDGE_NON_PROOF_CLAIM,
476
+ },
477
+ }),
478
+ });
479
+ });
480
+ const candidatesUnjudged = candidates.length - judgments.size;
481
+ const failureDetails = [
482
+ ...new Set(execution.failures.map((failure) => boundedError(failure.error))),
483
+ ];
484
+ const status =
485
+ execution.failures.length === 0 ? "complete" : judgments.size > 0 ? "partial" : "failed";
486
+ const reason =
487
+ execution.failures.length === 0
488
+ ? undefined
489
+ : status === "failed"
490
+ ? `Semantic judge unavailable; local semantic candidates retained without behavior classification: ${boundedError(failureDetails.join("; "))}`
491
+ : `Semantic judge partially unavailable; ${String(candidatesUnjudged)} candidates retained in local order without behavior classification across ${String(execution.failures.length)} failed batch${execution.failures.length === 1 ? "" : "es"}: ${boundedError(failureDetails.join("; "))}`;
492
+ const successfulResults = execution.successes.map((success) => success.result);
493
+ const models = [...new Set(successfulResults.map((judged) => judged.model))];
494
+ const model = models.length === 1 ? (models[0] ?? config.model) : config.model;
495
+ const inputTokens = optionalUsageTotal(successfulResults, (judged) => judged.inputTokens);
496
+ const outputTokens = optionalUsageTotal(successfulResults, (judged) => judged.outputTokens);
497
+ return {
498
+ ...result,
499
+ partial: result.partial || execution.failures.length > 0,
500
+ reasons: reason ? [...result.reasons, reason] : result.reasons,
501
+ items: reordered,
502
+ semanticJudge: {
503
+ ...initial,
504
+ status,
505
+ model,
506
+ judgedCandidates: judgments.size,
507
+ candidatesUnjudged,
508
+ batchesAttempted: execution.attempts,
509
+ batchesCompleted: execution.successes.length,
510
+ batchesFailed: execution.failures.length,
511
+ batchesSplit: execution.splits,
512
+ classificationCounts: counts,
513
+ ...(inputTokens === undefined ? {} : { inputTokens }),
514
+ ...(outputTokens === undefined ? {} : { outputTokens }),
515
+ elapsedMs: Math.round(performance.now() - startedAt),
516
+ ...(reason ? { reason } : {}),
517
+ },
518
+ };
519
+ }