@zokizuan/satori-mcp 6.1.0 → 6.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +11 -2
  2. package/dist/core/backend-diagnostics.d.ts +17 -0
  3. package/dist/core/backend-diagnostics.js +44 -0
  4. package/dist/core/call-graph.d.ts +1 -0
  5. package/dist/core/call-graph.js +1 -1
  6. package/dist/core/canonical-symbol-identity.js +33 -5
  7. package/dist/core/handlers.d.ts +2 -0
  8. package/dist/core/handlers.js +37 -6
  9. package/dist/core/manage-indexing-handlers.js +41 -4
  10. package/dist/core/manage-types.d.ts +1 -1
  11. package/dist/core/navigation-handlers.js +0 -1
  12. package/dist/core/python-call-fallback.js +88 -6
  13. package/dist/core/relationship-backed-call-graph.d.ts +2 -2
  14. package/dist/core/relationship-backed-call-graph.js +95 -34
  15. package/dist/core/search-debug-helpers.js +1 -0
  16. package/dist/core/search-exact-fast-path.js +1 -1
  17. package/dist/core/search-execution.d.ts +4 -1
  18. package/dist/core/search-execution.js +21 -7
  19. package/dist/core/search-frontdoor.js +1 -0
  20. package/dist/core/search-query-planning.js +10 -11
  21. package/dist/core/search-query-support.d.ts +1 -1
  22. package/dist/core/search-query-support.js +2 -2
  23. package/dist/core/search-response-envelopes.d.ts +2 -2
  24. package/dist/core/search-response-helpers.d.ts +3 -2
  25. package/dist/core/search-response-helpers.js +5 -3
  26. package/dist/core/search-result-finalization.js +6 -3
  27. package/dist/core/search-types.d.ts +6 -2
  28. package/dist/core/snapshot.d.ts +6 -0
  29. package/dist/core/snapshot.js +11 -0
  30. package/dist/core/sync.d.ts +20 -1
  31. package/dist/core/sync.js +304 -51
  32. package/dist/core/tool-response-builders.js +1 -0
  33. package/dist/index.js +31 -4
  34. package/dist/server/provider-runtime.d.ts +3 -0
  35. package/dist/server/provider-runtime.js +23 -3
  36. package/dist/server/shared-runtime-client.d.ts +11 -0
  37. package/dist/server/shared-runtime-client.js +360 -0
  38. package/dist/server/shared-runtime-host.d.ts +24 -0
  39. package/dist/server/shared-runtime-host.js +322 -0
  40. package/dist/server/shared-runtime-identity.d.ts +51 -0
  41. package/dist/server/shared-runtime-identity.js +160 -0
  42. package/dist/server/shared-runtime-lifecycle.d.ts +33 -0
  43. package/dist/server/shared-runtime-lifecycle.js +259 -0
  44. package/dist/server/shared-runtime-transport.d.ts +28 -0
  45. package/dist/server/shared-runtime-transport.js +158 -0
  46. package/dist/server/shared-runtime.d.ts +83 -0
  47. package/dist/server/shared-runtime.js +300 -0
  48. package/dist/server/start-server.d.ts +9 -32
  49. package/dist/server/start-server.js +15 -142
  50. package/dist/tools/search_codebase.js +7 -0
  51. package/package.json +2 -2
package/README.md CHANGED
@@ -7,12 +7,18 @@ Most users should install Satori through `@zokizuan/satori-cli`. The installer w
7
7
  ## Install
8
8
 
9
9
  ```bash
10
- npx -y @zokizuan/satori-cli@latest install --client all --runtime offline
11
- npx -y @zokizuan/satori-cli@latest doctor
10
+ npm install -g @zokizuan/satori-cli@latest
11
+ satori install --client all
12
+ satori doctor
12
13
  ```
13
14
 
