@zokizuan/satori-mcp 6.1.0 → 6.2.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/dist/core/backend-diagnostics.d.ts +17 -0
- package/dist/core/backend-diagnostics.js +44 -0
- package/dist/core/call-graph.d.ts +1 -0
- package/dist/core/call-graph.js +1 -1
- package/dist/core/handlers.d.ts +2 -0
- package/dist/core/handlers.js +37 -6
- package/dist/core/manage-indexing-handlers.js +34 -4
- package/dist/core/navigation-handlers.js +0 -1
- package/dist/core/python-call-fallback.js +88 -6
- package/dist/core/relationship-backed-call-graph.d.ts +2 -2
- package/dist/core/relationship-backed-call-graph.js +83 -21
- package/dist/core/search-exact-fast-path.js +1 -1
- package/dist/core/search-execution.d.ts +4 -1
- package/dist/core/search-execution.js +21 -7
- package/dist/core/search-frontdoor.js +1 -0
- package/dist/core/search-query-planning.js +10 -11
- package/dist/core/search-query-support.d.ts +1 -1
- package/dist/core/search-query-support.js +2 -2
- package/dist/core/search-response-envelopes.d.ts +2 -2
- package/dist/core/search-result-finalization.js +6 -3
- package/dist/core/search-types.d.ts +5 -2
- package/dist/core/snapshot.d.ts +6 -0
- package/dist/core/snapshot.js +11 -0
- package/dist/core/sync.d.ts +7 -1
- package/dist/core/sync.js +184 -2
- package/dist/core/tool-response-builders.js +1 -0
- package/dist/tools/search_codebase.js +7 -0
- package/package.json +2 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { EmbeddingProviderErrorCode } from "@zokizuan/satori-core";
|
|
1
2
|
export type VectorBackendDiagnosticCode = "ZILLIZ_CLUSTER_STOPPED" | "VECTOR_BACKEND_AUTH_FAILED" | "VECTOR_BACKEND_UNREACHABLE" | "VECTOR_BACKEND_TIMEOUT" | "VECTOR_BACKEND_CONNECTION_CLOSED";
|
|
2
3
|
export interface VectorBackendDiagnostic {
|
|
3
4
|
code: VectorBackendDiagnosticCode;
|
|
@@ -11,5 +12,21 @@ export interface VectorBackendDiagnostic {
|
|
|
11
12
|
};
|
|
12
13
|
};
|
|
13
14
|
}
|
|
15
|
+
export type SemanticPassFailureDiagnostic = {
|
|
16
|
+
passId: "primary" | "expanded";
|
|
17
|
+
errorName: "Error" | "TypeError" | "RangeError" | "ReferenceError" | "SyntaxError" | "AggregateError" | "EmbeddingProviderError" | "NonErrorThrow";
|
|
18
|
+
code: VectorBackendDiagnosticCode | EmbeddingProviderErrorCode | "UNCLASSIFIED_SEARCH_PASS_FAILURE";
|
|
19
|
+
classifier: "embedding_provider" | "vector_backend" | "unclassified";
|
|
20
|
+
retryable?: boolean;
|
|
21
|
+
};
|
|
22
|
+
export declare function buildSemanticPassFailureDiagnostic(input: {
|
|
23
|
+
passId: SemanticPassFailureDiagnostic["passId"];
|
|
24
|
+
error: unknown;
|
|
25
|
+
embeddingDiagnostic: {
|
|
26
|
+
code: EmbeddingProviderErrorCode;
|
|
27
|
+
retryable: boolean;
|
|
28
|
+
} | null;
|
|
29
|
+
vectorDiagnostic: VectorBackendDiagnostic | null;
|
|
30
|
+
}): SemanticPassFailureDiagnostic;
|
|
14
31
|
export declare function classifyVectorBackendError(error: unknown): VectorBackendDiagnostic | null;
|
|
15
32
|
//# sourceMappingURL=backend-diagnostics.d.ts.map
|
|
@@ -1,3 +1,47 @@
|
|
|
1
|
+
const ALLOWED_SEARCH_PASS_ERROR_NAMES = new Set([
|
|
2
|
+
"Error",
|
|
3
|
+
"TypeError",
|
|
4
|
+
"RangeError",
|
|
5
|
+
"ReferenceError",
|
|
6
|
+
"SyntaxError",
|
|
7
|
+
"AggregateError",
|
|
8
|
+
"EmbeddingProviderError",
|
|
9
|
+
]);
|
|
10
|
+
function classifySearchPassErrorName(error) {
|
|
11
|
+
if (!(error instanceof Error)) {
|
|
12
|
+
return "NonErrorThrow";
|
|
13
|
+
}
|
|
14
|
+
return ALLOWED_SEARCH_PASS_ERROR_NAMES.has(error.name)
|
|
15
|
+
? error.name
|
|
16
|
+
: "Error";
|
|
17
|
+
}
|
|
18
|
+
export function buildSemanticPassFailureDiagnostic(input) {
|
|
19
|
+
const errorName = classifySearchPassErrorName(input.error);
|
|
20
|
+
if (input.embeddingDiagnostic) {
|
|
21
|
+
return {
|
|
22
|
+
passId: input.passId,
|
|
23
|
+
errorName,
|
|
24
|
+
code: input.embeddingDiagnostic.code,
|
|
25
|
+
classifier: "embedding_provider",
|
|
26
|
+
retryable: input.embeddingDiagnostic.retryable,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (input.vectorDiagnostic) {
|
|
30
|
+
return {
|
|
31
|
+
passId: input.passId,
|
|
32
|
+
errorName,
|
|
33
|
+
code: input.vectorDiagnostic.code,
|
|
34
|
+
classifier: "vector_backend",
|
|
35
|
+
retryable: input.vectorDiagnostic.hints.backend.retryable,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
passId: input.passId,
|
|
40
|
+
errorName,
|
|
41
|
+
code: "UNCLASSIFIED_SEARCH_PASS_FAILURE",
|
|
42
|
+
classifier: "unclassified",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
1
45
|
function collectErrorText(value, depth = 0) {
|
|
2
46
|
if (depth > 2 || value == null) {
|
|
3
47
|
return "";
|
|
@@ -84,6 +84,7 @@ export type CallGraphQueryResponse = CallGraphResponseSupported | CallGraphRespo
|
|
|
84
84
|
export interface CallGraphDeltaPolicy {
|
|
85
85
|
shouldRebuild(changedFiles: string[]): boolean;
|
|
86
86
|
}
|
|
87
|
+
export declare const DEFAULT_CALL_GRAPH_TEST_REFERENCE_LIMIT = 50;
|
|
87
88
|
export declare class SupportedSourceDeltaPolicy implements CallGraphDeltaPolicy {
|
|
88
89
|
shouldRebuild(changedFiles: string[]): boolean;
|
|
89
90
|
}
|
package/dist/core/call-graph.js
CHANGED
|
@@ -9,7 +9,7 @@ const QUERY_SUPPORTED_EXTENSIONS = new Set(getSupportedExtensionsForCapability('
|
|
|
9
9
|
const QUERY_SUPPORTED_LANGUAGE_IDS = getSupportedLanguageIdsForCapability('callGraphQuery');
|
|
10
10
|
const DEFAULT_IGNORE_PATTERNS = ['**/node_modules/**', '**/.git/**', '**/dist/**', '**/build/**', '**/coverage/**', '**/.next/**'];
|
|
11
11
|
const DEFAULT_CALL_GRAPH_NOTE_LIMIT = 200;
|
|
12
|
-
const DEFAULT_CALL_GRAPH_TEST_REFERENCE_LIMIT = 50;
|
|
12
|
+
export const DEFAULT_CALL_GRAPH_TEST_REFERENCE_LIMIT = 50;
|
|
13
13
|
const CALL_KEYWORDS = new Set([
|
|
14
14
|
'if', 'for', 'while', 'switch', 'catch', 'return', 'new', 'typeof', 'function', 'class', 'def', 'await', 'with', 'from', 'import',
|
|
15
15
|
]);
|
package/dist/core/handlers.d.ts
CHANGED
|
@@ -296,6 +296,7 @@ export declare class ToolHandlers {
|
|
|
296
296
|
searchPassCount: number;
|
|
297
297
|
searchPassSuccessCount: number;
|
|
298
298
|
searchPassFailureCount: number;
|
|
299
|
+
semanticPassFailures?: import("./backend-diagnostics.js").SemanticPassFailureDiagnostic[];
|
|
299
300
|
rerankerAttempted: boolean;
|
|
300
301
|
rerankerUsed: boolean;
|
|
301
302
|
tool?: undefined;
|
|
@@ -336,6 +337,7 @@ export declare class ToolHandlers {
|
|
|
336
337
|
searchPassCount: number;
|
|
337
338
|
searchPassSuccessCount: number;
|
|
338
339
|
searchPassFailureCount: number;
|
|
340
|
+
semanticPassFailures?: import("./backend-diagnostics.js").SemanticPassFailureDiagnostic[];
|
|
339
341
|
rerankerAttempted: boolean;
|
|
340
342
|
rerankerUsed: boolean;
|
|
341
343
|
tool?: undefined;
|
package/dist/core/handlers.js
CHANGED
|
@@ -2298,6 +2298,21 @@ export class ToolHandlers {
|
|
|
2298
2298
|
isError: true,
|
|
2299
2299
|
};
|
|
2300
2300
|
}
|
|
2301
|
+
const parsedOperators = this.searchQuerySupport.parseSearchOperators(input.query);
|
|
2302
|
+
if (parsedOperators.semanticQuery.trim().length === 0) {
|
|
2303
|
+
const payload = this.buildInvalidSearchRequestPayload({
|
|
2304
|
+
path: input.path,
|
|
2305
|
+
query: input.query,
|
|
2306
|
+
scope: input.scope,
|
|
2307
|
+
groupBy: input.groupBy,
|
|
2308
|
+
resultMode: input.resultMode,
|
|
2309
|
+
limit: input.limit,
|
|
2310
|
+
}, 'Operator-only search requires semantic text or a positive must:, path:, or lang: value.');
|
|
2311
|
+
return {
|
|
2312
|
+
content: [{ type: "text", text: this.stringifyToolJson(payload) }],
|
|
2313
|
+
isError: true,
|
|
2314
|
+
};
|
|
2315
|
+
}
|
|
2301
2316
|
const searchDiagnostics = {
|
|
2302
2317
|
queryLength: input.query.length,
|
|
2303
2318
|
limitRequested: input.limit,
|
|
@@ -2425,9 +2440,16 @@ export class ToolHandlers {
|
|
|
2425
2440
|
thresholdMs: SEARCH_FRESHNESS_THRESHOLD_MS,
|
|
2426
2441
|
});
|
|
2427
2442
|
}
|
|
2428
|
-
return this.syncManager.ensureFreshness(effectiveRoot, exactSourceComparisonRequired ? 0 : SEARCH_FRESHNESS_THRESHOLD_MS,
|
|
2429
|
-
|
|
2430
|
-
|
|
2443
|
+
return this.syncManager.ensureFreshness(effectiveRoot, exactSourceComparisonRequired ? 0 : SEARCH_FRESHNESS_THRESHOLD_MS, {
|
|
2444
|
+
...(preparedRead?.vectorReceipt
|
|
2445
|
+
? { preparedVectorReceipt: preparedRead.vectorReceipt }
|
|
2446
|
+
: {}),
|
|
2447
|
+
...(exactSourceComparisonRequired
|
|
2448
|
+
? {
|
|
2449
|
+
exactSourceComparisonPaths: Array.from(changedFilesState.files).sort(),
|
|
2450
|
+
}
|
|
2451
|
+
: {}),
|
|
2452
|
+
});
|
|
2431
2453
|
}),
|
|
2432
2454
|
noteFreshnessMode: (mode) => {
|
|
2433
2455
|
searchDiagnostics.freshnessMode = mode;
|
|
@@ -2470,7 +2492,8 @@ export class ToolHandlers {
|
|
|
2470
2492
|
}
|
|
2471
2493
|
}
|
|
2472
2494
|
const sourceFreshnessWasEstablished = freshnessDecision.mode === 'synced'
|
|
2473
|
-
|| freshnessDecision.mode === 'reconciled_ignore_change'
|
|
2495
|
+
|| freshnessDecision.mode === 'reconciled_ignore_change'
|
|
2496
|
+
|| freshnessDecision.mode === 'skipped_source_unchanged';
|
|
2474
2497
|
const checkpointWarningAlreadyPresent = frontDoorWarnings.includes(WARNING_CODES.SOURCE_FRESHNESS_CHECKPOINT_UNAVAILABLE);
|
|
2475
2498
|
const partialIndexSearchWarnings = !sourceFreshnessWasEstablished
|
|
2476
2499
|
&& !checkpointWarningAlreadyPresent
|
|
@@ -2487,7 +2510,6 @@ export class ToolHandlers {
|
|
|
2487
2510
|
console.log(`${rootTag} Query metadata: length=${input.query.length}, requestId=${requestId}`);
|
|
2488
2511
|
console.log(`${rootTag} Indexing status: Completed`);
|
|
2489
2512
|
console.log(`${rootTag} 🧠 Using embedding provider: ${encoderEngine.getProvider()} for search`);
|
|
2490
|
-
const parsedOperators = this.searchQuerySupport.parseSearchOperators(input.query);
|
|
2491
2513
|
const semanticQuery = parsedOperators.semanticQuery;
|
|
2492
2514
|
const queryPlan = this.searchQuerySupport.buildSearchQueryPlan(semanticQuery, parsedOperators);
|
|
2493
2515
|
searchDiagnostics.routeKind = queryPlan.route.kind;
|
|
@@ -2537,6 +2559,7 @@ export class ToolHandlers {
|
|
|
2537
2559
|
const initialDirtyFilesNotFreshened = initialObservedChangedFilesState.available
|
|
2538
2560
|
&& initialObservedChangedFilesCount > 0
|
|
2539
2561
|
&& freshnessDecision.mode !== 'synced'
|
|
2562
|
+
&& freshnessDecision.mode !== 'skipped_source_unchanged'
|
|
2540
2563
|
&& freshnessDecision.mode !== 'reconciled_ignore_change';
|
|
2541
2564
|
const initialRankingProvenance = {
|
|
2542
2565
|
semanticPassesUsed: [],
|
|
@@ -2755,6 +2778,14 @@ export class ToolHandlers {
|
|
|
2755
2778
|
resultMode: input.resultMode,
|
|
2756
2779
|
limit: input.limit
|
|
2757
2780
|
}, "Search backend failed: all semantic search passes failed. Retry and verify embedding/vector backends are reachable.", "not_ready", "search_backend_failed");
|
|
2781
|
+
if (debugMode === 'full') {
|
|
2782
|
+
payload.hints = {
|
|
2783
|
+
...(payload.hints || {}),
|
|
2784
|
+
debugSearch: {
|
|
2785
|
+
semanticPassFailures: execution.semanticPassFailures.map((failure) => ({ ...failure })),
|
|
2786
|
+
},
|
|
2787
|
+
};
|
|
2788
|
+
}
|
|
2758
2789
|
return {
|
|
2759
2790
|
content: [{ type: "text", text: this.stringifyToolJson(payload) }],
|
|
2760
2791
|
isError: true,
|
|
@@ -3025,7 +3056,7 @@ export class ToolHandlers {
|
|
|
3025
3056
|
includeSummary: true,
|
|
3026
3057
|
buildEnvelope: (results, disclosure) => {
|
|
3027
3058
|
const recommendedNextAction = entry.recommendedActions[lookup.nextOffset] ?? null;
|
|
3028
|
-
const noiseMitigationHint = this.searchQuerySupport.buildNoiseMitigationHint(entry.canonicalRoot, results.map((result) => result.target.file), entry.baseEnvelope.scope);
|
|
3059
|
+
const noiseMitigationHint = this.searchQuerySupport.buildNoiseMitigationHint(entry.canonicalRoot, results.map((result) => result.target.file), entry.baseEnvelope.scope, this.searchQuerySupport.parseSearchOperators(entry.baseEnvelope.query));
|
|
3029
3060
|
const generatedArtifactsHint = this.buildGeneratedArtifactsVerificationHint(entry.canonicalRoot, results.map((result) => ({
|
|
3030
3061
|
file: result.target.file,
|
|
3031
3062
|
span: result.target.span,
|
|
@@ -766,6 +766,11 @@ export class ManageIndexingHandlers {
|
|
|
766
766
|
}
|
|
767
767
|
this.host.mutationLeaseCoordinator.publishWhileCurrent(mutationLease, publish);
|
|
768
768
|
},
|
|
769
|
+
publicationAuthority: {
|
|
770
|
+
ownerId: mutationLease.ownerId,
|
|
771
|
+
generation: mutationLease.generation,
|
|
772
|
+
operationId: mutationLease.operationId,
|
|
773
|
+
},
|
|
769
774
|
} : {}),
|
|
770
775
|
});
|
|
771
776
|
if (mutationLease) {
|
|
@@ -781,16 +786,41 @@ export class ManageIndexingHandlers {
|
|
|
781
786
|
...result.proof,
|
|
782
787
|
navigation: {
|
|
783
788
|
status: "unproven",
|
|
784
|
-
basis: "
|
|
789
|
+
basis: "generation_proof_in_progress",
|
|
785
790
|
},
|
|
786
791
|
};
|
|
787
|
-
await this.host.
|
|
792
|
+
const proven = await this.host.context.proveIndexedGeneration(absolutePath);
|
|
793
|
+
if (!proven
|
|
794
|
+
|| proven.collectionName !== result.collectionName
|
|
795
|
+
|| proven.marker.indexedFiles !== result.indexedFiles
|
|
796
|
+
|| proven.exactPayloadCount !== result.totalChunks) {
|
|
797
|
+
throw new Error(`Repair completion for '${absolutePath}' did not match an exact proven generation.`);
|
|
798
|
+
}
|
|
799
|
+
if (result.activatedGeneration) {
|
|
800
|
+
const activated = result.activatedGeneration;
|
|
801
|
+
if (activated.collectionName !== proven.collectionName
|
|
802
|
+
|| activated.markerRunId !== proven.marker.runId
|
|
803
|
+
|| activated.relationshipVersion !== this.host.runtimeFingerprint.relationshipVersion
|
|
804
|
+
|| activated.navigation.generationId !== proven.navigation.generationId
|
|
805
|
+
|| activated.navigation.sealHash !== proven.navigation.navigationSealHash
|
|
806
|
+
|| activated.navigation.symbolRegistryManifestHash
|
|
807
|
+
!== proven.navigation.symbolRegistryManifestHash
|
|
808
|
+
|| activated.navigation.relationshipManifestHash
|
|
809
|
+
!== proven.navigation.relationshipManifestHash) {
|
|
810
|
+
throw new Error(`Relationship-only repair for '${absolutePath}' did not prove its activated navigation generation.`);
|
|
811
|
+
}
|
|
812
|
+
const checkpoint = await this.host.context.inspectSourceFreshnessCheckpoint(absolutePath, proven.collectionName, proven);
|
|
813
|
+
if (checkpoint.status !== "valid"
|
|
814
|
+
|| checkpoint.documentDigest !== activated.sourceCheckpointDocumentDigest) {
|
|
815
|
+
throw new Error(`Relationship-only repair for '${absolutePath}' did not preserve its source checkpoint identity.`);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
788
818
|
assertMutationCurrent?.();
|
|
789
819
|
lastRepairProof = {
|
|
790
820
|
...result.proof,
|
|
791
821
|
navigation: {
|
|
792
822
|
status: "matched",
|
|
793
|
-
basis: "
|
|
823
|
+
basis: "activated_generation_proven",
|
|
794
824
|
},
|
|
795
825
|
};
|
|
796
826
|
try {
|
|
@@ -853,7 +883,7 @@ export class ManageIndexingHandlers {
|
|
|
853
883
|
}
|
|
854
884
|
catch (error) {
|
|
855
885
|
console.error("Error in handleRepairIndex:", error);
|
|
856
|
-
if (lastRepairProof?.navigation.basis === "
|
|
886
|
+
if (lastRepairProof?.navigation.basis === "generation_proof_in_progress") {
|
|
857
887
|
lastRepairProof = {
|
|
858
888
|
...lastRepairProof,
|
|
859
889
|
navigation: {
|
|
@@ -528,7 +528,6 @@ export class NavigationHandlers {
|
|
|
528
528
|
const exactRegistrySymbols = findExactRegistrySymbols({
|
|
529
529
|
symbols: registryState.symbols,
|
|
530
530
|
symbolIdExact: symbolRef.symbolId,
|
|
531
|
-
symbolLabelExact: symbolRef.symbolLabel,
|
|
532
531
|
});
|
|
533
532
|
if (exactRegistrySymbols.length === 0) {
|
|
534
533
|
const payload = this.host.withProofDebugHint({
|
|
@@ -140,21 +140,103 @@ function findPythonDefinitionIndexNearSpan(lines, symbol) {
|
|
|
140
140
|
const windowEnd = Math.min(lines.length, Math.max(spanEndExclusive, startIndex + 48));
|
|
141
141
|
return findPythonDefinitionIndexByName(lines, symbol.name, windowStart, windowEnd);
|
|
142
142
|
}
|
|
143
|
+
function isPythonDecoratorBlock(lines, startIndex, definitionIndex, indent) {
|
|
144
|
+
let delimiterDepth = 0;
|
|
145
|
+
let quote = null;
|
|
146
|
+
let tripleQuoted = false;
|
|
147
|
+
let escaped = false;
|
|
148
|
+
let expectsDecorator = true;
|
|
149
|
+
for (let index = startIndex; index < definitionIndex; index += 1) {
|
|
150
|
+
const line = lines[index] || "";
|
|
151
|
+
const trimmed = line.trim();
|
|
152
|
+
if (expectsDecorator) {
|
|
153
|
+
if (trimmed.length === 0
|
|
154
|
+
|| !trimmed.startsWith("@")
|
|
155
|
+
|| countPythonIndent(line) !== indent) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
expectsDecorator = false;
|
|
159
|
+
}
|
|
160
|
+
else if (trimmed.length === 0 && delimiterDepth === 0 && !tripleQuoted) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
for (let offset = 0; offset < line.length; offset += 1) {
|
|
164
|
+
const char = line[offset];
|
|
165
|
+
const nextTwo = line.slice(offset, offset + 3);
|
|
166
|
+
if (quote) {
|
|
167
|
+
if (escaped) {
|
|
168
|
+
escaped = false;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (char === "\\") {
|
|
172
|
+
escaped = true;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (tripleQuoted && nextTwo === quote.repeat(3)) {
|
|
176
|
+
quote = null;
|
|
177
|
+
tripleQuoted = false;
|
|
178
|
+
offset += 2;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (!tripleQuoted && char === quote) {
|
|
182
|
+
quote = null;
|
|
183
|
+
}
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (char === "#") {
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
if (nextTwo === "'''" || nextTwo === "\"\"\"") {
|
|
190
|
+
quote = char;
|
|
191
|
+
tripleQuoted = true;
|
|
192
|
+
offset += 2;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (char === "'" || char === "\"") {
|
|
196
|
+
quote = char;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (char === "(" || char === "[" || char === "{") {
|
|
200
|
+
delimiterDepth += 1;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (char === ")" || char === "]" || char === "}") {
|
|
204
|
+
delimiterDepth -= 1;
|
|
205
|
+
if (delimiterDepth < 0) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const explicitContinuation = line.trimEnd().endsWith("\\");
|
|
211
|
+
if (!quote && delimiterDepth === 0 && !explicitContinuation) {
|
|
212
|
+
expectsDecorator = true;
|
|
213
|
+
}
|
|
214
|
+
else if (quote && !tripleQuoted) {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
escaped = false;
|
|
218
|
+
}
|
|
219
|
+
return expectsDecorator && delimiterDepth === 0 && quote === null;
|
|
220
|
+
}
|
|
143
221
|
function findPythonDecoratedDefinitionStart(lines, definitionIndex) {
|
|
144
222
|
const indent = countPythonIndent(lines[definitionIndex] || "");
|
|
145
|
-
|
|
223
|
+
const candidates = [];
|
|
146
224
|
for (let index = definitionIndex - 1; index >= 0; index -= 1) {
|
|
147
225
|
const line = lines[index] || "";
|
|
148
226
|
const trimmed = line.trim();
|
|
149
|
-
if (trimmed.length === 0) {
|
|
227
|
+
if (trimmed.length === 0 && candidates.length === 0) {
|
|
150
228
|
break;
|
|
151
229
|
}
|
|
152
|
-
if (
|
|
153
|
-
|
|
230
|
+
if (trimmed.startsWith("@") && countPythonIndent(line) === indent) {
|
|
231
|
+
candidates.push(index);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
for (const candidate of candidates.reverse()) {
|
|
235
|
+
if (isPythonDecoratorBlock(lines, candidate, definitionIndex, indent)) {
|
|
236
|
+
return candidate;
|
|
154
237
|
}
|
|
155
|
-
startIndex = index;
|
|
156
238
|
}
|
|
157
|
-
return
|
|
239
|
+
return definitionIndex;
|
|
158
240
|
}
|
|
159
241
|
function findPythonSourceBackedBlockEnd(lines, definitionIndex, indent) {
|
|
160
242
|
let lastContentLine = definitionIndex + 1;
|
|
@@ -40,8 +40,6 @@ export type RelationshipBackedCallGraphResult = {
|
|
|
40
40
|
};
|
|
41
41
|
hints?: Record<string, unknown>;
|
|
42
42
|
};
|
|
43
|
-
/** True when a repo-relative path looks like a test/fixture site (not production call graph signal). */
|
|
44
|
-
export declare function isTestOrFixtureCallerFile(file: string): boolean;
|
|
45
43
|
/**
|
|
46
44
|
* Prefer a single unique suppressed inbound caller *site* file for recovery search.
|
|
47
45
|
* Prefer production files when both production and test sites exist.
|
|
@@ -58,6 +56,8 @@ export declare class RelationshipBackedCallGraph {
|
|
|
58
56
|
private sortNodes;
|
|
59
57
|
private compareEdges;
|
|
60
58
|
private sortEdges;
|
|
59
|
+
private sortTestReferences;
|
|
60
|
+
private buildTestReferences;
|
|
61
61
|
private sortNotes;
|
|
62
62
|
private mapRelationshipConfidence;
|
|
63
63
|
private createNode;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { compareContractStrings, getGraphNeighbors, } from "@zokizuan/satori-core";
|
|
1
|
+
import { compareContractStrings, getGraphNeighbors, getRelationshipsForSymbol, isTestOrFixturePath, } from "@zokizuan/satori-core";
|
|
2
|
+
import { DEFAULT_CALL_GRAPH_TEST_REFERENCE_LIMIT } from "./call-graph.js";
|
|
2
3
|
import { buildSourceBackedPythonCalleeFallback, buildSourceBackedPythonCallerFallback, } from "./python-call-fallback.js";
|
|
3
4
|
import { buildInboundNotesOnlySearchQuery } from "./search-response-helpers.js";
|
|
4
5
|
function compareNullableNumbersAsc(a, b) {
|
|
@@ -11,24 +12,6 @@ function compareNullableStringsAsc(a, b) {
|
|
|
11
12
|
const right = typeof b === "string" ? b : "";
|
|
12
13
|
return compareContractStrings(left, right);
|
|
13
14
|
}
|
|
14
|
-
/** True when a repo-relative path looks like a test/fixture site (not production call graph signal). */
|
|
15
|
-
export function isTestOrFixtureCallerFile(file) {
|
|
16
|
-
const normalized = file.trim().replace(/\\/g, "/");
|
|
17
|
-
if (!normalized) {
|
|
18
|
-
return false;
|
|
19
|
-
}
|
|
20
|
-
const base = normalized.split("/").pop() || normalized;
|
|
21
|
-
if (/\.(test|spec)\.[cm]?[jt]sx?$/i.test(base)) {
|
|
22
|
-
return true;
|
|
23
|
-
}
|
|
24
|
-
if (/(^|\/)(__tests__|__mocks__|fixtures|testdata|test-data)(\/|$)/i.test(normalized)) {
|
|
25
|
-
return true;
|
|
26
|
-
}
|
|
27
|
-
if (/(^|\/)tests?(\/|$)/i.test(normalized) && !/(^|\/)packages\/[^/]+\/src\//i.test(normalized)) {
|
|
28
|
-
return true;
|
|
29
|
-
}
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
15
|
/**
|
|
33
16
|
* Prefer a single unique suppressed inbound caller *site* file for recovery search.
|
|
34
17
|
* Prefer production files when both production and test sites exist.
|
|
@@ -49,7 +32,7 @@ export function uniqueInboundCallerSiteFile(notes) {
|
|
|
49
32
|
continue;
|
|
50
33
|
}
|
|
51
34
|
allSites.add(file);
|
|
52
|
-
if (!
|
|
35
|
+
if (!isTestOrFixturePath(file)) {
|
|
53
36
|
productionSites.add(file);
|
|
54
37
|
}
|
|
55
38
|
}
|
|
@@ -76,7 +59,7 @@ export function prioritizeInboundSuppressedNotes(notes) {
|
|
|
76
59
|
continue;
|
|
77
60
|
}
|
|
78
61
|
const file = typeof note.file === "string" ? note.file : "";
|
|
79
|
-
if (
|
|
62
|
+
if (isTestOrFixturePath(file)) {
|
|
80
63
|
testCallerNotes.push(note);
|
|
81
64
|
}
|
|
82
65
|
else {
|
|
@@ -138,6 +121,71 @@ export class RelationshipBackedCallGraph {
|
|
|
138
121
|
sortEdges(edges) {
|
|
139
122
|
return [...edges].sort((a, b) => this.compareEdges(a, b));
|
|
140
123
|
}
|
|
124
|
+
sortTestReferences(references) {
|
|
125
|
+
return [...references].sort((a, b) => {
|
|
126
|
+
const fileCmp = compareNullableStringsAsc(a.file, b.file);
|
|
127
|
+
if (fileCmp !== 0)
|
|
128
|
+
return fileCmp;
|
|
129
|
+
const startCmp = compareNullableNumbersAsc(a.span?.startLine, b.span?.startLine);
|
|
130
|
+
if (startCmp !== 0)
|
|
131
|
+
return startCmp;
|
|
132
|
+
const labelCmp = compareNullableStringsAsc(a.symbolLabel, b.symbolLabel);
|
|
133
|
+
if (labelCmp !== 0)
|
|
134
|
+
return labelCmp;
|
|
135
|
+
const symbolCmp = compareNullableStringsAsc(a.symbolId, b.symbolId);
|
|
136
|
+
if (symbolCmp !== 0)
|
|
137
|
+
return symbolCmp;
|
|
138
|
+
const targetCmp = compareNullableStringsAsc(a.targetSymbolId, b.targetSymbolId);
|
|
139
|
+
if (targetCmp !== 0)
|
|
140
|
+
return targetCmp;
|
|
141
|
+
const siteFileCmp = compareNullableStringsAsc(a.site?.file, b.site?.file);
|
|
142
|
+
if (siteFileCmp !== 0)
|
|
143
|
+
return siteFileCmp;
|
|
144
|
+
return compareNullableNumbersAsc(a.site?.startLine, b.site?.startLine);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
buildTestReferences(records, registry, targetSymbolId) {
|
|
148
|
+
const referencesByKey = new Map();
|
|
149
|
+
for (const record of records) {
|
|
150
|
+
if (record.type !== "TESTS"
|
|
151
|
+
|| !record.sourceInstanceId
|
|
152
|
+
|| record.targetInstanceId !== targetSymbolId) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const source = registry.symbolsByInstanceId.get(record.sourceInstanceId);
|
|
156
|
+
if (!source) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const startLine = record.span?.startLine ?? source.span.startLine;
|
|
160
|
+
const reference = {
|
|
161
|
+
file: source.file,
|
|
162
|
+
symbolId: source.symbolInstanceId,
|
|
163
|
+
symbolLabel: source.label,
|
|
164
|
+
span: {
|
|
165
|
+
startLine: source.span.startLine,
|
|
166
|
+
endLine: source.span.endLine,
|
|
167
|
+
},
|
|
168
|
+
site: {
|
|
169
|
+
file: record.file,
|
|
170
|
+
startLine,
|
|
171
|
+
...(record.span?.endLine ? { endLine: record.span.endLine } : {}),
|
|
172
|
+
},
|
|
173
|
+
targetSymbolId,
|
|
174
|
+
kind: "call",
|
|
175
|
+
confidence: this.mapRelationshipConfidence(record.confidence),
|
|
176
|
+
};
|
|
177
|
+
const key = [
|
|
178
|
+
reference.symbolId,
|
|
179
|
+
reference.targetSymbolId,
|
|
180
|
+
reference.kind,
|
|
181
|
+
reference.site.file,
|
|
182
|
+
reference.site.startLine,
|
|
183
|
+
].join("\0");
|
|
184
|
+
referencesByKey.set(key, reference);
|
|
185
|
+
}
|
|
186
|
+
return this.sortTestReferences([...referencesByKey.values()])
|
|
187
|
+
.slice(0, DEFAULT_CALL_GRAPH_TEST_REFERENCE_LIMIT);
|
|
188
|
+
}
|
|
141
189
|
sortNotes(notes) {
|
|
142
190
|
return [...notes].sort((a, b) => {
|
|
143
191
|
const fileCmp = compareNullableStringsAsc(a.file, b.file);
|
|
@@ -243,6 +291,19 @@ export class RelationshipBackedCallGraph {
|
|
|
243
291
|
if (neighbors.status !== "ok") {
|
|
244
292
|
return null;
|
|
245
293
|
}
|
|
294
|
+
const testRelationshipResult = await getRelationshipsForSymbol({
|
|
295
|
+
normalizedRootPath: input.codebaseRoot,
|
|
296
|
+
generationId: input.generationId,
|
|
297
|
+
expectedSymbolRegistryManifestHash: input.registryManifestHash,
|
|
298
|
+
navigationStore: this.host.navigationStore,
|
|
299
|
+
targetInstanceId: input.resolvedSymbol.symbolInstanceId,
|
|
300
|
+
direction: "callers",
|
|
301
|
+
types: ["TESTS"],
|
|
302
|
+
});
|
|
303
|
+
if (testRelationshipResult.status !== "ok") {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
const testReferences = this.buildTestReferences(testRelationshipResult.records, input.registry, input.resolvedSymbol.symbolInstanceId);
|
|
246
307
|
const suppressedLowConfidenceRecords = neighbors.suppressedLowConfidenceRecords || [];
|
|
247
308
|
const resolveNodeSymbol = (symbolInstanceId) => (symbolInstanceId === input.resolvedSymbol.symbolInstanceId
|
|
248
309
|
? input.resolvedSymbol
|
|
@@ -408,6 +469,7 @@ export class RelationshipBackedCallGraph {
|
|
|
408
469
|
nodeCount: combinedNodes.length,
|
|
409
470
|
edgeCount: combinedEdges.length,
|
|
410
471
|
},
|
|
472
|
+
...(testReferences.length > 0 ? { testReferences } : {}),
|
|
411
473
|
...(hints ? { hints } : {}),
|
|
412
474
|
};
|
|
413
475
|
}
|
|
@@ -350,7 +350,7 @@ export async function runExactRegistryFastPath(input, host) {
|
|
|
350
350
|
partialIndexSearchWarnings: input.partialIndexSearchWarnings,
|
|
351
351
|
dirtyFilesNotFreshened: input.dirtyFilesNotFreshened,
|
|
352
352
|
changedFilesBoostSkippedForLargeChangeSet: input.changedFilesBoostSkippedForLargeChangeSet,
|
|
353
|
-
buildNoiseMitigationHint: (files) => host.searchQuerySupport.buildNoiseMitigationHint(input.effectiveRoot, files, input.scope),
|
|
353
|
+
buildNoiseMitigationHint: (files) => host.searchQuerySupport.buildNoiseMitigationHint(input.effectiveRoot, files, input.scope, input.parsedOperators),
|
|
354
354
|
buildGeneratedArtifactsVerificationHint: (results) => host.buildGeneratedArtifactsVerificationHint(input.effectiveRoot, results),
|
|
355
355
|
});
|
|
356
356
|
if (!envelope) {
|
|
@@ -4,7 +4,7 @@ import type { SearchCandidateSurvivalDebug, SearchDebugMode, SearchFreshnessSumm
|
|
|
4
4
|
import type { SearchQuerySupport } from "./search-query-support.js";
|
|
5
5
|
import type { SearchQueryPlan, SearchResultLike } from "./search-lexical-scoring.js";
|
|
6
6
|
import type { ParsedSearchOperators } from "./search-query-planning.js";
|
|
7
|
-
import type
|
|
7
|
+
import { type SemanticPassFailureDiagnostic, type VectorBackendDiagnostic } from "./backend-diagnostics.js";
|
|
8
8
|
import type { EmbeddingProviderDiagnostic } from "./embedding-provider-diagnostics.js";
|
|
9
9
|
import type { FreshnessDecision } from "./sync.js";
|
|
10
10
|
import { type RerankBudgetReason } from "./search-rerank-policy.js";
|
|
@@ -39,6 +39,7 @@ export type SearchDiagnostics = SearchProviderWorkDiagnostics & {
|
|
|
39
39
|
searchPassCount: number;
|
|
40
40
|
searchPassSuccessCount: number;
|
|
41
41
|
searchPassFailureCount: number;
|
|
42
|
+
semanticPassFailures?: SemanticPassFailureDiagnostic[];
|
|
42
43
|
rerankerAttempted: boolean;
|
|
43
44
|
rerankerUsed: boolean;
|
|
44
45
|
};
|
|
@@ -149,6 +150,7 @@ export type SearchExecutionOutcome = {
|
|
|
149
150
|
};
|
|
150
151
|
providerWork: SearchProviderWorkDiagnostics;
|
|
151
152
|
candidateSurvival?: SearchCandidateSurvivalDebug;
|
|
153
|
+
semanticPassFailures: SemanticPassFailureDiagnostic[];
|
|
152
154
|
} | {
|
|
153
155
|
kind: "vector_backend_unavailable";
|
|
154
156
|
diagnostic: VectorBackendDiagnostic;
|
|
@@ -157,6 +159,7 @@ export type SearchExecutionOutcome = {
|
|
|
157
159
|
diagnostic: EmbeddingProviderDiagnostic;
|
|
158
160
|
} | {
|
|
159
161
|
kind: "all_semantic_passes_failed";
|
|
162
|
+
semanticPassFailures: SemanticPassFailureDiagnostic[];
|
|
160
163
|
};
|
|
161
164
|
export type SearchExecutionHost = {
|
|
162
165
|
searchQuerySupport: SearchQuerySupport;
|
|
@@ -3,6 +3,7 @@ import { appendCoreCandidateTrace, appendSearchCandidatePass, appendSearchCandid
|
|
|
3
3
|
import { WARNING_CODES } from "./warnings.js";
|
|
4
4
|
import { buildSearchPassWarning as buildSearchPassWarningHelper, } from "./search-response-helpers.js";
|
|
5
5
|
import { classifyPathCategory, resolveAgentFitMultiplier as resolveSearchAgentFitMultiplier, shouldApplyChangedFilesBoost, shouldIncludeCategoryInScope, sortSearchCandidates as sortSearchCandidatesHelper, } from "./search-ranking-policy.js";
|
|
6
|
+
import { buildSemanticPassFailureDiagnostic, } from "./backend-diagnostics.js";
|
|
6
7
|
import { resolveRerankFamilyKey, selectRerankCandidates, selectRerankInputWithinUtf8Budget, } from "./search-rerank-policy.js";
|
|
7
8
|
import { resolveNextSearchCandidateLimit, } from './search-policy.js';
|
|
8
9
|
const SEARCH_EXPANSION_MIN_PRIMARY_SCOPED_CANDIDATES = 5;
|
|
@@ -298,6 +299,7 @@ export async function runSearchExecution(input, host, searchDiagnostics) {
|
|
|
298
299
|
const dirtyFilesNotFreshened = observedChangedFilesState.available
|
|
299
300
|
&& observedChangedFilesCount > 0
|
|
300
301
|
&& input.freshnessMode !== "synced"
|
|
302
|
+
&& input.freshnessMode !== "skipped_source_unchanged"
|
|
301
303
|
&& input.freshnessMode !== "reconciled_ignore_change";
|
|
302
304
|
const canSupplementLivePathEvidence = observedChangedFilesState.available
|
|
303
305
|
&& observedChangedFilesCount > 0
|
|
@@ -305,6 +307,7 @@ export async function runSearchExecution(input, host, searchDiagnostics) {
|
|
|
305
307
|
let boostedCandidates = 0;
|
|
306
308
|
let attemptsUsed = 0;
|
|
307
309
|
const searchWarningsSet = new Set();
|
|
310
|
+
const semanticPassFailures = [];
|
|
308
311
|
const suppressedDirtyPaths = new Set();
|
|
309
312
|
const representedDirtyPaths = new Set();
|
|
310
313
|
const passesUsed = new Set();
|
|
@@ -438,16 +441,23 @@ export async function runSearchExecution(input, host, searchDiagnostics) {
|
|
|
438
441
|
passesUsed.add(passDescriptor.id);
|
|
439
442
|
continue;
|
|
440
443
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
444
|
+
const passEmbeddingDiagnostic = host.classifyEmbeddingProviderError(passResult.reason);
|
|
445
|
+
const passVectorDiagnostic = passEmbeddingDiagnostic
|
|
446
|
+
? null
|
|
447
|
+
: host.classifyVectorBackendError(passResult.reason);
|
|
448
|
+
embeddingProviderDiagnostic ?? (embeddingProviderDiagnostic = passEmbeddingDiagnostic);
|
|
449
|
+
vectorBackendDiagnostic ?? (vectorBackendDiagnostic = passVectorDiagnostic);
|
|
450
|
+
semanticPassFailures.push(buildSemanticPassFailureDiagnostic({
|
|
451
|
+
passId: passDescriptor.id,
|
|
452
|
+
error: passResult.reason,
|
|
453
|
+
embeddingDiagnostic: passEmbeddingDiagnostic,
|
|
454
|
+
vectorDiagnostic: passVectorDiagnostic,
|
|
455
|
+
}));
|
|
447
456
|
searchWarningsSet.add(buildSearchPassWarningHelper(passDescriptor.id));
|
|
448
457
|
}
|
|
449
458
|
searchDiagnostics.searchPassSuccessCount += successfulPasses.length;
|
|
450
459
|
searchDiagnostics.searchPassFailureCount += passDescriptors.length - successfulPasses.length;
|
|
460
|
+
searchDiagnostics.semanticPassFailures = semanticPassFailures.map((failure) => ({ ...failure }));
|
|
451
461
|
if (successfulPasses.length === 0) {
|
|
452
462
|
if (embeddingProviderDiagnostic) {
|
|
453
463
|
return {
|
|
@@ -461,7 +471,10 @@ export async function runSearchExecution(input, host, searchDiagnostics) {
|
|
|
461
471
|
diagnostic: vectorBackendDiagnostic,
|
|
462
472
|
};
|
|
463
473
|
}
|
|
464
|
-
return {
|
|
474
|
+
return {
|
|
475
|
+
kind: "all_semantic_passes_failed",
|
|
476
|
+
semanticPassFailures,
|
|
477
|
+
};
|
|
465
478
|
}
|
|
466
479
|
const byChunkKey = new Map();
|
|
467
480
|
const attemptFilterSummary = buildEmptyFilterSummary();
|
|
@@ -876,6 +889,7 @@ export async function runSearchExecution(input, host, searchDiagnostics) {
|
|
|
876
889
|
semanticExpansionAttempted: searchDiagnostics.semanticExpansionAttempted,
|
|
877
890
|
semanticExpansionReason: searchDiagnostics.semanticExpansionReason,
|
|
878
891
|
},
|
|
892
|
+
semanticPassFailures,
|
|
879
893
|
...(candidateSurvival ? { candidateSurvival } : {}),
|
|
880
894
|
};
|
|
881
895
|
}
|
|
@@ -28,6 +28,7 @@ function buildReadinessWarnings(host, state) {
|
|
|
28
28
|
}
|
|
29
29
|
function freshnessDecisionPreservesAuthority(decision) {
|
|
30
30
|
return decision.mode === 'skipped_recent'
|
|
31
|
+
|| decision.mode === 'skipped_source_unchanged'
|
|
31
32
|
|| decision.mode === 'skipped_source_checkpoint_unavailable';
|
|
32
33
|
}
|
|
33
34
|
function buildBlockedReadinessPayload(state, searchContext, host) {
|
|
@@ -57,18 +57,17 @@ function unquoteOperatorValue(value) {
|
|
|
57
57
|
return trimmed;
|
|
58
58
|
}
|
|
59
59
|
function deriveOperatorOnlySemanticQuery(operators) {
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
const mustTerms = operators.must.map((value) => value.trim()).filter(Boolean);
|
|
61
|
+
if (mustTerms.length > 0) {
|
|
62
|
+
return mustTerms.join(" ");
|
|
62
63
|
}
|
|
63
|
-
const
|
|
64
|
-
if (
|
|
65
|
-
return
|
|
64
|
+
const pathTerms = operators.path.map((value) => value.trim()).filter(Boolean);
|
|
65
|
+
if (pathTerms.length > 0) {
|
|
66
|
+
return pathTerms.join(" ");
|
|
66
67
|
}
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (symbolInstanceIdLike || (identifierLike && strongIdentifierSignal)) {
|
|
71
|
-
return mustValue;
|
|
68
|
+
const languageTerms = operators.lang.map((value) => value.trim()).filter(Boolean);
|
|
69
|
+
if (languageTerms.length > 0) {
|
|
70
|
+
return languageTerms.join(" ");
|
|
72
71
|
}
|
|
73
72
|
return null;
|
|
74
73
|
}
|
|
@@ -151,7 +150,7 @@ export function parseSearchOperators(query) {
|
|
|
151
150
|
const semanticParts = [semanticFromPrefix, semanticSuffix].filter((part) => part.length > 0);
|
|
152
151
|
operators.semanticQuery = semanticParts.length > 0
|
|
153
152
|
? semanticParts.join("\n")
|
|
154
|
-
: (deriveOperatorOnlySemanticQuery(operators) ||
|
|
153
|
+
: (deriveOperatorOnlySemanticQuery(operators) || "");
|
|
155
154
|
return operators;
|
|
156
155
|
}
|
|
157
156
|
function tokenizeLexicalTerms(tokens) {
|
|
@@ -99,7 +99,7 @@ export declare class SearchQuerySupport {
|
|
|
99
99
|
private loadRootGitignoreMatcher;
|
|
100
100
|
private patternMatchesAnyPath;
|
|
101
101
|
private filterNoiseHintPatternsByRootGitignore;
|
|
102
|
-
buildNoiseMitigationHint(codebaseRoot: string, filesInOrder: string[], scope: SearchScope): SearchNoiseMitigationHint | undefined;
|
|
102
|
+
buildNoiseMitigationHint(codebaseRoot: string, filesInOrder: string[], scope: SearchScope, parsedOperators: Pick<ParsedSearchOperators, "path">): SearchNoiseMitigationHint | undefined;
|
|
103
103
|
parseSearchOperators(query: string): ParsedSearchOperators;
|
|
104
104
|
buildSearchQueryPlan(semanticQuery: string, parsedOperators?: ParsedSearchOperators): SearchQueryPlan;
|
|
105
105
|
hasTokenBoundaryMatch(field: string, term: string): boolean;
|
|
@@ -912,8 +912,8 @@ export class SearchQuerySupport {
|
|
|
912
912
|
coveredByRootGitignore,
|
|
913
913
|
};
|
|
914
914
|
}
|
|
915
|
-
buildNoiseMitigationHint(codebaseRoot, filesInOrder, scope) {
|
|
916
|
-
if (scope === 'docs') {
|
|
915
|
+
buildNoiseMitigationHint(codebaseRoot, filesInOrder, scope, parsedOperators) {
|
|
916
|
+
if (scope === 'docs' || scope === 'runtime' || parsedOperators.path.length > 0) {
|
|
917
917
|
return undefined;
|
|
918
918
|
}
|
|
919
919
|
if (!Array.isArray(filesInOrder) || filesInOrder.length === 0) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SearchGroupBy, SearchScope } from "./search-constants.js";
|
|
2
2
|
import type { FreshnessDecision } from "./sync.js";
|
|
3
|
-
import type { SearchChunkResult, SearchDebugHint, SearchFreshnessDebugHint, SearchFreshnessSummary, SearchGroupResult, SearchDisclosureSummary, SearchRankingDebugHint, SearchGroupedResultV2, SearchResponseEnvelope } from "./search-types.js";
|
|
3
|
+
import type { SearchChunkResult, SearchDebugHint, SearchFreshnessDebugHint, SearchFreshnessSummary, SearchGroupResult, SearchDisclosureSummary, SearchRankingDebugHint, SearchPassFailureDebugHint, SearchGroupedResultV2, SearchResponseEnvelope } from "./search-types.js";
|
|
4
4
|
import type { CompletionProbeDebugHint } from "./tracked-root-readiness.js";
|
|
5
5
|
type SearchResponseCommonInput = {
|
|
6
6
|
codebaseRoot: string;
|
|
@@ -13,7 +13,7 @@ type SearchResponseCommonInput = {
|
|
|
13
13
|
freshnessSummary: SearchFreshnessSummary;
|
|
14
14
|
warnings: string[];
|
|
15
15
|
debugSummary?: NonNullable<NonNullable<SearchResponseEnvelope["hints"]>["debugSummary"]>;
|
|
16
|
-
debugSearch?: SearchDebugHint | SearchRankingDebugHint | SearchFreshnessDebugHint;
|
|
16
|
+
debugSearch?: SearchDebugHint | SearchRankingDebugHint | SearchFreshnessDebugHint | SearchPassFailureDebugHint;
|
|
17
17
|
proofDebugHint?: CompletionProbeDebugHint;
|
|
18
18
|
noiseMitigationHint?: unknown;
|
|
19
19
|
generatedArtifactsHint?: unknown;
|
|
@@ -5,7 +5,7 @@ import { buildSearchDebugSummary, buildSearchGroupRecommendedAction, SEARCH_GROU
|
|
|
5
5
|
import { appendGroupedCandidateStage, appendSearchCandidateRemoval, appendSearchCandidateStage, } from "./search-candidate-survival.js";
|
|
6
6
|
import { projectGroupedDisclosure } from "./search-disclosure.js";
|
|
7
7
|
export async function finalizeSearchResults(input, host) {
|
|
8
|
-
let { scored, operatorSummary, filterSummary, trackedLexicalDebug, candidateLimit, diagnosticCandidateLimit, attemptsUsed, searchWarnings, passesUsed, backendScoreKinds, exactMatchPinningApplied, boostedCandidates, changedFilesState, debugChangedFilesState, changedFilesCount, changedFilesBoostSkippedForLargeChangeSet, rankingProvenance, rerankerAttempted, rerankerApplied, skippedByExactPin, rerankerFailurePhase, rerankerCandidatesIn, rerankerCandidatesReranked, rerankerFamilyCount, rerankerSupplementalCandidates, rerankerCandidatePoolCount, rerankerCandidateBudget, rerankerBudgetReason, rerankerByteBudgetOmittedCandidates, semanticExpansion, providerWork, candidateSurvival, } = input.execution;
|
|
8
|
+
let { scored, operatorSummary, filterSummary, trackedLexicalDebug, candidateLimit, diagnosticCandidateLimit, attemptsUsed, searchWarnings, passesUsed, backendScoreKinds, exactMatchPinningApplied, boostedCandidates, changedFilesState, debugChangedFilesState, changedFilesCount, changedFilesBoostSkippedForLargeChangeSet, rankingProvenance, rerankerAttempted, rerankerApplied, skippedByExactPin, rerankerFailurePhase, rerankerCandidatesIn, rerankerCandidatesReranked, rerankerFamilyCount, rerankerSupplementalCandidates, rerankerCandidatePoolCount, rerankerCandidateBudget, rerankerBudgetReason, rerankerByteBudgetOmittedCandidates, semanticExpansion, providerWork, candidateSurvival, semanticPassFailures, } = input.execution;
|
|
9
9
|
let freshnessSummary = input.freshnessSummary;
|
|
10
10
|
let finalizedSearchWarnings = Array.from(new Set([
|
|
11
11
|
...searchWarnings,
|
|
@@ -47,6 +47,9 @@ export async function finalizeSearchResults(input, host) {
|
|
|
47
47
|
candidatesWithLexicalEvidence: providerWork.candidatesWithLexicalEvidence,
|
|
48
48
|
candidatesWithCurrentSourceEvidence: providerWork.candidatesWithCurrentSourceEvidence,
|
|
49
49
|
},
|
|
50
|
+
...(semanticPassFailures.length > 0 ? {
|
|
51
|
+
semanticPassFailures: semanticPassFailures.map((failure) => ({ ...failure })),
|
|
52
|
+
} : {}),
|
|
50
53
|
semanticExpansion,
|
|
51
54
|
rankingProvenance,
|
|
52
55
|
...(trackedLexicalDebug ? { trackedLexical: trackedLexicalDebug } : {}),
|
|
@@ -200,7 +203,7 @@ export async function finalizeSearchResults(input, host) {
|
|
|
200
203
|
debugMode: input.debugMode,
|
|
201
204
|
now: host.now,
|
|
202
205
|
});
|
|
203
|
-
const noiseMitigationHint = host.searchQuerySupport.buildNoiseMitigationHint(input.effectiveRoot, rawResults.map((result) => result.file), input.scope);
|
|
206
|
+
const noiseMitigationHint = host.searchQuerySupport.buildNoiseMitigationHint(input.effectiveRoot, rawResults.map((result) => result.file), input.scope, input.parsedOperators);
|
|
204
207
|
const generatedArtifactsHint = host.buildGeneratedArtifactsVerificationHint(input.effectiveRoot, rawResults.map((result) => ({
|
|
205
208
|
file: result.file,
|
|
206
209
|
span: result.span,
|
|
@@ -347,7 +350,7 @@ export async function finalizeSearchResults(input, host) {
|
|
|
347
350
|
includeSummary: input.disclosureLimit < input.limit
|
|
348
351
|
|| completeDisclosureOrder.length > input.limit,
|
|
349
352
|
buildEnvelope: (results, disclosure) => {
|
|
350
|
-
const noiseMitigationHint = host.searchQuerySupport.buildNoiseMitigationHint(input.effectiveRoot, results.map((result) => result.target.file), input.scope);
|
|
353
|
+
const noiseMitigationHint = host.searchQuerySupport.buildNoiseMitigationHint(input.effectiveRoot, results.map((result) => result.target.file), input.scope, input.parsedOperators);
|
|
351
354
|
const generatedArtifactsHint = host.buildGeneratedArtifactsVerificationHint(input.effectiveRoot, results.map((result) => ({
|
|
352
355
|
file: result.target.file,
|
|
353
356
|
span: result.target.span,
|
|
@@ -4,6 +4,7 @@ import { SearchGroupBy, SearchNoiseCategory, SearchRankingMode, SearchResultMode
|
|
|
4
4
|
import { FingerprintSource, IndexFingerprint } from "../config.js";
|
|
5
5
|
import type { SearchRouteContract } from "./search-lexical-scoring.js";
|
|
6
6
|
import type { RerankBudgetReason } from "./search-rerank-policy.js";
|
|
7
|
+
import type { SemanticPassFailureDiagnostic } from "./backend-diagnostics.js";
|
|
7
8
|
export type StalenessBucket = "fresh" | "aging" | "stale" | "unknown";
|
|
8
9
|
export declare const SEARCH_RESPONSE_FORMAT_VERSION: 2;
|
|
9
10
|
export interface SearchSpan {
|
|
@@ -304,6 +305,7 @@ export interface SearchDebugHint {
|
|
|
304
305
|
rrfK: number;
|
|
305
306
|
};
|
|
306
307
|
providerWork: SearchProviderWorkDebugHint;
|
|
308
|
+
semanticPassFailures?: SemanticPassFailureDiagnostic[];
|
|
307
309
|
candidateSurvival?: SearchCandidateSurvivalDebug;
|
|
308
310
|
semanticExpansion?: {
|
|
309
311
|
attempted: boolean;
|
|
@@ -463,12 +465,13 @@ export interface SearchDebugHint {
|
|
|
463
465
|
};
|
|
464
466
|
};
|
|
465
467
|
}
|
|
466
|
-
export type SearchRankingDebugHint = Pick<SearchDebugHint, "route" | "queryIntent" | "retrieval" | "mcpFusion" | "providerWork" | "semanticExpansion" | "rankingProvenance" | "trackedLexical" | "exactRegistry" | "passesUsed" | "candidateLimit" | "mustRetry" | "operatorSummary" | "filterSummary" | "diversitySummary" | "changedFilesBoost" | "rerank">;
|
|
468
|
+
export type SearchRankingDebugHint = Pick<SearchDebugHint, "route" | "queryIntent" | "retrieval" | "mcpFusion" | "providerWork" | "semanticPassFailures" | "semanticExpansion" | "rankingProvenance" | "trackedLexical" | "exactRegistry" | "passesUsed" | "candidateLimit" | "mustRetry" | "operatorSummary" | "filterSummary" | "diversitySummary" | "changedFilesBoost" | "rerank">;
|
|
467
469
|
export type SearchFreshnessDebugHint = Pick<SearchDebugHint, "phaseTimingsMs" | "readiness" | "changedCode">;
|
|
470
|
+
export type SearchPassFailureDebugHint = Pick<SearchDebugHint, "semanticPassFailures">;
|
|
468
471
|
export interface SearchResponseHints extends Record<string, unknown> {
|
|
469
472
|
version?: 1;
|
|
470
473
|
noiseMitigation?: SearchNoiseMitigationHint;
|
|
471
|
-
debugSearch?: SearchDebugHint | SearchRankingDebugHint | SearchFreshnessDebugHint;
|
|
474
|
+
debugSearch?: SearchDebugHint | SearchRankingDebugHint | SearchFreshnessDebugHint | SearchPassFailureDebugHint;
|
|
472
475
|
debugSummary?: {
|
|
473
476
|
retrieval: string;
|
|
474
477
|
freshness: FreshnessDecision["mode"] | "skipped_requires_reindex" | "skipped_indexing" | "unknown";
|
package/dist/core/snapshot.d.ts
CHANGED
|
@@ -106,6 +106,12 @@ export declare class SnapshotManager {
|
|
|
106
106
|
getCodebaseStatus(codebasePath: string): CodebaseInfo['status'] | 'not_found';
|
|
107
107
|
getCodebaseInfo(codebasePath: string): CodebaseInfo | undefined;
|
|
108
108
|
getLatestOperation(codebasePath: string): IndexOperationReceipt | undefined;
|
|
109
|
+
/**
|
|
110
|
+
* Read the latest operation directly from the durable shared snapshot.
|
|
111
|
+
* Cross-process waiters use this instead of trusting their startup cache.
|
|
112
|
+
*/
|
|
113
|
+
observeDurableLatestOperation(codebasePath: string): IndexOperationReceipt | undefined;
|
|
114
|
+
operationMatchesRuntimeFingerprint(receipt: IndexOperationReceipt): boolean;
|
|
109
115
|
setLatestOperation(codebasePath: string, receipt: IndexOperationReceipt): void;
|
|
110
116
|
startOperation(lease: RootMutationLease): IndexOperationReceipt;
|
|
111
117
|
transitionOperation(lease: RootMutationLease, phase: IndexOperationPhase): IndexOperationReceipt;
|
package/dist/core/snapshot.js
CHANGED
|
@@ -1175,6 +1175,17 @@ export class SnapshotManager {
|
|
|
1175
1175
|
const receipt = this.latestOperations.get(codebasePath);
|
|
1176
1176
|
return receipt ? structuredClone(receipt) : undefined;
|
|
1177
1177
|
}
|
|
1178
|
+
/**
|
|
1179
|
+
* Read the latest operation directly from the durable shared snapshot.
|
|
1180
|
+
* Cross-process waiters use this instead of trusting their startup cache.
|
|
1181
|
+
*/
|
|
1182
|
+
observeDurableLatestOperation(codebasePath) {
|
|
1183
|
+
const receipt = this.readOperationMapFromDisk().get(codebasePath);
|
|
1184
|
+
return receipt ? structuredClone(receipt) : undefined;
|
|
1185
|
+
}
|
|
1186
|
+
operationMatchesRuntimeFingerprint(receipt) {
|
|
1187
|
+
return fingerprintsEqual(receipt.runtimeFingerprint, this.runtimeFingerprint);
|
|
1188
|
+
}
|
|
1178
1189
|
setLatestOperation(codebasePath, receipt) {
|
|
1179
1190
|
if (!this.isValidOperationReceipt(receipt, codebasePath)) {
|
|
1180
1191
|
throw new Error(`Invalid operation receipt for '${codebasePath}'.`);
|
package/dist/core/sync.d.ts
CHANGED
|
@@ -8,8 +8,10 @@ interface SyncManagerOptions {
|
|
|
8
8
|
now?: () => number;
|
|
9
9
|
onSyncCompleted?: (codebasePath: string, stats: SyncStats, assertMutationCurrent: () => void) => Promise<void> | void;
|
|
10
10
|
mutationLeaseCoordinator?: MutationLeaseCoordinator;
|
|
11
|
+
crossProcessJoinTimeoutMs?: number;
|
|
12
|
+
crossProcessJoinPollMs?: number;
|
|
11
13
|
}
|
|
12
|
-
export type FreshnessDecisionMode = 'synced' | 'skipped_recent' | 'coalesced' | 'skipped_indexing' | 'skipped_requires_reindex' | 'skipped_source_checkpoint_unavailable' | 'skipped_mutation_in_progress' | 'skipped_missing_path' | 'reconciled_ignore_change' | 'ignore_reload_failed';
|
|
14
|
+
export type FreshnessDecisionMode = 'synced' | 'skipped_recent' | 'skipped_source_unchanged' | 'coalesced' | 'skipped_indexing' | 'skipped_requires_reindex' | 'skipped_source_checkpoint_unavailable' | 'skipped_mutation_in_progress' | 'skipped_missing_path' | 'reconciled_ignore_change' | 'ignore_reload_failed';
|
|
13
15
|
export interface FreshnessDecision {
|
|
14
16
|
mode: FreshnessDecisionMode;
|
|
15
17
|
checkedAt: string;
|
|
@@ -83,6 +85,7 @@ interface EnsureFreshnessOptions {
|
|
|
83
85
|
skipIgnoreControlCheck?: boolean;
|
|
84
86
|
mutationLease?: RootMutationLease;
|
|
85
87
|
preparedVectorReceipt?: ProvenVectorGenerationReceipt;
|
|
88
|
+
exactSourceComparisonPaths?: readonly string[];
|
|
86
89
|
}
|
|
87
90
|
export declare class SyncOperationError extends Error {
|
|
88
91
|
readonly operation: IndexOperationReceipt | undefined;
|
|
@@ -115,6 +118,8 @@ export declare class SyncManager {
|
|
|
115
118
|
private readonly now;
|
|
116
119
|
private readonly onSyncCompleted?;
|
|
117
120
|
private readonly mutationLeaseCoordinator?;
|
|
121
|
+
private readonly crossProcessJoinTimeoutMs;
|
|
122
|
+
private readonly crossProcessJoinPollMs;
|
|
118
123
|
constructor(context: Context, snapshotManager: SnapshotManager, options?: SyncManagerOptions);
|
|
119
124
|
private bumpFreshnessEpoch;
|
|
120
125
|
getPreparedReadObservation(codebasePath: string): PreparedReadObservationResult;
|
|
@@ -132,6 +137,7 @@ export declare class SyncManager {
|
|
|
132
137
|
private runIgnoreReconcile;
|
|
133
138
|
private reconcileIgnoreRulesChange;
|
|
134
139
|
private syncCodebase;
|
|
140
|
+
private joinCrossProcessSync;
|
|
135
141
|
handleSyncIndex(): Promise<void>;
|
|
136
142
|
startBackgroundSync(): void;
|
|
137
143
|
stopBackgroundSync(): void;
|
package/dist/core/sync.js
CHANGED
|
@@ -8,6 +8,8 @@ import { BACKGROUND_FRESHNESS_THRESHOLD_MS, BACKGROUND_SYNC_INITIAL_DELAY_MS, BA
|
|
|
8
8
|
import { formatMutationLeaseBlockedMessage, } from "./mutation-lease.js";
|
|
9
9
|
// v1 policy: only root-level control files trigger index-policy reconciliation.
|
|
10
10
|
const IGNORE_RULE_CONTROL_FILES = new Set(['.satoriignore', '.gitignore', 'satori.toml']);
|
|
11
|
+
const DEFAULT_CROSS_PROCESS_JOIN_TIMEOUT_MS = 15000;
|
|
12
|
+
const DEFAULT_CROSS_PROCESS_JOIN_POLL_MS = 25;
|
|
11
13
|
function errorMessage(error, fallback = "unknown_error") {
|
|
12
14
|
if (error instanceof Error && error.message) {
|
|
13
15
|
return error.message;
|
|
@@ -60,6 +62,8 @@ export class SyncManager {
|
|
|
60
62
|
this.now = options.now || (() => Date.now());
|
|
61
63
|
this.onSyncCompleted = options.onSyncCompleted;
|
|
62
64
|
this.mutationLeaseCoordinator = options.mutationLeaseCoordinator;
|
|
65
|
+
this.crossProcessJoinTimeoutMs = Math.max(1, options.crossProcessJoinTimeoutMs ?? DEFAULT_CROSS_PROCESS_JOIN_TIMEOUT_MS);
|
|
66
|
+
this.crossProcessJoinPollMs = Math.max(1, options.crossProcessJoinPollMs ?? DEFAULT_CROSS_PROCESS_JOIN_POLL_MS);
|
|
63
67
|
}
|
|
64
68
|
bumpFreshnessEpoch(codebasePath) {
|
|
65
69
|
this.freshnessEpochs.set(codebasePath, (this.freshnessEpochs.get(codebasePath) ?? 0) + 1);
|
|
@@ -329,6 +333,20 @@ export class SyncManager {
|
|
|
329
333
|
}
|
|
330
334
|
}
|
|
331
335
|
}
|
|
336
|
+
const exactSourceComparisonPaths = options.exactSourceComparisonPaths;
|
|
337
|
+
if (exactSourceComparisonPaths && exactSourceComparisonPaths.length > 0) {
|
|
338
|
+
const compareSourcePaths = this.context.compareSourcePathsToFreshnessCheckpoint;
|
|
339
|
+
if (typeof compareSourcePaths === 'function') {
|
|
340
|
+
const comparison = await compareSourcePaths.call(this.context, codebasePath, exactSourceComparisonPaths, options.preparedVectorReceipt);
|
|
341
|
+
if (comparison.status === 'matches') {
|
|
342
|
+
return {
|
|
343
|
+
mode: 'skipped_source_unchanged',
|
|
344
|
+
checkedAt,
|
|
345
|
+
thresholdMs,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
332
350
|
// 2. Throttling: Skip if recently synced
|
|
333
351
|
const lastSync = this.lastSyncTimes.get(codebasePath) || 0;
|
|
334
352
|
const timeSince = checkedAtMs - lastSync;
|
|
@@ -347,7 +365,9 @@ export class SyncManager {
|
|
|
347
365
|
this.bumpFreshnessEpoch(codebasePath);
|
|
348
366
|
const syncPromise = (async () => {
|
|
349
367
|
try {
|
|
350
|
-
return await this.syncCodebase(codebasePath, options.mutationLease, currentIgnoreControlSignature
|
|
368
|
+
return await this.syncCodebase(codebasePath, options.mutationLease, currentIgnoreControlSignature, {
|
|
369
|
+
exactSourceComparisonPaths: options.exactSourceComparisonPaths,
|
|
370
|
+
});
|
|
351
371
|
}
|
|
352
372
|
catch (e) {
|
|
353
373
|
// Log and rethrow to allow callers to handle/see failure
|
|
@@ -386,6 +406,7 @@ export class SyncManager {
|
|
|
386
406
|
} : undefined,
|
|
387
407
|
activeMutation: outcome.activeMutation,
|
|
388
408
|
operation: outcome.operation,
|
|
409
|
+
errorMessage: outcome.errorMessage,
|
|
389
410
|
};
|
|
390
411
|
}
|
|
391
412
|
async runIgnoreReconcile(codebasePath, coalescedEdits = 1, nextIgnoreControlSignature, existingLease) {
|
|
@@ -576,7 +597,7 @@ export class SyncManager {
|
|
|
576
597
|
};
|
|
577
598
|
}
|
|
578
599
|
}
|
|
579
|
-
async syncCodebase(codebasePath, existingLease, currentIgnoreControlSignature) {
|
|
600
|
+
async syncCodebase(codebasePath, existingLease, currentIgnoreControlSignature, joinRequest = {}) {
|
|
580
601
|
if (this.snapshotManager.getCodebaseStatus(codebasePath) === 'indexing') {
|
|
581
602
|
console.log(`[SYNC] ⏭️ Skipping sync for '${codebasePath}' because indexing is active.`);
|
|
582
603
|
return { mode: 'skipped_indexing' };
|
|
@@ -595,6 +616,9 @@ export class SyncManager {
|
|
|
595
616
|
else {
|
|
596
617
|
const acquired = this.mutationLeaseCoordinator.acquire(codebasePath, 'sync');
|
|
597
618
|
if (!acquired.acquired) {
|
|
619
|
+
if (acquired.activeLease.action === 'sync') {
|
|
620
|
+
return this.joinCrossProcessSync(codebasePath, acquired.activeLease, joinRequest);
|
|
621
|
+
}
|
|
598
622
|
return { mode: 'skipped_mutation_in_progress', activeMutation: acquired.activeLease };
|
|
599
623
|
}
|
|
600
624
|
lease = acquired.lease;
|
|
@@ -778,6 +802,164 @@ export class SyncManager {
|
|
|
778
802
|
}
|
|
779
803
|
}
|
|
780
804
|
}
|
|
805
|
+
async joinCrossProcessSync(codebasePath, activeLease, request) {
|
|
806
|
+
const observeOperation = this.snapshotManager.observeDurableLatestOperation;
|
|
807
|
+
const matchesRuntime = this.snapshotManager.operationMatchesRuntimeFingerprint;
|
|
808
|
+
if (!this.mutationLeaseCoordinator
|
|
809
|
+
|| typeof observeOperation !== 'function'
|
|
810
|
+
|| typeof matchesRuntime !== 'function') {
|
|
811
|
+
return {
|
|
812
|
+
mode: 'skipped_mutation_in_progress',
|
|
813
|
+
activeMutation: activeLease,
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
const deadline = Date.now() + this.crossProcessJoinTimeoutMs;
|
|
817
|
+
let lastOperation;
|
|
818
|
+
while (Date.now() <= deadline) {
|
|
819
|
+
const operation = observeOperation.call(this.snapshotManager, codebasePath);
|
|
820
|
+
if (operation
|
|
821
|
+
&& operation.id === activeLease.operationId
|
|
822
|
+
&& operation.generation === activeLease.generation
|
|
823
|
+
&& operation.action === 'sync'
|
|
824
|
+
&& operation.canonicalRoot === activeLease.canonicalRoot) {
|
|
825
|
+
lastOperation = operation;
|
|
826
|
+
if (!matchesRuntime.call(this.snapshotManager, operation)) {
|
|
827
|
+
return {
|
|
828
|
+
mode: 'coalesced',
|
|
829
|
+
activeMutation: activeLease,
|
|
830
|
+
operation,
|
|
831
|
+
errorMessage: 'The in-flight sync uses an incompatible runtime fingerprint.',
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
if (operation.phase === 'failed' || operation.phase === 'blocked') {
|
|
835
|
+
return {
|
|
836
|
+
mode: 'coalesced',
|
|
837
|
+
activeMutation: activeLease,
|
|
838
|
+
operation,
|
|
839
|
+
errorMessage: `The joined sync ended in terminal phase '${operation.phase}'.`,
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
if (operation.phase === 'completed') {
|
|
843
|
+
let checkpoint = await this.inspectSourceFreshnessCheckpoint(codebasePath);
|
|
844
|
+
if (!checkpoint || checkpoint.status !== 'valid') {
|
|
845
|
+
return {
|
|
846
|
+
mode: 'coalesced',
|
|
847
|
+
activeMutation: activeLease,
|
|
848
|
+
operation,
|
|
849
|
+
errorMessage: 'The joined sync completed without a proven active source checkpoint.',
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
const registeredObservation = this.context.getRegisteredSourceFreshnessCheckpointObservation?.(codebasePath);
|
|
853
|
+
if (registeredObservation !== checkpoint.observationToken
|
|
854
|
+
&& typeof this.context.recreateSynchronizerForCodebase === 'function') {
|
|
855
|
+
try {
|
|
856
|
+
await this.context.recreateSynchronizerForCodebase(codebasePath, undefined, undefined, { requireAuthorityCheckpoint: true });
|
|
857
|
+
}
|
|
858
|
+
catch {
|
|
859
|
+
return {
|
|
860
|
+
mode: 'coalesced',
|
|
861
|
+
activeMutation: activeLease,
|
|
862
|
+
operation,
|
|
863
|
+
errorMessage: 'The joined sync completed, but this runtime could not bind its source checkpoint to the active publication.',
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
checkpoint = await this.inspectSourceFreshnessCheckpoint(codebasePath);
|
|
867
|
+
if (!checkpoint || checkpoint.status !== 'valid') {
|
|
868
|
+
return {
|
|
869
|
+
mode: 'coalesced',
|
|
870
|
+
activeMutation: activeLease,
|
|
871
|
+
operation,
|
|
872
|
+
errorMessage: 'The joined sync source checkpoint changed while this runtime was binding it.',
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
if (this.context.getRegisteredSourceFreshnessCheckpointObservation?.(codebasePath)
|
|
876
|
+
!== checkpoint.observationToken) {
|
|
877
|
+
return {
|
|
878
|
+
mode: 'coalesced',
|
|
879
|
+
activeMutation: activeLease,
|
|
880
|
+
operation,
|
|
881
|
+
errorMessage: 'The joined sync source checkpoint did not bind to the active publication.',
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
const paths = request.exactSourceComparisonPaths;
|
|
886
|
+
if (paths && paths.length > 0) {
|
|
887
|
+
const compareSourcePaths = this.context.compareSourcePathsToFreshnessCheckpoint;
|
|
888
|
+
if (typeof compareSourcePaths !== 'function') {
|
|
889
|
+
return {
|
|
890
|
+
mode: 'coalesced',
|
|
891
|
+
activeMutation: activeLease,
|
|
892
|
+
operation,
|
|
893
|
+
errorMessage: 'The joined sync cannot prove the requested source observation.',
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
const comparison = await compareSourcePaths.call(this.context, codebasePath, paths);
|
|
897
|
+
if (comparison.status !== 'matches') {
|
|
898
|
+
return {
|
|
899
|
+
mode: 'coalesced',
|
|
900
|
+
activeMutation: activeLease,
|
|
901
|
+
operation,
|
|
902
|
+
errorMessage: `The joined sync did not prove the requested source observation (${comparison.status}).`,
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
const finalOperation = observeOperation.call(this.snapshotManager, codebasePath);
|
|
907
|
+
if (!finalOperation
|
|
908
|
+
|| finalOperation.id !== operation.id
|
|
909
|
+
|| finalOperation.generation !== operation.generation
|
|
910
|
+
|| finalOperation.phase !== 'completed') {
|
|
911
|
+
return {
|
|
912
|
+
mode: 'coalesced',
|
|
913
|
+
activeMutation: activeLease,
|
|
914
|
+
...(finalOperation ? { operation: finalOperation } : { operation }),
|
|
915
|
+
errorMessage: 'The durable sync authority changed before the joined result could be accepted.',
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
this.sourceCheckpointStatuses.set(codebasePath, 'valid');
|
|
919
|
+
this.sourceCheckpointObservations.set(codebasePath, checkpoint.observationToken);
|
|
920
|
+
this.lastSyncTimes.set(codebasePath, this.now());
|
|
921
|
+
return {
|
|
922
|
+
mode: 'coalesced',
|
|
923
|
+
activeMutation: activeLease,
|
|
924
|
+
operation,
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
else if (operation && operation.generation >= activeLease.generation) {
|
|
929
|
+
return {
|
|
930
|
+
mode: 'coalesced',
|
|
931
|
+
activeMutation: activeLease,
|
|
932
|
+
operation,
|
|
933
|
+
errorMessage: 'The durable sync operation no longer matches the active mutation lease.',
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
const currentLease = this.mutationLeaseCoordinator.getActiveLease(codebasePath);
|
|
937
|
+
if (!currentLease
|
|
938
|
+
|| currentLease.operationId !== activeLease.operationId
|
|
939
|
+
|| currentLease.generation !== activeLease.generation) {
|
|
940
|
+
const terminal = observeOperation.call(this.snapshotManager, codebasePath);
|
|
941
|
+
if (terminal?.id === activeLease.operationId
|
|
942
|
+
&& terminal.generation === activeLease.generation
|
|
943
|
+
&& terminal.phase === 'completed') {
|
|
944
|
+
lastOperation = terminal;
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
return {
|
|
948
|
+
mode: 'coalesced',
|
|
949
|
+
activeMutation: activeLease,
|
|
950
|
+
...(terminal ? { operation: terminal } : lastOperation ? { operation: lastOperation } : {}),
|
|
951
|
+
errorMessage: 'The in-flight sync lost its durable owner before proving completion.',
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
await new Promise((resolve) => setTimeout(resolve, this.crossProcessJoinPollMs));
|
|
955
|
+
}
|
|
956
|
+
return {
|
|
957
|
+
mode: 'coalesced',
|
|
958
|
+
activeMutation: activeLease,
|
|
959
|
+
...(lastOperation ? { operation: lastOperation } : {}),
|
|
960
|
+
errorMessage: 'Timed out waiting for the in-flight sync to prove completion.',
|
|
961
|
+
};
|
|
962
|
+
}
|
|
781
963
|
async handleSyncIndex() {
|
|
782
964
|
const indexedCodebases = this.snapshotManager.getIndexedCodebases();
|
|
783
965
|
if (indexedCodebases.length === 0)
|
|
@@ -250,6 +250,7 @@ export class ToolResponseBuilders {
|
|
|
250
250
|
return null;
|
|
251
251
|
case "synced":
|
|
252
252
|
case "skipped_recent":
|
|
253
|
+
case "skipped_source_unchanged":
|
|
253
254
|
case "skipped_source_checkpoint_unavailable":
|
|
254
255
|
case "reconciled_ignore_change":
|
|
255
256
|
case "skipped_mutation_in_progress":
|
|
@@ -202,6 +202,13 @@ const buildSearchSchema = (ctx) => z.object({
|
|
|
202
202
|
message: "disclosureLimit cannot exceed limit.",
|
|
203
203
|
});
|
|
204
204
|
}
|
|
205
|
+
if (parseSearchOperators(value.query).semanticQuery.trim().length === 0) {
|
|
206
|
+
refinementContext.addIssue({
|
|
207
|
+
code: z.ZodIssueCode.custom,
|
|
208
|
+
path: ["query"],
|
|
209
|
+
message: "Operator-only search requires semantic text or a positive must:, path:, or lang: value.",
|
|
210
|
+
});
|
|
211
|
+
}
|
|
205
212
|
});
|
|
206
213
|
export const searchCodebaseTool = {
|
|
207
214
|
name: "search_codebase",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zokizuan/satori-mcp",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.2.0",
|
|
4
4
|
"description": "Repo-aware MCP tools for AI coding agents: intent search, symbol navigation, advisory call graphs, exact reads, and freshness-aware recovery.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"jsonc-parser": "^3.3.1",
|
|
16
16
|
"zod": "^3.25.55",
|
|
17
17
|
"zod-to-json-schema": "^3.25.1",
|
|
18
|
-
"@zokizuan/satori-core": "3.
|
|
18
|
+
"@zokizuan/satori-core": "3.1.0"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@types/node": "^20.0.0",
|