sift-light 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/README.md +11 -4
- package/README.zh-CN.md +11 -4
- package/package.json +1 -1
- package/plugins/sift-light/.claude-plugin/plugin.json +1 -1
- package/plugins/sift-light/.codex-plugin/plugin.json +1 -1
- package/plugins/sift-light/.mcp.json +1 -1
- package/plugins/sift-light/concept-worker.mjs +39 -12
- package/plugins/sift-light/kimi.plugin.json +2 -2
- package/plugins/sift-light/omp-extension.mjs +558 -83
- package/plugins/sift-light/package.json +1 -1
- package/src/analysis-limits.ts +1 -1
- package/src/analysis-store.ts +6 -2
- package/src/analysis-types.ts +10 -1
- package/src/concept-inference.ts +44 -11
- package/src/concept-model.ts +2 -2
- package/src/concept-search.ts +12 -2
- package/src/concept-worker.mjs +39 -12
- package/src/concept-worker.ts +1 -1
- package/src/config-reader.ts +11 -3
- package/src/evidence-service.ts +117 -13
- package/src/file-discovery.ts +111 -17
- package/src/hybrid-search.ts +13 -0
- package/src/index.ts +4 -10
- package/src/inspect.ts +3 -0
- package/src/language-capability-definitions.ts +9 -0
- package/src/mcp-model-output.ts +47 -1
- package/src/mcp-semantic-judge.ts +19 -8
- package/src/mcp-server.mjs +754 -247
- package/src/mcp-server.ts +11 -13
- package/src/mcp.ts +2 -0
- package/src/model-error.ts +6 -16
- package/src/omp-index.ts +4 -6
- package/src/prompt-guidelines.ts +5 -5
- package/src/python-roles.ts +247 -0
- package/src/redaction.ts +9 -2
- package/src/request-aliases.ts +48 -0
- package/src/request-contract-catalog.ts +24 -7
- package/src/request-contract.ts +60 -6
- package/src/request.ts +3 -1
- package/src/runtime.ts +2 -2
- package/src/semantic-judge.ts +17 -3
- package/src/service.ts +31 -1
- package/src/session-summary.ts +4 -4
- package/src/source-inspection.ts +27 -2
- package/src/tool-schema.ts +18 -18
- package/src/tui/layout.ts +2 -2
- package/src/tui/renderers.ts +2 -2
- package/src/types.ts +4 -0
package/src/request-contract.ts
CHANGED
|
@@ -115,6 +115,38 @@ function schemaError(
|
|
|
115
115
|
);
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
function plainFilePatternRecovery(
|
|
119
|
+
mode: SiftLightMode,
|
|
120
|
+
input: Record<string, unknown>,
|
|
121
|
+
invalid: readonly string[],
|
|
122
|
+
): boolean {
|
|
123
|
+
return (
|
|
124
|
+
mode === "files" &&
|
|
125
|
+
invalid.includes("pattern") &&
|
|
126
|
+
input.query === undefined &&
|
|
127
|
+
typeof input.pattern === "string" &&
|
|
128
|
+
input.pattern.trim().length > 0 &&
|
|
129
|
+
input.pattern.length <= 256 &&
|
|
130
|
+
input.pattern.isWellFormed() &&
|
|
131
|
+
!/[\\^$.*+?()[\]{}|\r\n\0]/u.test(input.pattern)
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** `pattern: ".*"` in files mode means "every file": the exact repair omits it. */
|
|
136
|
+
function matchAllFilePatternRecovery(
|
|
137
|
+
mode: SiftLightMode,
|
|
138
|
+
input: Record<string, unknown>,
|
|
139
|
+
invalid: readonly string[],
|
|
140
|
+
): boolean {
|
|
141
|
+
return (
|
|
142
|
+
mode === "files" &&
|
|
143
|
+
invalid.includes("pattern") &&
|
|
144
|
+
input.query === undefined &&
|
|
145
|
+
typeof input.pattern === "string" &&
|
|
146
|
+
/^(?:\.\*|\.\+|\*+)$/u.test(input.pattern.trim())
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
118
150
|
function safeNextRequest(
|
|
119
151
|
input: Record<string, unknown>,
|
|
120
152
|
mode: SiftLightMode,
|
|
@@ -122,9 +154,12 @@ function safeNextRequest(
|
|
|
122
154
|
): Record<string, unknown> | undefined {
|
|
123
155
|
if (input.redact === true && containsSensitiveText(input)) return undefined;
|
|
124
156
|
const safe = new Set<string>(SAFE_DROP_FIELDS[mode] ?? []);
|
|
157
|
+
const plainFilePattern = plainFilePatternRecovery(mode, input, invalid);
|
|
158
|
+
if (plainFilePattern || matchAllFilePatternRecovery(mode, input, invalid)) safe.add("pattern");
|
|
125
159
|
if (invalid.some((field) => !safe.has(field))) return undefined;
|
|
126
160
|
const next: Record<string, unknown> = { ...input };
|
|
127
161
|
for (const field of invalid) delete next[field];
|
|
162
|
+
if (plainFilePattern) next.query = input.pattern;
|
|
128
163
|
if (selectorIssuesFor(next, mode).length > 0) return undefined;
|
|
129
164
|
// A copied request must remain within the transport's bounded line budget.
|
|
130
165
|
try {
|
|
@@ -165,7 +200,14 @@ function fieldsError(
|
|
|
165
200
|
// the signature of arguments shaped as a per-mode object, which the flat
|
|
166
201
|
// schema never advertises. Naming the accepted fields replaces an otherwise
|
|
167
202
|
// puzzling rejection with the rule and the valid names.
|
|
168
|
-
const
|
|
203
|
+
const nestedModeObject = invalid.includes(mode);
|
|
204
|
+
const flatRule = nestedModeObject
|
|
205
|
+
? `Fields are flat: pass them at the top level, not inside a per-mode object. mode=${mode} accepts: ${modeFields(mode).join(", ")}.`
|
|
206
|
+
: "";
|
|
207
|
+
const filesPatternRule =
|
|
208
|
+
mode === "files" && invalid.includes("pattern")
|
|
209
|
+
? "For filename or path discovery, put the literal name text in query; pattern is a content-search regex, so check any regex syntax before copying it."
|
|
210
|
+
: "";
|
|
169
211
|
return new RequestContractError(
|
|
170
212
|
{
|
|
171
213
|
code: "E_MODE_FIELDS",
|
|
@@ -174,17 +216,28 @@ function fieldsError(
|
|
|
174
216
|
recovery: nextRequest
|
|
175
217
|
? {
|
|
176
218
|
action: "retry",
|
|
177
|
-
reason:
|
|
219
|
+
reason: plainFilePatternRecovery(mode, input, invalid)
|
|
220
|
+
? "For filename discovery, move the plain text from pattern to query and copy nextRequest; all filters are preserved."
|
|
221
|
+
: matchAllFilePatternRecovery(mode, input, invalid)
|
|
222
|
+
? "A match-all pattern lists every file; omitting query does that, so copy nextRequest; all filters are preserved."
|
|
223
|
+
: `Remove only ${visibleFields} and copy the exact nextRequest; all other fields are preserved.`,
|
|
178
224
|
nextRequest,
|
|
179
225
|
}
|
|
180
226
|
: {
|
|
181
227
|
action: "manual",
|
|
182
|
-
reason:
|
|
228
|
+
reason: [
|
|
229
|
+
reason,
|
|
230
|
+
filesPatternRule ||
|
|
231
|
+
"Choose the mode explicitly or remove unsupported fields without changing the requested scope.",
|
|
232
|
+
flatRule,
|
|
233
|
+
]
|
|
234
|
+
.filter(Boolean)
|
|
235
|
+
.join(" "),
|
|
183
236
|
},
|
|
184
237
|
},
|
|
185
238
|
nextRequest
|
|
186
|
-
? `${reason};
|
|
187
|
-
: `${reason}; no semantics-preserving automatic request is available. ${
|
|
239
|
+
? `${reason}; ${plainFilePatternRecovery(mode, input, invalid) ? "move plain filename text to query" : "retry the exact nextRequest"}.`
|
|
240
|
+
: `${reason}; no semantics-preserving automatic request is available. ${filesPatternRule || flatRule || "Check the mode's accepted fields."}`,
|
|
188
241
|
);
|
|
189
242
|
}
|
|
190
243
|
|
|
@@ -422,7 +475,8 @@ export function validateRequestContract(input: SiftLightInput): void {
|
|
|
422
475
|
const allowed = new Set(MODE_FIELDS_BY_MODE[mode]);
|
|
423
476
|
if (mode === "inspect" && raw.sourceCursor !== undefined) {
|
|
424
477
|
allowed.clear();
|
|
425
|
-
|
|
478
|
+
// path is checked against the continuation's own source; it cannot redirect it.
|
|
479
|
+
for (const field of ["mode", "sourceCursor", "redact", "path"] as const) allowed.add(field);
|
|
426
480
|
}
|
|
427
481
|
if (
|
|
428
482
|
(mode === "auto" || mode === "summary" || mode === "matches") &&
|
package/src/request.ts
CHANGED
|
@@ -108,7 +108,9 @@ export function normalizeRequest(input: RawSearchInput): SearchRequest {
|
|
|
108
108
|
throw new SiftLightError("ignorePolicy must be respect or include");
|
|
109
109
|
const pattern = input.pattern;
|
|
110
110
|
if (pattern === undefined) {
|
|
111
|
-
throw new SiftLightError(
|
|
111
|
+
throw new SiftLightError(
|
|
112
|
+
'pattern is required when cursor is not provided; to list files use mode="files" (query optional), to read a known file use mode="inspect" with path',
|
|
113
|
+
);
|
|
112
114
|
}
|
|
113
115
|
|
|
114
116
|
const path = input.path?.replace(/^@/, "");
|
package/src/runtime.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SiftLightLocale } from "./config.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { SiftLightRequest, SiftLightSearchOptions, SiftLightService } from "./service.js";
|
|
3
3
|
import { type SessionSummarySnapshot, SessionSummary } from "./session-summary.js";
|
|
4
4
|
import type { ContextBudget, SiftLightResult } from "./types.js";
|
|
5
5
|
|
|
@@ -12,7 +12,7 @@ export class SiftLightRuntime {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
async search(
|
|
15
|
-
input:
|
|
15
|
+
input: SiftLightRequest,
|
|
16
16
|
cwd: string,
|
|
17
17
|
signal?: AbortSignal,
|
|
18
18
|
contextBudget?: ContextBudget,
|
package/src/semantic-judge.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_SEMANTIC_JUDGE_CONFIG,
|
|
3
|
+
type SemanticJudgeConfig,
|
|
4
|
+
type SiftLightConfig,
|
|
5
|
+
} from "./config-reader.js";
|
|
2
6
|
import {
|
|
3
7
|
SEMANTIC_JUDGE_CLASSIFICATIONS,
|
|
4
8
|
SEMANTIC_JUDGE_NON_PROOF_CLAIM,
|
|
@@ -134,8 +138,7 @@ function requestBody(
|
|
|
134
138
|
for (const candidate of candidates) {
|
|
135
139
|
questions[candidate.id] = {
|
|
136
140
|
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.",
|
|
141
|
+
instructions: `Classify only candidate ${candidate.id} in state.candidates for state.query. Judge that candidate's excerpt by its actual behavior, not by matching words or the other candidates.`,
|
|
139
142
|
criteria: {
|
|
140
143
|
"implementation-candidate":
|
|
141
144
|
"The excerpt appears to implement the requested behavior or its core decision/side effect.",
|
|
@@ -374,6 +377,17 @@ export function createSemanticJudgeIntegration(
|
|
|
374
377
|
}
|
|
375
378
|
return { config, runner: createJevRunner(config, key, fetcher) };
|
|
376
379
|
}
|
|
380
|
+
|
|
381
|
+
export function createConfiguredSemanticJudgeIntegration(
|
|
382
|
+
config: SiftLightConfig,
|
|
383
|
+
environment: NodeJS.ProcessEnv = process.env,
|
|
384
|
+
): SemanticJudgeIntegration {
|
|
385
|
+
const judge = config.semanticJudge ?? DEFAULT_SEMANTIC_JUDGE_CONFIG;
|
|
386
|
+
return config.vectorSearchEnabled === true
|
|
387
|
+
? createSemanticJudgeIntegration(judge, environment)
|
|
388
|
+
: createDisabledSemanticJudgeIntegration(judge);
|
|
389
|
+
}
|
|
390
|
+
|
|
377
391
|
function baseDetails(config: SemanticJudgeConfig): SemanticJudgeDetails {
|
|
378
392
|
return {
|
|
379
393
|
enabled: config.enabled,
|
package/src/service.ts
CHANGED
|
@@ -7,6 +7,18 @@ import { resolve } from "node:path";
|
|
|
7
7
|
import { CursorError, SiftLightError } from "./errors.js";
|
|
8
8
|
import { DISCOVERY_MODE_REQUIRED_ERROR } from "./discovery-errors.js";
|
|
9
9
|
import { validateRequestContract } from "./request-contract.js";
|
|
10
|
+
import { normalizeRequestAliases, type SiftLightRequest } from "./request-aliases.js";
|
|
11
|
+
|
|
12
|
+
export type { SiftLightRequest } from "./request-aliases.js";
|
|
13
|
+
|
|
14
|
+
/** Normalizations are part of the evidence: show them in text and details. */
|
|
15
|
+
function withRequestNotes(result: SiftLightResult, notes: readonly string[]): SiftLightResult {
|
|
16
|
+
if (notes.length === 0) return result;
|
|
17
|
+
return {
|
|
18
|
+
text: `${notes.map((note) => `[Request note: ${note}]`).join("\n")}\n${result.text}`,
|
|
19
|
+
details: { ...result.details, requestNotes: [...notes] },
|
|
20
|
+
};
|
|
21
|
+
}
|
|
10
22
|
import {
|
|
11
23
|
formatMatchMetadataPage,
|
|
12
24
|
formatMatchPage,
|
|
@@ -87,6 +99,7 @@ export interface SiftLightServiceOptions {
|
|
|
87
99
|
snapshots?: SnapshotStore;
|
|
88
100
|
summaryFileLimit?: number;
|
|
89
101
|
structure?: CodeStructureProvider;
|
|
102
|
+
vectorSearchEnabled?: boolean;
|
|
90
103
|
conceptSearch?: ConceptSearchRunner;
|
|
91
104
|
semanticJudge?: SemanticJudgeIntegration;
|
|
92
105
|
}
|
|
@@ -367,6 +380,7 @@ export class SiftLightService {
|
|
|
367
380
|
readonly #runRipgrep: RipgrepRunner;
|
|
368
381
|
readonly #snapshots: SnapshotStore;
|
|
369
382
|
readonly #summaryFileLimit: number;
|
|
383
|
+
readonly #vectorSearchEnabled: boolean;
|
|
370
384
|
readonly #capabilities = new LanguageCapabilityCatalog();
|
|
371
385
|
readonly #evidence: EvidenceService;
|
|
372
386
|
#lifecycle = new AbortController();
|
|
@@ -378,6 +392,7 @@ export class SiftLightService {
|
|
|
378
392
|
this.#runRipgrep = options.runRipgrep;
|
|
379
393
|
this.#snapshots = options.snapshots ?? new SnapshotStore();
|
|
380
394
|
this.#summaryFileLimit = options.summaryFileLimit ?? DEFAULT_SUMMARY_FILE_LIMIT;
|
|
395
|
+
this.#vectorSearchEnabled = options.vectorSearchEnabled ?? options.conceptSearch !== undefined;
|
|
381
396
|
this.#operations = new OperationLifecycle({ deadlineMs: resolveConceptTimeoutMs() });
|
|
382
397
|
this.#evidence = new EvidenceService(
|
|
383
398
|
this.#runRipgrep,
|
|
@@ -389,13 +404,28 @@ export class SiftLightService {
|
|
|
389
404
|
}
|
|
390
405
|
|
|
391
406
|
async search(
|
|
392
|
-
|
|
407
|
+
request: SiftLightRequest,
|
|
393
408
|
cwd: string,
|
|
394
409
|
signal?: AbortSignal,
|
|
395
410
|
options: SiftLightSearchOptions = {},
|
|
411
|
+
): Promise<SiftLightResult> {
|
|
412
|
+
const { input, notes } = normalizeRequestAliases(request);
|
|
413
|
+
return withRequestNotes(await this.#searchNormalized(input, cwd, signal, options), notes);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async #searchNormalized(
|
|
417
|
+
input: SiftLightInput,
|
|
418
|
+
cwd: string,
|
|
419
|
+
signal: AbortSignal | undefined,
|
|
420
|
+
options: SiftLightSearchOptions,
|
|
396
421
|
): Promise<SiftLightResult> {
|
|
397
422
|
validateRawSearchInput(input);
|
|
398
423
|
validateRequestContract(input);
|
|
424
|
+
if (!this.#vectorSearchEnabled && (input.mode === "concept" || input.mode === "hybrid")) {
|
|
425
|
+
throw new SiftLightError(
|
|
426
|
+
`${input.mode} search is disabled; set vectorSearchEnabled to true in sift-light.json and restart the host`,
|
|
427
|
+
);
|
|
428
|
+
}
|
|
399
429
|
let request: Promise<SiftLightResult>;
|
|
400
430
|
if (input.mode === "await" || input.mode === "cancel") {
|
|
401
431
|
request = this.#operationCommand(input, cwd, signal);
|
package/src/session-summary.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SiftLightLocale } from "./config.js";
|
|
2
2
|
import { SIFT_LIGHT_VERSION } from "./package-version.js";
|
|
3
|
-
import type {
|
|
3
|
+
import type { SiftLightRequest } from "./service.js";
|
|
4
4
|
import type { SiftLightResult } from "./types.js";
|
|
5
5
|
|
|
6
6
|
export const SESSION_STATUS_KEY = "sift-light_session";
|
|
@@ -13,7 +13,7 @@ export interface SessionSummarySnapshot {
|
|
|
13
13
|
failedCalls: number;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
function isNewQuery(input:
|
|
16
|
+
function isNewQuery(input: SiftLightRequest): boolean {
|
|
17
17
|
return (
|
|
18
18
|
input.cursor === undefined &&
|
|
19
19
|
input.sourceCursor === undefined &&
|
|
@@ -21,7 +21,7 @@ function isNewQuery(input: SiftLightInput): boolean {
|
|
|
21
21
|
);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
function wasAutomaticallyOrganized(input:
|
|
24
|
+
function wasAutomaticallyOrganized(input: SiftLightRequest, result: SiftLightResult): boolean {
|
|
25
25
|
const autoMode = input.mode === undefined || input.mode === "auto";
|
|
26
26
|
return autoMode && input.limit === undefined && result.details.summaryFilesShown !== undefined;
|
|
27
27
|
}
|
|
@@ -59,7 +59,7 @@ export class SessionSummary {
|
|
|
59
59
|
failedCalls: 0,
|
|
60
60
|
};
|
|
61
61
|
|
|
62
|
-
record(input:
|
|
62
|
+
record(input: SiftLightRequest, result: SiftLightResult): void {
|
|
63
63
|
if (!isNewQuery(input)) return;
|
|
64
64
|
this.#snapshot.queries += 1;
|
|
65
65
|
if (result.details.status === "complete") this.#snapshot.completeQueries += 1;
|
package/src/source-inspection.ts
CHANGED
|
@@ -33,6 +33,8 @@ export interface SourceInspectionTarget {
|
|
|
33
33
|
/** Raw source byte offset; retained-match focus is line-relative. */
|
|
34
34
|
absoluteFocus?: number;
|
|
35
35
|
focus?: number;
|
|
36
|
+
/** Structure already proven by the analysis that produced `range`. */
|
|
37
|
+
structure?: StructureDetails;
|
|
36
38
|
expectedRevision?: SourceRevision;
|
|
37
39
|
unverified?: boolean;
|
|
38
40
|
retry?: SiftLightInput;
|
|
@@ -121,7 +123,20 @@ async function prepare(
|
|
|
121
123
|
let range = target.range;
|
|
122
124
|
let details: StructureDetails = { status: "no-symbol" };
|
|
123
125
|
const language = syntaxLanguage(document.path);
|
|
124
|
-
if (
|
|
126
|
+
if (target.range && target.structure) {
|
|
127
|
+
document.checkRange(target.range);
|
|
128
|
+
const lines = {
|
|
129
|
+
startLine: document.lineAt(target.range.start),
|
|
130
|
+
endLine: document.lineAt(Math.max(target.range.start, target.range.end - 1)),
|
|
131
|
+
};
|
|
132
|
+
// Whole lines keep leading modifiers such as `export` or decorators readable.
|
|
133
|
+
range = document.lineRange(lines.startLine, lines.endLine);
|
|
134
|
+
details = {
|
|
135
|
+
...target.structure,
|
|
136
|
+
range: lines,
|
|
137
|
+
...(target.structure.symbol ? { symbol: { ...target.structure.symbol, range: lines } } : {}),
|
|
138
|
+
};
|
|
139
|
+
} else if (document.utf8 && language && language !== "go") {
|
|
125
140
|
const syntax = await access.syntax(document);
|
|
126
141
|
details = {
|
|
127
142
|
status:
|
|
@@ -205,7 +220,9 @@ async function prepare(
|
|
|
205
220
|
Math.min(document.lineStarts.length, target.line + 10),
|
|
206
221
|
);
|
|
207
222
|
const boundary: Exclude<SourceBoundary, "mixed"> = target.range
|
|
208
|
-
?
|
|
223
|
+
? target.structure
|
|
224
|
+
? "syntax"
|
|
225
|
+
: "requested-range"
|
|
209
226
|
: details.status === "available" && details.range
|
|
210
227
|
? "syntax"
|
|
211
228
|
: "line-window";
|
|
@@ -692,8 +709,16 @@ export async function continueSource(
|
|
|
692
709
|
cursor: string,
|
|
693
710
|
access: SourceAccess,
|
|
694
711
|
continuations: SourceContinuations,
|
|
712
|
+
expectedPath?: string,
|
|
695
713
|
): Promise<SiftLightResult> {
|
|
696
714
|
const state = continuations.resolve(cursor);
|
|
715
|
+
if (
|
|
716
|
+
expectedPath !== undefined &&
|
|
717
|
+
resolve(access.cwd, expectedPath.replace(/^@/, "")) !== resolve(access.cwd, state.source.path)
|
|
718
|
+
)
|
|
719
|
+
throw new SiftLightError(
|
|
720
|
+
`sourceCursor continues ${JSON.stringify(state.source.path)}, not ${JSON.stringify(expectedPath)}; copy the returned nextRequest exactly`,
|
|
721
|
+
);
|
|
697
722
|
const document = await access.load(state.source.path, state.source);
|
|
698
723
|
const page = sourcePage(document, state.remaining, MAX_RESULT_BYTES - 1400);
|
|
699
724
|
const next = continuations.advance(cursor, page.fragment);
|
package/src/tool-schema.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "./analysis-limits.js";
|
|
11
11
|
import {
|
|
12
12
|
MAX_CONTEXT_LINES,
|
|
13
|
+
MAX_INSPECT_REQUEST_TARGETS,
|
|
13
14
|
MAX_INSPECT_TARGETS,
|
|
14
15
|
MAX_PAGE_SIZE,
|
|
15
16
|
MAX_SELECTED_PATHS,
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
SIFT_LIGHT_MODES,
|
|
26
27
|
fieldGuidance,
|
|
27
28
|
} from "./request-contract.js";
|
|
29
|
+
import { MODE_ALIASES } from "./request-aliases.js";
|
|
28
30
|
|
|
29
31
|
function stringEnum<const Values extends readonly string[]>(
|
|
30
32
|
values: Values,
|
|
@@ -37,15 +39,15 @@ function stringEnum<const Values extends readonly string[]>(
|
|
|
37
39
|
});
|
|
38
40
|
}
|
|
39
41
|
|
|
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}`;
|
|
42
|
+
export const SIFT_LIGHT_DESCRIPTION = `Search and navigate code with bounded, verifiable evidence. Routine searches should use exact content, filenames or applicable structural modes first. Omitted mode is ordinary exact search and does not load the embedding model; concept/hybrid require vectorSearchEnabled:true in sift-light.json plus an installed model and may take tens of seconds on an uncached scope. 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
43
|
|
|
42
|
-
export const SIFT_LIGHT_MODEL_DESCRIPTION = `Bounded local evidence search. ${MODEL_USAGE_GUIDANCE}. Copy cursors; analysis is evidence, not proof.`;
|
|
44
|
+
export const SIFT_LIGHT_MODEL_DESCRIPTION = `Bounded local evidence search. Default exact search is model-free; concept/hybrid require vectorSearchEnabled:true in sift-light.json and a model. ${MODEL_USAGE_GUIDANCE}. Copy cursors; analysis is evidence, not proof.`;
|
|
43
45
|
|
|
44
46
|
export const siftLightSchema = Type.Object({
|
|
45
47
|
query: Type.Optional(
|
|
46
48
|
Type.String({
|
|
47
49
|
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.`,
|
|
50
|
+
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 vectorSearchEnabled:true in sift-light.json and an explicitly installed local model. A semantic judge is optional and remains disabled unless the active configuration explicitly enables it.`,
|
|
49
51
|
}),
|
|
50
52
|
),
|
|
51
53
|
scope: Type.Optional(
|
|
@@ -68,9 +70,9 @@ export const siftLightSchema = Type.Object({
|
|
|
68
70
|
),
|
|
69
71
|
allOf: Type.Optional(
|
|
70
72
|
Type.Array(Type.String({ maxLength: MAX_PATH_CHARACTERS }), {
|
|
71
|
-
minItems:
|
|
73
|
+
minItems: 1,
|
|
72
74
|
maxItems: 3,
|
|
73
|
-
description: `${fieldGuidance("allOf")}.
|
|
75
|
+
description: `${fieldGuidance("allOf")}. 1-3 distinct terms must occur in one file (default) or one function; one term lists the files or functions containing it.`,
|
|
74
76
|
}),
|
|
75
77
|
),
|
|
76
78
|
within: Type.Optional(
|
|
@@ -95,7 +97,7 @@ export const siftLightSchema = Type.Object({
|
|
|
95
97
|
{
|
|
96
98
|
minItems: 1,
|
|
97
99
|
description:
|
|
98
|
-
"Filter each single-pattern occurrence by syntax role (JS/TS/TSX/Go). Roles may be candidates, especially Go call/conversion
|
|
100
|
+
"Filter each single-pattern occurrence by syntax role (JS/TS/TSX/Go parsed; Python lexical: comment, string, code, declaration, import, call candidates). Roles may be candidates, especially Go call/conversion and Python calls. Cannot combine with allOf.",
|
|
99
101
|
},
|
|
100
102
|
),
|
|
101
103
|
),
|
|
@@ -151,7 +153,7 @@ export const siftLightSchema = Type.Object({
|
|
|
151
153
|
minItems: 1,
|
|
152
154
|
maxItems: MAX_SELECTED_PATHS,
|
|
153
155
|
description:
|
|
154
|
-
"Exact retained files to select together from a cursor. A new search accepts one path; split multiple roots into separate requests.",
|
|
156
|
+
"Exact retained files to select together from a cursor. A new search accepts one path; split multiple roots into separate requests. With mode=inspect and no cursor, opens each file from line 1 (same as targets with line 1).",
|
|
155
157
|
}),
|
|
156
158
|
),
|
|
157
159
|
glob: Type.Optional(
|
|
@@ -193,7 +195,7 @@ export const siftLightSchema = Type.Object({
|
|
|
193
195
|
ignorePolicy: Type.Optional(
|
|
194
196
|
stringEnum(["respect", "include"] as const, {
|
|
195
197
|
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.",
|
|
198
|
+
"respect (default) honors ignore rules and reports policy-filtered coverage when files are omitted, including mode=files. include searches or lists ignored files while still excluding .git internals and protected paths.",
|
|
197
199
|
}),
|
|
198
200
|
),
|
|
199
201
|
patterns: Type.Optional(
|
|
@@ -264,19 +266,19 @@ export const siftLightSchema = Type.Object({
|
|
|
264
266
|
Type.Integer({
|
|
265
267
|
minimum: 1,
|
|
266
268
|
maximum: MAX_PAGE_SIZE,
|
|
267
|
-
description: `${fieldGuidance("limit")}. Ordinary search
|
|
269
|
+
description: `${fieldGuidance("limit")}. Ordinary search: explicit detail-page match limit (max 100); mode=files: files per page (default 30). Normally omit to preserve automatic summarization; other analysis and inspect modes reject it.`,
|
|
268
270
|
}),
|
|
269
271
|
),
|
|
270
272
|
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
|
+
stringEnum([...SIFT_LIGHT_MODES, ...MODE_ALIASES], {
|
|
274
|
+
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} anyOf/allOf are field names, not modes; as mode values they are accepted aliases for omitting mode, disclosed in a request note.`,
|
|
273
275
|
}),
|
|
274
276
|
),
|
|
275
277
|
|
|
276
278
|
line: Type.Optional(
|
|
277
279
|
Type.Number({
|
|
278
280
|
description:
|
|
279
|
-
"1-indexed source line for path inspection/navigation. Omit with matchIndex, matchIndices or targets.",
|
|
281
|
+
"1-indexed source line for path inspection/navigation; path-only inspect starts at line 1. Omit with matchIndex, matchIndices or targets.",
|
|
280
282
|
}),
|
|
281
283
|
),
|
|
282
284
|
matchIndex: Type.Optional(
|
|
@@ -288,9 +290,8 @@ export const siftLightSchema = Type.Object({
|
|
|
288
290
|
matchIndices: Type.Optional(
|
|
289
291
|
Type.Array(Type.Integer({ minimum: 1 }), {
|
|
290
292
|
minItems: 1,
|
|
291
|
-
maxItems:
|
|
292
|
-
description:
|
|
293
|
-
"Inspect up to five visible match numbers together using the same cursor; mutually exclusive with matchIndex, path, line and targets.",
|
|
293
|
+
maxItems: MAX_INSPECT_REQUEST_TARGETS,
|
|
294
|
+
description: `Inspect visible match numbers together using the same cursor; mutually exclusive with matchIndex, path, line and targets. One response inspects ${String(MAX_INSPECT_TARGETS)}; the rest are returned as an exact nextRequest.`,
|
|
294
295
|
}),
|
|
295
296
|
),
|
|
296
297
|
targets: Type.Optional(
|
|
@@ -301,9 +302,8 @@ export const siftLightSchema = Type.Object({
|
|
|
301
302
|
}),
|
|
302
303
|
{
|
|
303
304
|
minItems: 1,
|
|
304
|
-
maxItems:
|
|
305
|
-
description:
|
|
306
|
-
"Inspect known path/line locations together without a cursor. The complete batch shares one 16 KiB response budget.",
|
|
305
|
+
maxItems: MAX_INSPECT_REQUEST_TARGETS,
|
|
306
|
+
description: `Inspect known path/line locations together without a cursor. One response inspects ${String(MAX_INSPECT_TARGETS)} within a shared 16 KiB budget; the rest are returned as an exact nextRequest.`,
|
|
307
307
|
},
|
|
308
308
|
),
|
|
309
309
|
),
|
package/src/tui/layout.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
2
|
import type { SiftLightLocale } from "../config.js";
|
|
3
|
-
import type {
|
|
3
|
+
import type { SiftLightRequest } from "../service.js";
|
|
4
4
|
import type { Dashboard } from "./dashboard.js";
|
|
5
5
|
|
|
6
6
|
import { candy, type SiftLightTheme } from "./palette.js";
|
|
@@ -254,7 +254,7 @@ export function renderDashboard(
|
|
|
254
254
|
}
|
|
255
255
|
|
|
256
256
|
export function renderSiftLightCallLines(
|
|
257
|
-
input:
|
|
257
|
+
input: SiftLightRequest,
|
|
258
258
|
locale: SiftLightLocale,
|
|
259
259
|
theme: SiftLightTheme,
|
|
260
260
|
width: number,
|
package/src/tui/renderers.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Component } from "@earendil-works/pi-tui";
|
|
2
2
|
import type { SiftLightLocale } from "../config.js";
|
|
3
|
-
import type {
|
|
3
|
+
import type { SiftLightRequest } from "../service.js";
|
|
4
4
|
import type { SiftLightDetails } from "../types.js";
|
|
5
5
|
import { dashboard } from "./dashboard.js";
|
|
6
6
|
import { fit, renderDashboard, renderSiftLightCallLines, type SiftLightTheme } from "./layout.js";
|
|
@@ -47,7 +47,7 @@ function failure(text: string, locale: SiftLightLocale): string {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
export function renderSiftLightCall(
|
|
50
|
-
input:
|
|
50
|
+
input: SiftLightRequest,
|
|
51
51
|
locale: SiftLightLocale,
|
|
52
52
|
theme: SiftLightTheme,
|
|
53
53
|
): Component {
|
package/src/types.ts
CHANGED
|
@@ -23,6 +23,8 @@ export const ESTIMATED_CHARACTERS_PER_TOKEN = 4;
|
|
|
23
23
|
export const DEFAULT_SUMMARY_FILE_LIMIT = 30;
|
|
24
24
|
export const MAX_SELECTED_PATHS = 20;
|
|
25
25
|
export const MAX_INSPECT_TARGETS = 5;
|
|
26
|
+
/** A request may name more targets; they are served in pages of MAX_INSPECT_TARGETS. */
|
|
27
|
+
export const MAX_INSPECT_REQUEST_TARGETS = 20;
|
|
26
28
|
export const MAX_DISPLAYED_OCCURRENCES = 20;
|
|
27
29
|
export const MAX_STORED_MATCHES = 50_000;
|
|
28
30
|
export const MAX_STORED_OCCURRENCES = 200_000;
|
|
@@ -310,6 +312,8 @@ export interface SiftLightDetails {
|
|
|
310
312
|
source?: SourceExcerptDetails;
|
|
311
313
|
inspections?: InspectBatchItemDetails[];
|
|
312
314
|
scope?: SearchScopeDetails;
|
|
315
|
+
/** Disclosed request normalizations, such as a field name sent as mode. */
|
|
316
|
+
requestNotes?: string[];
|
|
313
317
|
redactedCount?: number;
|
|
314
318
|
redactionRequested?: boolean;
|
|
315
319
|
redactionApplied?: boolean;
|