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,321 @@
1
+ import { Type } from "typebox";
2
+ import {
3
+ MAX_ANY_OF_TOTAL_TERMS,
4
+ MAX_ANY_OF_TERMS,
5
+ MAX_CONFIGURABLE_STRUCTURE_FILES,
6
+ DEFAULT_HYBRID_CONCEPT_LIMIT,
7
+ MAX_HYBRID_CONCEPT_LIMIT,
8
+ MAX_LITERAL_TERM_BYTES,
9
+ MIN_ANY_OF_TERMS,
10
+ } from "./analysis-limits.js";
11
+ import {
12
+ MAX_CONTEXT_LINES,
13
+ MAX_INSPECT_TARGETS,
14
+ MAX_PAGE_SIZE,
15
+ MAX_SELECTED_PATHS,
16
+ MAX_FILE_FILTER_ITEMS,
17
+ MAX_PATH_CHARACTERS,
18
+ MAX_PATTERN_CHARACTERS,
19
+ MAX_AUDIT_LITERAL_CHARACTERS,
20
+ } from "./types.js";
21
+ import {
22
+ MODE_CONTRACT_DESCRIPTION,
23
+ MODEL_USAGE_GUIDANCE,
24
+ REQUEST_USAGE_GUIDANCE,
25
+ SIFT_LIGHT_MODES,
26
+ fieldGuidance,
27
+ } from "./request-contract.js";
28
+
29
+ function stringEnum<const Values extends readonly string[]>(
30
+ values: Values,
31
+ options?: { description?: string },
32
+ ) {
33
+ return Type.Unsafe<Values[number]>({
34
+ type: "string",
35
+ enum: values,
36
+ ...(options?.description ? { description: options.description } : {}),
37
+ });
38
+ }
39
+
40
+ export const SIFT_LIGHT_DESCRIPTION = `Search and navigate code with bounded, verifiable evidence. Ordinary pattern searches use auto detail/summary; pattern is regex by default and literal=true matches source text exactly. A path selects an existing exact file or root; use mode=files with query to discover an unknown name. scope=strict prevents zero-result path expansion and wholeWord requires word boundaries. mode=capabilities returns a compact names-only project language inventory and the modes available for each detected language; capability providers are loaded only when the requested analysis runs. It never starts a parser, compiler, model or language server. mode=concept accepts a natural-language query, path and source filters; mode=hybrid uses one natural-language query for exact and local concept evidence, ranks exact evidence first, and retains a bounded semantic supplement. An explicitly enabled semantic judge may classify hybrid candidates, but it is disabled by default and never turns classification into a runtime proof. Slow concept/hybrid requests return status=waiting or running with operationId, progress, and an exact nextRequest using mode=await; copy that request unchanged to continue the same computation. Await expiry never downgrades evidence to literal-only or partial, and final results remain stable for the operation retention window. mode=cancel explicitly stops one operation. allOf and anyOf are explicit literal variants and cannot be mixed with pattern/literal; limit and context are output intent and are never silently dropped. modifiedAfter/modifiedBefore filter worktree files by inclusive/exclusive modification-time bounds in Unix milliseconds. structure requires a nonempty AST pattern and JS/TS/TSX/Go sources; lang is not a field. Outline uses a concrete source file path (not a directory) or retained cursor+matchIndex and follows declared syntax capabilities. imports/tests return bounded static module and related-test candidates without proving runtime execution. validate checks saved source evidence against its recorded origin. Partial coverage stays explicit. ${REQUEST_USAGE_GUIDANCE}`;
41
+
42
+ export const SIFT_LIGHT_MODEL_DESCRIPTION = `Bounded local evidence search. ${MODEL_USAGE_GUIDANCE}. Copy cursors; analysis is evidence, not proof.`;
43
+
44
+ export const siftLightSchema = Type.Object({
45
+ query: Type.Optional(
46
+ Type.String({
47
+ maxLength: 256,
48
+ description: `${fieldGuidance("query")}. Hybrid uses the same query as exact literal text and as the local concept query. Discovery modes preserve their requested path. Concept and hybrid require an explicitly installed local model. A semantic judge is optional and remains disabled unless the active configuration explicitly enables it.`,
49
+ }),
50
+ ),
51
+ scope: Type.Optional(
52
+ stringEnum(["strict", "expand"] as const, {
53
+ description: `${fieldGuidance("scope")}; expand (default) retries ordinary content search from project cwd. Applies to ordinary, multi-term and role searches.`,
54
+ }),
55
+ ),
56
+ wholeWord: Type.Optional(
57
+ Type.Boolean({
58
+ description:
59
+ "Single-pattern search only: require ripgrep Unicode word boundaries around the match. Works with regex or literal=true.",
60
+ }),
61
+ ),
62
+ anyOf: Type.Optional(
63
+ Type.Array(Type.String({ maxLength: MAX_LITERAL_TERM_BYTES }), {
64
+ minItems: MIN_ANY_OF_TERMS,
65
+ maxItems: MAX_ANY_OF_TOTAL_TERMS,
66
+ description: `${fieldGuidance("anyOf")}. ${String(MIN_ANY_OF_TERMS)}-${String(MAX_ANY_OF_TOTAL_TERMS)} distinct case-sensitive single-line terms, at most ${String(MAX_LITERAL_TERM_BYTES)} UTF-8 bytes each. Requests above ${String(MAX_ANY_OF_TERMS)} terms are split into version-checked chunks and merged. Returns every retained occurrence attributed to its term.`,
67
+ }),
68
+ ),
69
+ allOf: Type.Optional(
70
+ Type.Array(Type.String({ maxLength: MAX_PATH_CHARACTERS }), {
71
+ minItems: 2,
72
+ maxItems: 3,
73
+ description: `${fieldGuidance("allOf")}. 2-3 distinct terms must occur in one file (default) or one function.`,
74
+ }),
75
+ ),
76
+ within: Type.Optional(
77
+ stringEnum(["file", "function"] as const, {
78
+ description:
79
+ "Only valid with allOf; omit for ordinary single-pattern searches. function requires JS/TS/TSX and counts only that implementation's own code, excluding nested callbacks, strings/comments/types. Not proof of a shared execution path.",
80
+ }),
81
+ ),
82
+ roles: Type.Optional(
83
+ Type.Array(
84
+ stringEnum([
85
+ "declaration",
86
+ "call",
87
+ "import",
88
+ "export",
89
+ "comment",
90
+ "string",
91
+ "jsx-text",
92
+ "code",
93
+ "unknown",
94
+ ] as const),
95
+ {
96
+ minItems: 1,
97
+ description:
98
+ "Filter each single-pattern occurrence by syntax role (JS/TS/TSX/Go). Roles may be candidates, especially Go call/conversion ambiguity. Cannot combine with allOf.",
99
+ },
100
+ ),
101
+ ),
102
+ changes: Type.Optional(
103
+ Type.Object({
104
+ base: Type.Optional(
105
+ Type.String({
106
+ description: "Git base commit/ref; default HEAD, pinned to a commit at query time.",
107
+ }),
108
+ ),
109
+ target: Type.Optional(
110
+ Type.String({
111
+ description:
112
+ "Optional target commit/ref. Omit for final working-tree contents including unignored untracked files, not just the staged index.",
113
+ }),
114
+ ),
115
+ scope: stringEnum(["files", "lines"] as const, {
116
+ description:
117
+ "Search changed files or only changed lines. With allOf every term must lie on the chosen side's changed lines.",
118
+ }),
119
+ side: stringEnum(["new", "old"] as const, {
120
+ description:
121
+ "Choose final/new content or deleted/old content. Historical inspect and continuation remain bound to that commit/blob.",
122
+ }),
123
+ }),
124
+ ),
125
+ sourceCursor: Type.Optional(
126
+ Type.String({
127
+ description:
128
+ "Missing-source continuation token. Copy nextRequest exactly: mode=inspect plus sourceCursor only. Same token replays the same page; changed or expired sources fail clearly.",
129
+ }),
130
+ ),
131
+ symbol: Type.Optional(
132
+ Type.String({
133
+ description:
134
+ "Syntax name for outline/imports/tests; use a concrete source file and line to narrow repeated names.",
135
+ }),
136
+ ),
137
+ pattern: Type.Optional(
138
+ Type.String({
139
+ maxLength: MAX_PATTERN_CHARACTERS,
140
+ description: `${fieldGuidance("pattern")}. mode=structure requires a nonempty ast-grep code pattern for JS/TS/TSX/Go (no lang field), at most 4 KiB, including $NAME and $$$ARGS metavariables; no regex/literal options. Omit for discovery, syntax navigation, inspection and cursors.`,
141
+ }),
142
+ ),
143
+ path: Type.Optional(
144
+ Type.String({
145
+ maxLength: MAX_PATH_CHARACTERS,
146
+ description: `${fieldGuidance("path")}. A zero-result content search expands from cwd unless scope=strict. outline and compiler navigation require a concrete file, not a directory. Compiler navigation stays within admitted workspace sources. Absolute paths and .. traversal may resolve outside cwd, except protected external system areas and .git internals; Git changes mode remains cwd-scoped.`,
147
+ }),
148
+ ),
149
+ paths: Type.Optional(
150
+ Type.Array(Type.String(), {
151
+ minItems: 1,
152
+ maxItems: MAX_SELECTED_PATHS,
153
+ description:
154
+ "Exact retained files to select together from a cursor. A new search accepts one path; split multiple roots into separate requests.",
155
+ }),
156
+ ),
157
+ glob: Type.Optional(
158
+ Type.Union(
159
+ [
160
+ Type.String({ maxLength: MAX_PATH_CHARACTERS }),
161
+ Type.Array(Type.String({ maxLength: MAX_PATH_CHARACTERS }), {
162
+ maxItems: MAX_FILE_FILTER_ITEMS,
163
+ }),
164
+ ],
165
+ {
166
+ description: "Include glob or globs, for example '*.ts' or 'src/**'.",
167
+ },
168
+ ),
169
+ ),
170
+ exclude: Type.Optional(
171
+ Type.Union(
172
+ [
173
+ Type.String({ maxLength: MAX_PATH_CHARACTERS }),
174
+ Type.Array(Type.String({ maxLength: MAX_PATH_CHARACTERS }), {
175
+ maxItems: MAX_FILE_FILTER_ITEMS,
176
+ }),
177
+ ],
178
+ {
179
+ description:
180
+ "Exclude file/path globs (not content negation); applied after include globs. A leading ! is optional.",
181
+ },
182
+ ),
183
+ ),
184
+ literal: Type.Optional(Type.Boolean({ description: fieldGuidance("literal") })),
185
+ ignoreCase: Type.Optional(
186
+ Type.Boolean({
187
+ description: "true for insensitive, false for sensitive; omitted uses smart-case.",
188
+ }),
189
+ ),
190
+ hidden: Type.Optional(
191
+ Type.Boolean({ description: "Search hidden files (default true; .git is always excluded)." }),
192
+ ),
193
+ ignorePolicy: Type.Optional(
194
+ stringEnum(["respect", "include"] as const, {
195
+ description:
196
+ "respect (default) honors ignore rules and reports policy-filtered coverage when files are omitted. include searches ignored files while still excluding .git internals and protected paths.",
197
+ }),
198
+ ),
199
+ patterns: Type.Optional(
200
+ Type.Array(
201
+ Type.Object(
202
+ {
203
+ id: Type.String({ minLength: 1, maxLength: 64 }),
204
+ literal: Type.String({
205
+ minLength: 1,
206
+ maxLength: MAX_AUDIT_LITERAL_CHARACTERS,
207
+ description: `Single-line exact text, limited to ${String(MAX_AUDIT_LITERAL_CHARACTERS)} characters and UTF-8 bytes.`,
208
+ }),
209
+ },
210
+ { additionalProperties: false },
211
+ ),
212
+ {
213
+ minItems: 1,
214
+ maxItems: 32,
215
+ description:
216
+ "Named exact-literal checks for mode=audit. Each finding is present, absent_with_complete_coverage, or unknown.",
217
+ },
218
+ ),
219
+ ),
220
+ redact: Type.Optional(
221
+ Type.Boolean({
222
+ description:
223
+ "Optional display-only masking for credential-like values and private-key bodies. Default false. It never changes searched files, admitted matches, counts, or cursor completeness.",
224
+ }),
225
+ ),
226
+ modifiedAfter: Type.Optional(
227
+ Type.Integer({
228
+ minimum: 0,
229
+ maximum: Number.MAX_SAFE_INTEGER,
230
+ description:
231
+ "Worktree modification-time lower bound, inclusive, as a Unix timestamp in milliseconds. Not valid with Git changes.",
232
+ }),
233
+ ),
234
+ modifiedBefore: Type.Optional(
235
+ Type.Integer({
236
+ minimum: 0,
237
+ maximum: Number.MAX_SAFE_INTEGER,
238
+ description:
239
+ "Worktree modification-time upper bound, exclusive, as a Unix timestamp in milliseconds. Not valid with Git changes.",
240
+ }),
241
+ ),
242
+ maxFilesToParse: Type.Optional(
243
+ Type.Integer({
244
+ minimum: 1,
245
+ maximum: MAX_CONFIGURABLE_STRUCTURE_FILES,
246
+ description: `Advanced hard ceiling for source files admitted by one analysis request (max ${String(MAX_CONFIGURABLE_STRUCTURE_FILES)}). Concept and hybrid automatically process the requested scope in bounded batches when omitted; other structural modes default to 200. Candidate discovery still searches the full requested scope.`,
247
+ }),
248
+ ),
249
+ conceptLimit: Type.Optional(
250
+ Type.Integer({
251
+ minimum: 1,
252
+ maximum: MAX_HYBRID_CONCEPT_LIMIT,
253
+ description: `mode=hybrid only: retain the top semantic candidates after overlap deduplication (default ${String(DEFAULT_HYBRID_CONCEPT_LIMIT)}, max ${String(MAX_HYBRID_CONCEPT_LIMIT)}). Literal evidence has an independent retention budget and is never displaced by this limit.`,
254
+ }),
255
+ ),
256
+ context: Type.Optional(
257
+ Type.Integer({
258
+ minimum: 0,
259
+ maximum: MAX_CONTEXT_LINES,
260
+ description: `${fieldGuidance("context")}. New search only: nearby lines (0-20). MUST be omitted for inspect, which selects its own bounded source window.`,
261
+ }),
262
+ ),
263
+ limit: Type.Optional(
264
+ Type.Integer({
265
+ minimum: 1,
266
+ maximum: MAX_PAGE_SIZE,
267
+ description: `${fieldGuidance("limit")}. Ordinary search only: explicit detail-page match limit (max 100). Normally omit to preserve automatic summarization; analysis and inspect modes reject it.`,
268
+ }),
269
+ ),
270
+ mode: Type.Optional(
271
+ stringEnum(SIFT_LIGHT_MODES, {
272
+ description: `Ordinary search defaults to auto; summary/matches request explicit pages. capabilities returns a compact names-only project inventory and per-language supported modes without loading providers. files uses query, structure uses an AST pattern, concept uses a required natural-language query, and hybrid uses one query for exact literal plus concept evidence in a single snapshot. validate rechecks saved search or analysis sources against their recorded origin. await waits for an existing long-running concept or hybrid operation without restarting it; cancel explicitly cancels one and waits for owned cleanup. Copy the returned nextRequest exactly and do not repeat the original query. Waiting is an operation state, not evidence. Validation details report the requested scope, comparison target, coverage, and freshness as current, stale, or unknown; partial coverage is retained during validation. inspect/outline/imports/tests retain their documented location selectors. Syntax results are static evidence; concept and related-test results remain candidates. ${MODE_CONTRACT_DESCRIPTION}`,
273
+ }),
274
+ ),
275
+
276
+ line: Type.Optional(
277
+ Type.Number({
278
+ description:
279
+ "1-indexed source line for path inspection/navigation. Omit with matchIndex, matchIndices or targets.",
280
+ }),
281
+ ),
282
+ matchIndex: Type.Optional(
283
+ Type.Number({
284
+ description:
285
+ "1-based retained match index for cursor-scoped inspect; replaces path and line.",
286
+ }),
287
+ ),
288
+ matchIndices: Type.Optional(
289
+ Type.Array(Type.Integer({ minimum: 1 }), {
290
+ minItems: 1,
291
+ maxItems: MAX_INSPECT_TARGETS,
292
+ description:
293
+ "Inspect up to five visible match numbers together using the same cursor; mutually exclusive with matchIndex, path, line and targets.",
294
+ }),
295
+ ),
296
+ targets: Type.Optional(
297
+ Type.Array(
298
+ Type.Object({
299
+ path: Type.String({ maxLength: MAX_PATH_CHARACTERS }),
300
+ line: Type.Integer({ minimum: 1 }),
301
+ }),
302
+ {
303
+ minItems: 1,
304
+ maxItems: MAX_INSPECT_TARGETS,
305
+ description:
306
+ "Inspect known path/line locations together without a cursor. The complete batch shares one 16 KiB response budget.",
307
+ },
308
+ ),
309
+ ),
310
+ cursor: Type.Optional(
311
+ Type.String({ description: "Opaque cursor from a previous stable search snapshot." }),
312
+ ),
313
+ operationId: Type.Optional(
314
+ Type.String({
315
+ minLength: 1,
316
+ maxLength: 128,
317
+ description:
318
+ "Operation handle returned by a waiting concept/hybrid result. Required with mode=await or mode=cancel; copy it exactly and do not start a new query.",
319
+ }),
320
+ ),
321
+ });
@@ -0,0 +1,107 @@
1
+ import type { SiftLightDetails } from "../types.js";
2
+ import { recognizeSiftLightResult, type SummaryRow } from "./presentation.js";
3
+
4
+ export interface Dashboard {
5
+ total: number;
6
+ files: number;
7
+ unit: "matches" | "items" | "files" | "locations";
8
+ rows: SummaryRow[];
9
+ pageOnly: boolean;
10
+ partial: boolean;
11
+ more: boolean;
12
+ stored: number;
13
+ inspection: boolean;
14
+ unavailable: number;
15
+ }
16
+
17
+ function validCount(value: number): boolean {
18
+ return Number.isSafeInteger(value) && value >= 0;
19
+ }
20
+
21
+ function grouped(paths: string[]): SummaryRow[] {
22
+ const counts = new Map<string, number>();
23
+ for (const path of paths) counts.set(path, (counts.get(path) ?? 0) + 1);
24
+ return Array.from(counts, ([path, matches]) => ({ path, matches }));
25
+ }
26
+
27
+ /** Derive human statistics without copying any source, labels or protocol instructions. */
28
+ export function dashboard(
29
+ text: string,
30
+ details: SiftLightDetails | undefined,
31
+ ): Dashboard | undefined {
32
+ if (!details || details.version !== 1 || !["complete", "partial"].includes(details.status))
33
+ return undefined;
34
+ if (
35
+ ![
36
+ details.totalMatches,
37
+ details.storedMatches,
38
+ details.totalFiles,
39
+ details.returnedMatches,
40
+ ].every(validCount)
41
+ )
42
+ return undefined;
43
+ const statistics = details.analysis?.statistics ?? details.statistics;
44
+ const inspection = details.mode === "inspect";
45
+ let total = details.analysis?.totalItems ?? details.totalMatches;
46
+ let rows: SummaryRow[] = [];
47
+ let pageOnly = false;
48
+ let unavailable = 0;
49
+ const presentation = recognizeSiftLightResult(text, details);
50
+ if (presentation?.kind === "summary") {
51
+ rows = presentation.rows;
52
+ } else if (statistics && !inspection) {
53
+ rows = statistics.topFiles.map(({ label, count }) => ({ path: label, matches: count }));
54
+ } else if (details.analysis) {
55
+ rows = grouped(details.analysis.items.map((item) => item.path));
56
+ pageOnly = true;
57
+ } else if (inspection) {
58
+ const items = details.inspections;
59
+ const paths =
60
+ items?.flatMap((item) => (item.path ? [item.path] : [])) ??
61
+ details.sourceBlocks?.map((block) => block.path) ??
62
+ (details.source?.reference ? [details.source.reference.path] : []);
63
+ if (paths.length === 0) {
64
+ if (presentation?.kind === "inspect") paths.push(presentation.target.replace(/:\d+$/, ""));
65
+ }
66
+ rows = grouped(paths);
67
+ total = items?.length ?? paths.length;
68
+ unavailable =
69
+ items?.filter((item) => item.status !== "returned").length ??
70
+ (details.status === "partial" ? total : 0);
71
+ } else {
72
+ if (presentation?.kind === "matches") {
73
+ const paths: string[] = [];
74
+ let path: string | undefined;
75
+ for (const line of presentation.bodyLines) {
76
+ if (line && !/^\s/.test(line)) path = line;
77
+ else if (path && /^ \d+:/.test(line)) paths.push(path);
78
+ }
79
+ rows = grouped(paths);
80
+ pageOnly = true;
81
+ }
82
+ }
83
+ if (!validCount(total) || rows.some((row) => !validCount(row.matches))) return undefined;
84
+ return {
85
+ total,
86
+ files: details.totalFiles,
87
+ unit: inspection
88
+ ? "locations"
89
+ : details.analysis?.unit === "files"
90
+ ? "files"
91
+ : details.analysis
92
+ ? "items"
93
+ : "matches",
94
+ rows,
95
+ pageOnly,
96
+ inspection,
97
+ unavailable,
98
+ partial: details.status === "partial" || !details.snapshotComplete || unavailable > 0,
99
+ more: Boolean(
100
+ details.cursor ||
101
+ details.nextRequest ||
102
+ details.source?.nextRequest ||
103
+ details.analysis?.termCountsNextRequest,
104
+ ),
105
+ stored: details.storedMatches,
106
+ };
107
+ }