14
15
  The local Potion runtime currently supports Linux x64, including Windows through WSL2. Connected Voyage and explicit local Ollama configurations are also available. See the [main README](https://github.com/ham-zax/satori#quick-start) for runtime choices.
15
16
 
17
+ When installed through the CLI, compatible offline Potion + LanceDB clients
18
+ share one private local host, provider/LanceDB state, and one Potion worker. Each
19
+ client remains an independent MCP session. Direct `npx @zokizuan/satori-mcp`
20
+ execution is still isolated and does not join the managed host.
21
+
16
22
  Direct package execution is intended for inspection and custom harnesses:
17
23
 
18
24
  ```bash
@@ -64,6 +70,9 @@ In a fresh two-task OpenCode comparison where both arms answered correctly, Sato
64
70
  - Inbound call-graph evidence is advisory and should be verified before blast-radius edits.
65
71
  - Provider, model, dimensions, projection, and vector backend are persisted compatibility identities; changing them requires a reindex.
66
72
  - Multiple incompatible live Satori runtimes are blocked from mutating the same publication.
73
+ - Managed offline Potion + LanceDB clients on Linux x64/WSL2 share one private
74
+ local host. Connected providers, Milvus, and explicit Ollama runtimes keep
75
+ the direct per-client lifecycle.
67
76
  - Offline neural reranking is not shipped today. The candidate boundary permits a future complete-set local scorer to fail back atomically to exact + BM25 + single-vector ordering.
68
77
 
69
78
  ## Development
@@ -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
  }
@@ -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
  ]);
@@ -8,6 +8,25 @@ export function buildCanonicalSymbolRegistryView(symbols) {
8
8
  }
9
9
  return symbolsByKey;
10
10
  }
11
+ function spanContains(parent, child) {
12
+ if (parent.file !== child.file) {
13
+ return false;
14
+ }
15
+ if (parent.span.startByte !== undefined
16
+ && parent.span.endByte !== undefined
17
+ && child.span.startByte !== undefined
18
+ && child.span.endByte !== undefined) {
19
+ return parent.span.startByte <= child.span.startByte
20
+ && child.span.endByte <= parent.span.endByte;
21
+ }
22
+ return parent.span.startLine <= child.span.startLine
23
+ && child.span.endLine <= parent.span.endLine;
24
+ }
25
+ function spanSize(symbol, useBytes) {
26
+ return useBytes
27
+ ? Math.max(0, symbol.span.endByte - symbol.span.startByte)
28
+ : Math.max(0, symbol.span.endLine - symbol.span.startLine);
29
+ }
11
30
  function resolveParent(input) {
12
31
  const { symbol } = input;
13
32
  if (!symbol.parentKey) {
@@ -17,17 +36,26 @@ function resolveParent(input) {
17
36
  : "missing",
18
37
  };
19
38
  }
20
- const candidates = (input.registry.get(symbol.parentKey) || [])
21
- .filter((candidate) => candidate.symbolInstanceId !== symbol.symbolInstanceId);
22
- if (candidates.length === 0) {
39
+ const containingCandidates = (input.registry.get(symbol.parentKey) || [])
40
+ .filter((candidate) => (candidate.symbolInstanceId !== symbol.symbolInstanceId
41
+ && spanContains(candidate, symbol)));
42
+ if (containingCandidates.length === 0) {
23
43
  return { state: "missing" };
24
44
  }
25
- if (candidates.length > 1) {
45
+ const byteCandidates = containingCandidates.filter((candidate) => (candidate.span.startByte !== undefined
46
+ && candidate.span.endByte !== undefined
47
+ && symbol.span.startByte !== undefined
48
+ && symbol.span.endByte !== undefined));
49
+ const candidates = byteCandidates.length > 0 ? byteCandidates : containingCandidates;
50
+ const useBytes = byteCandidates.length > 0;
51
+ const innermostSize = Math.min(...candidates.map((candidate) => spanSize(candidate, useBytes)));
52
+ const innermost = candidates.filter((candidate) => spanSize(candidate, useBytes) === innermostSize);
53
+ if (innermost.length !== 1) {
26
54
  return { state: "ambiguous" };
27
55
  }
28
56
  return {
29
57
  state: "resolved",
30
- parentSymbolId: candidates[0].symbolInstanceId,
58
+ parentSymbolId: innermost[0].symbolInstanceId,
31
59
  };
32
60
  }
33
61
  export function projectCanonicalSymbolIdentity(input) {
@@ -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;
@@ -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, preparedRead?.vectorReceipt
2429
- ? { preparedVectorReceipt: preparedRead.vectorReceipt }
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: "call_graph_rebuild_in_progress",
789
+ basis: "generation_proof_in_progress",
785
790
  },
786
791
  };
787
- await this.host.rebuildCallGraphForIndex(absolutePath, assertMutationCurrent);
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: "navigation_and_call_graph_rebuilt",
823
+ basis: "activated_generation_proven",
794
824
  },
795
825
  };
796
826
  try {
@@ -838,6 +868,13 @@ export class ManageIndexingHandlers {
838
868
  }
839
869
  else {
840
870
  const operation = persistOperation("blocked");
871
+ if (result.reason === "repair_proof_limit") {
872
+ return this.host.manageResponse("repair", absolutePath, "blocked", result.message, {
873
+ reason: "repair_proof_limit",
874
+ repairProof: result.proof,
875
+ ...(operation ? { operation } : {}),
876
+ });
877
+ }
841
878
  const createHint = this.host.buildCreateHint(absolutePath);
842
879
  return this.host.manageResponse("repair", absolutePath, "blocked", result.message, {
843
880
  reason: result.reason || "needs_create",
@@ -853,7 +890,7 @@ export class ManageIndexingHandlers {
853
890
  }
854
891
  catch (error) {
855
892
  console.error("Error in handleRepairIndex:", error);
856
- if (lastRepairProof?.navigation.basis === "call_graph_rebuild_in_progress") {
893
+ if (lastRepairProof?.navigation.basis === "generation_proof_in_progress") {
857
894
  lastRepairProof = {
858
895
  ...lastRepairProof,
859
896
  navigation: {
@@ -7,7 +7,7 @@ export type ManageIndexAction = (typeof MANAGE_INDEX_ACTIONS)[number];
7
7
  export type ManageIndexStatus = "ok" | "not_ready" | "not_indexed" | "requires_reindex" | "blocked" | "error";
8
8
  export declare const MANAGE_INDEX_STATUS_DETAILS: readonly ["summary", "capabilities", "diagnostics", "full"];
9
9
  export type ManageIndexStatusDetail = (typeof MANAGE_INDEX_STATUS_DETAILS)[number];
10
- export type ManageIndexReason = "indexing" | "not_indexed" | "requires_reindex" | "unnecessary_reindex_ignore_only" | "preflight_unknown" | "backend_timeout" | "remote_delete_pending" | "missing_provider_config" | "vector_backend_unavailable" | "runtime_owner_conflict" | "mutation_in_progress" | "needs_create";
10
+ export type ManageIndexReason = "indexing" | "not_indexed" | "requires_reindex" | "unnecessary_reindex_ignore_only" | "preflight_unknown" | "backend_timeout" | "remote_delete_pending" | "missing_provider_config" | "vector_backend_unavailable" | "runtime_owner_conflict" | "mutation_in_progress" | "needs_create" | "repair_proof_limit";
11
11
  export type VectorBackendResponseCode = "ZILLIZ_CLUSTER_STOPPED" | "VECTOR_BACKEND_AUTH_FAILED" | "VECTOR_BACKEND_UNREACHABLE" | "VECTOR_BACKEND_TIMEOUT" | "VECTOR_BACKEND_CONNECTION_CLOSED";
12
12
  export type ManageReindexPreflightOutcome = "reindex_required" | "reindex_unnecessary_ignore_only" | "unknown" | "probe_failed";
13
13
  export interface ManageIndexToolHint {
@@ -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
- let startIndex = definitionIndex;
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 (!trimmed.startsWith("@") || countPythonIndent(line) !== indent) {
153
- break;
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 startIndex;
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;