@zokizuan/satori-mcp 4.11.13 → 4.11.16

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 (58) hide show
  1. package/README.md +26 -21
  2. package/dist/cli/args.d.ts +3 -12
  3. package/dist/cli/args.js +6 -47
  4. package/dist/cli/index.d.ts +0 -3
  5. package/dist/cli/index.js +2 -21
  6. package/dist/config.d.ts +2 -0
  7. package/dist/config.js +21 -3
  8. package/dist/core/call-graph.d.ts +2 -0
  9. package/dist/core/handlers.d.ts +1 -1
  10. package/dist/core/handlers.js +30 -8
  11. package/dist/core/indexing-recovery.d.ts +2 -1
  12. package/dist/core/indexing-recovery.js +6 -2
  13. package/dist/core/manage-indexing-handlers.d.ts +4 -3
  14. package/dist/core/manage-indexing-handlers.js +30 -9
  15. package/dist/core/manage-maintenance-handlers.d.ts +7 -4
  16. package/dist/core/manage-maintenance-handlers.js +57 -7
  17. package/dist/core/manage-types.d.ts +6 -1
  18. package/dist/core/manage-types.js +9 -1
  19. package/dist/core/navigation-handlers.d.ts +6 -0
  20. package/dist/core/navigation-handlers.js +79 -12
  21. package/dist/core/registry-file-outline.d.ts +1 -1
  22. package/dist/core/registry-file-outline.js +3 -2
  23. package/dist/core/relationship-backed-call-graph.d.ts +18 -0
  24. package/dist/core/relationship-backed-call-graph.js +151 -6
  25. package/dist/core/runtime-owner.d.ts +43 -1
  26. package/dist/core/runtime-owner.js +128 -3
  27. package/dist/core/search-exact-fast-path.js +2 -0
  28. package/dist/core/search-execution.d.ts +10 -0
  29. package/dist/core/search-execution.js +36 -1
  30. package/dist/core/search-frontdoor.js +11 -2
  31. package/dist/core/search-group-ordering.d.ts +3 -0
  32. package/dist/core/search-group-ordering.js +83 -7
  33. package/dist/core/search-group-results.js +27 -1
  34. package/dist/core/search-ranking-policy.js +2 -1
  35. package/dist/core/search-response-helpers.d.ts +27 -1
  36. package/dist/core/search-response-helpers.js +116 -11
  37. package/dist/core/search-result-finalization.js +3 -2
  38. package/dist/core/search-types.d.ts +17 -0
  39. package/dist/core/tool-response-builders.d.ts +2 -0
  40. package/dist/core/tool-response-builders.js +3 -0
  41. package/dist/core/tracked-root-readiness.js +2 -1
  42. package/dist/server/provider-runtime.js +1 -0
  43. package/dist/tools/call_graph.js +19 -7
  44. package/dist/tools/file_outline.js +18 -6
  45. package/dist/tools/list_codebases.js +82 -33
  46. package/dist/tools/manage_index.d.ts +3 -0
  47. package/dist/tools/manage_index.js +77 -5
  48. package/dist/tools/read_file.js +151 -35
  49. package/dist/tools/search_codebase.js +15 -3
  50. package/dist/tools/types.d.ts +7 -0
  51. package/dist/tools/types.js +28 -0
  52. package/dist/utils.d.ts +36 -1
  53. package/dist/utils.js +83 -7
  54. package/package.json +3 -3
  55. package/dist/cli/install.d.ts +0 -46
  56. package/dist/cli/install.js +0 -928
  57. package/dist/cli/package-installability.d.ts +0 -14
  58. package/dist/cli/package-installability.js +0 -124
@@ -2,7 +2,7 @@ import * as fs from "fs";
2
2
  import * as crypto from "node:crypto";
3
3
  import { COLLECTION_LIMIT_MESSAGE, deleteCollectionWithVerification, RemoteCollectionDeletePendingError, } from "@zokizuan/satori-core";
4
4
  import { classifyVectorBackendError, } from "./backend-diagnostics.js";
5
- import { ensureAbsolutePath, trackCodebasePath } from "../utils.js";
5
+ import { absolutePathOrRaw, requireAbsoluteFilesystemPath, trackCodebasePath } from "../utils.js";
6
6
  function isMatchingVerifiedFingerprint(info, runtimeFingerprint) {
7
7
  if (info?.fingerprintSource !== "verified") {
8
8
  return undefined;
@@ -144,7 +144,11 @@ export class ManageIndexingHandlers {
144
144
  const requestedDropCollection = typeof zillizDropCollection === "string" ? zillizDropCollection.trim() : undefined;
145
145
  let dropSummaryLine = "";
146
146
  try {
147
- const absolutePath = ensureAbsolutePath(codebasePath);
147
+ const absolutePathResult = requireAbsoluteFilesystemPath(codebasePath, "path");
148
+ if (!absolutePathResult.ok) {
149
+ return this.host.manageResponse(manageAction, codebasePath, "error", absolutePathResult.message, preflightOptions);
150
+ }
151
+ const absolutePath = absolutePathResult.absolutePath;
148
152
  if (!fs.existsSync(absolutePath)) {
149
153
  return this.host.manageResponse(manageAction, absolutePath, "error", `Error: Path '${absolutePath}' does not exist. Original input: '${codebasePath}'`, preflightOptions);
150
154
  }
@@ -314,7 +318,7 @@ export class ManageIndexingHandlers {
314
318
  const ignoreInfo = customIgnorePatterns.length > 0
315
319
  ? `\nUsing ${customIgnorePatterns.length} custom ignore patterns: ${customIgnorePatterns.join(", ")}`
316
320
  : "";
317
- return this.host.manageResponse(manageAction, absolutePath, "ok", `Started background indexing for codebase '${absolutePath}'.${pathInfo}${dropSummaryLine}${extensionInfo}${ignoreInfo}\n\nIndexing is running in the background. You can search the codebase while indexing is in progress, but results may be incomplete until indexing completes.`, preflightOptions);
321
+ return this.host.manageResponse(manageAction, absolutePath, "ok", `Started background indexing for codebase '${absolutePath}'.${pathInfo}${dropSummaryLine}${extensionInfo}${ignoreInfo}\n\nIndexing is running in the background. Search and navigation are blocked until indexing completes. Poll manage_index with {"action":"status","path":"${absolutePath}"} (or wait for completion); do not search for partial results while status is indexing.`, preflightOptions);
318
322
  }
319
323
  catch (error) {
320
324
  console.error("Error in handleIndexCodebase:", error);
@@ -325,14 +329,18 @@ export class ManageIndexingHandlers {
325
329
  const humanText = preservesLocalState
326
330
  ? `${vectorBackendDiagnostic.message} ${errorMessage}`
327
331
  : vectorBackendDiagnostic.message;
328
- return this.host.manageVectorBackendResponse(manageAction, ensureAbsolutePath(codebasePath), vectorBackendDiagnostic, humanText);
332
+ return this.host.manageVectorBackendResponse(manageAction, absolutePathOrRaw(codebasePath), vectorBackendDiagnostic, humanText);
329
333
  }
330
- return this.host.manageResponse(manageAction, ensureAbsolutePath(codebasePath), "error", `Error starting indexing: ${formatUnknownError(error)}`, preflightOptions);
334
+ return this.host.manageResponse(manageAction, absolutePathOrRaw(codebasePath), "error", `Error starting indexing: ${formatUnknownError(error)}`, preflightOptions);
331
335
  }
332
336
  }
333
337
  async handleReindexCodebase(args) {
334
338
  const { path: codebasePath, customExtensions, ignorePatterns, zillizDropCollection, allowUnnecessaryReindex } = args;
335
- const absolutePath = ensureAbsolutePath(codebasePath);
339
+ const absolutePathResult = requireAbsoluteFilesystemPath(codebasePath, "path");
340
+ if (!absolutePathResult.ok) {
341
+ return this.host.manageResponse("reindex", codebasePath, "error", absolutePathResult.message);
342
+ }
343
+ const absolutePath = absolutePathResult.absolutePath;
336
344
  const runtimeOwnerConflict = await this.host.buildRuntimeOwnerConflictResponseIfBlocked("reindex", absolutePath);
337
345
  if (runtimeOwnerConflict) {
338
346
  return runtimeOwnerConflict;
@@ -374,7 +382,11 @@ export class ManageIndexingHandlers {
374
382
  }
375
383
  let absolutePath = codebasePath;
376
384
  try {
377
- absolutePath = ensureAbsolutePath(codebasePath);
385
+ const absolutePathResult = requireAbsoluteFilesystemPath(codebasePath, "path");
386
+ if (!absolutePathResult.ok) {
387
+ return this.host.manageResponse("repair", codebasePath, "error", absolutePathResult.message);
388
+ }
389
+ absolutePath = absolutePathResult.absolutePath;
378
390
  if (!fs.existsSync(absolutePath)) {
379
391
  return this.host.manageResponse("repair", absolutePath, "error", `Error: Path '${absolutePath}' does not exist. Original input: '${codebasePath}'`);
380
392
  }
@@ -491,16 +503,25 @@ export class ManageIndexingHandlers {
491
503
  catch (pruneError) {
492
504
  console.warn(`[BACKGROUND-INDEX] Failed to retire superseded generations for '${absolutePath}': ${formatUnknownError(pruneError)}`);
493
505
  }
506
+ // indexStatus is carried on stats (completed | limit_reached). limit_reached remains
507
+ // searchable only when core wrote a completion marker for partial vector proof;
508
+ // navigation still fails closed via indexStatus checks (partial_index_navigation_unavailable).
494
509
  this.host.snapshotManager.setCodebaseIndexed(absolutePath, stats, this.host.runtimeFingerprint, "verified", targetCollectionName);
495
510
  this.host.snapshotManager.setCodebaseIndexManifest(absolutePath, this.host.getContextTrackedRelativePaths(absolutePath));
496
511
  this.host.setIndexingStats({ indexedFiles: stats.indexedFiles, totalChunks: stats.totalChunks });
497
512
  await this.host.syncManager.recordCurrentIgnoreControlSignature(absolutePath);
498
513
  this.host.saveSnapshotIfSupported();
499
- await this.host.rebuildCallGraphForIndex(absolutePath);
514
+ // Full navigation rebuild only for completed indexes; partial indexes have no registry seal.
515
+ if (stats.status === "completed") {
516
+ await this.host.rebuildCallGraphForIndex(absolutePath);
517
+ }
500
518
  await this.host.touchWatchedCodebase(absolutePath);
501
519
  let message = `Background indexing completed for '${absolutePath}'.\nIndexed ${stats.indexedFiles} files, ${stats.totalChunks} chunks.`;
502
520
  if (stats.status === "limit_reached") {
503
- message += "\n⚠️ Warning: Indexing stopped because the chunk limit (450,000) was reached. The index may be incomplete.";
521
+ message += "\n⚠️ Warning: Indexing stopped because the chunk limit (450,000) was reached."
522
+ + " Search may return incomplete results with SEARCH_PARTIAL_INDEX warnings."
523
+ + " file_outline/call_graph are unavailable until a full reindex completes successfully."
524
+ + " This is not a fully complete index.";
504
525
  }
505
526
  console.log(`[BACKGROUND-INDEX] ${message}`);
506
527
  }
@@ -3,6 +3,8 @@ import type { SnapshotCorruptionWarning, SnapshotManager } from "./snapshot.js";
3
3
  import type { SyncManager } from "./sync.js";
4
4
  import type { TrackedRootReadiness } from "./tracked-root-readiness.js";
5
5
  import { type VectorBackendDiagnostic } from "./backend-diagnostics.js";
6
+ import type { ManageIndexAction, ManageIndexStatus } from "./manage-types.js";
7
+ import { type RuntimeOwnerMutationAction, type RuntimeOwnersSummary } from "./runtime-owner.js";
6
8
  type ToolArgs = Record<string, unknown>;
7
9
  type ToolTextResponse = {
8
10
  content: Array<{
@@ -11,7 +13,6 @@ type ToolTextResponse = {
11
13
  }>;
12
14
  isError?: boolean;
13
15
  };
14
- type ManageIndexStatus = "ok" | "error" | "not_ready" | "not_indexed" | "blocked" | "requires_reindex";
15
16
  type ManageMaintenanceHandlersHost = {
16
17
  context: Pick<Context, "clearIndex">;
17
18
  snapshotManager: Pick<SnapshotManager, "removeCodebaseCompletely">;
@@ -23,11 +24,11 @@ type ManageMaintenanceHandlersHost = {
23
24
  getSnapshotCodebaseStatus(codebasePath: string): string;
24
25
  getSnapshotCodebaseInfo(codebasePath: string): Record<string, unknown> | undefined;
25
26
  getSnapshotCorruptionWarning(): SnapshotCorruptionWarning | undefined;
26
- buildRuntimeOwnerConflictResponseIfBlocked(action: "clear" | "sync", codebasePath: string): Promise<ToolTextResponse | null>;
27
+ buildRuntimeOwnerConflictResponseIfBlocked(action: Extract<RuntimeOwnerMutationAction, "clear" | "sync">, codebasePath: string): Promise<ToolTextResponse | null>;
27
28
  recoverStaleIndexingStateIfNeeded(codebasePath: string): Promise<void>;
28
- manageResponse(action: string, path: string, status: ManageIndexStatus | string, message: string, options?: Record<string, unknown>): ToolTextResponse;
29
+ manageResponse(action: ManageIndexAction | string, path: string, status: ManageIndexStatus | string, message: string, options?: Record<string, unknown>): ToolTextResponse;
29
30
  buildCreateHint(codebasePath: string): Record<string, unknown>;
30
- buildManageActionBlockedMessage(codebasePath: string, action: "clear" | "sync"): string;
31
+ buildManageActionBlockedMessage(codebasePath: string, action: Extract<RuntimeOwnerMutationAction, "clear" | "sync">): string;
31
32
  buildStatusHint(codebasePath: string): Record<string, unknown>;
32
33
  getManageRetryAfterMs(): number;
33
34
  buildIndexingMetadata(codebasePath: string): Record<string, unknown> | undefined;
@@ -51,6 +52,8 @@ type ManageMaintenanceHandlersHost = {
51
52
  buildReindexHint(codebasePath: string): Record<string, unknown>;
52
53
  touchWatchedCodebase(codebasePath: string): Promise<void>;
53
54
  manageVectorBackendResponse(action: string, path: string, diagnostic: VectorBackendDiagnostic, humanText?: string): ToolTextResponse;
55
+ /** Optional live MCP runtime owner summary for status diagnostics. */
56
+ getLiveOwnersSummary?(): Promise<RuntimeOwnersSummary | null> | RuntimeOwnersSummary | null;
54
57
  };
55
58
  export declare class ManageMaintenanceHandlers {
56
59
  private readonly host;
@@ -1,8 +1,9 @@
1
1
  import * as fs from "fs";
2
- import { COLLECTION_LIMIT_MESSAGE, RemoteCollectionDeletePendingError, } from "@zokizuan/satori-core";
2
+ import { COLLECTION_LIMIT_MESSAGE, RemoteCollectionDeletePendingError, formatSymbolQualityMarker, resolveSymbolQualitySummary, } from "@zokizuan/satori-core";
3
3
  import { WARNING_CODES } from "./warnings.js";
4
4
  import { classifyVectorBackendError, } from "./backend-diagnostics.js";
5
- import { ensureAbsolutePath } from "../utils.js";
5
+ import { requireAbsoluteFilesystemPath } from "../utils.js";
6
+ import { formatRuntimeOwnersStatusLine, } from "./runtime-owner.js";
6
7
  function collectErrorFragments(value, output, visited, depth = 0) {
7
8
  if (value === null || value === undefined || depth > 4 || output.length >= 8) {
8
9
  return;
@@ -78,7 +79,11 @@ export class ManageMaintenanceHandlers {
78
79
  }
79
80
  async handleClearIndex(args) {
80
81
  const codebasePath = typeof args.path === "string" ? args.path : "";
81
- const requestedPath = ensureAbsolutePath(codebasePath);
82
+ const absolutePathResult = requireAbsoluteFilesystemPath(codebasePath, "path");
83
+ if (!absolutePathResult.ok) {
84
+ return this.host.manageResponse("clear", codebasePath, "error", absolutePathResult.message);
85
+ }
86
+ const requestedPath = absolutePathResult.absolutePath;
82
87
  if (this.host.getSnapshotAllCodebases().length === 0) {
83
88
  return this.host.manageResponse("clear", requestedPath, "not_indexed", "No codebases are currently tracked.", { reason: "not_indexed" });
84
89
  }
@@ -167,7 +172,11 @@ export class ManageMaintenanceHandlers {
167
172
  }
168
173
  async handleGetIndexingStatus(args) {
169
174
  const codebasePath = typeof args.path === "string" ? args.path : "";
170
- const requestedPath = ensureAbsolutePath(codebasePath);
175
+ const absolutePathResult = requireAbsoluteFilesystemPath(codebasePath, "path");
176
+ if (!absolutePathResult.ok) {
177
+ return this.host.manageResponse("status", codebasePath, "error", absolutePathResult.message);
178
+ }
179
+ const requestedPath = absolutePathResult.absolutePath;
171
180
  try {
172
181
  const absolutePath = requestedPath;
173
182
  if (!fs.existsSync(absolutePath)) {
@@ -281,7 +290,16 @@ export class ManageMaintenanceHandlers {
281
290
  const info = trackedRootState.root.info || this.host.getSnapshotCodebaseInfo(trackedRootState.root.path);
282
291
  switch (status) {
283
292
  case "indexed":
284
- if (info?.status === "indexed") {
293
+ if (info?.status === "indexed" && info.indexStatus === "limit_reached") {
294
+ statusMessage = `⚠️ Codebase '${trackedRootState.root.path}' is partially indexed (limit_reached).`;
295
+ statusMessage += `\n📊 Statistics: ${info.indexedFiles} files, ${info.totalChunks} chunks`;
296
+ statusMessage += `\n📅 Status: ${info.indexStatus}`;
297
+ statusMessage += `\nSearch may return incomplete results; file_outline/call_graph are unavailable until a full reindex completes.`;
298
+ if (typeof info.lastUpdated === "string") {
299
+ statusMessage += `\n🕐 Last updated: ${new Date(info.lastUpdated).toLocaleString()}`;
300
+ }
301
+ }
302
+ else if (info?.status === "indexed") {
285
303
  statusMessage = `✅ Codebase '${trackedRootState.root.path}' is fully indexed and ready for search.`;
286
304
  statusMessage += `\n📊 Statistics: ${info.indexedFiles} files, ${info.totalChunks} chunks`;
287
305
  statusMessage += `\n📅 Status: ${info.indexStatus}`;
@@ -319,6 +337,17 @@ export class ManageMaintenanceHandlers {
319
337
  statusMessage += `\n⚠️ Completion proof check is temporarily unavailable (probe_failed); keeping local status.`;
320
338
  warnings.push(WARNING_CODES.IGNORE_POLICY_PROBE_FAILED);
321
339
  }
340
+ // F9: observed symbol quality from registry (not parser-cause diagnosis).
341
+ let symbolQuality;
342
+ // Attach observed quality for lifecycle statuses that refer to a real root path.
343
+ if (envelopeStatus === "ok" || envelopeStatus === "not_ready" || envelopeStatus === "not_indexed") {
344
+ symbolQuality = await resolveSymbolQualitySummary({
345
+ normalizedRootPath: envelopePath,
346
+ });
347
+ if (envelopeStatus === "ok") {
348
+ statusMessage += `\n🧭 ${formatSymbolQualityMarker(symbolQuality)}: ${symbolQuality.message}`;
349
+ }
350
+ }
322
351
  const pathInfo = codebasePath !== envelopePath
323
352
  ? `\nNote: Input path '${codebasePath}' was resolved to absolute path '${envelopePath}'`
324
353
  : "";
@@ -335,10 +364,27 @@ export class ManageMaintenanceHandlers {
335
364
  snapshotCorruption: snapshotCorruptionWarning,
336
365
  };
337
366
  }
338
- return this.host.manageResponse("status", envelopePath, envelopeStatus, statusMessage + compatibilityStatus + pathInfo + snapshotWarningText, {
367
+ let runtimeOwnersLine = "";
368
+ if (typeof this.host.getLiveOwnersSummary === "function") {
369
+ try {
370
+ const ownersSummary = await this.host.getLiveOwnersSummary();
371
+ if (ownersSummary) {
372
+ runtimeOwnersLine = `\n👥 ${formatRuntimeOwnersStatusLine(ownersSummary)}`;
373
+ envelopeHints = {
374
+ ...(envelopeHints || {}),
375
+ runtimeOwners: ownersSummary,
376
+ };
377
+ }
378
+ }
379
+ catch {
380
+ // Diagnostic only; never fail status on owner registry issues.
381
+ }
382
+ }
383
+ return this.host.manageResponse("status", envelopePath, envelopeStatus, statusMessage + compatibilityStatus + pathInfo + snapshotWarningText + runtimeOwnersLine, {
339
384
  reason: envelopeReason,
340
385
  hints: envelopeHints,
341
386
  warnings,
387
+ ...(symbolQuality ? { symbolQuality } : {}),
342
388
  });
343
389
  }
344
390
  catch (error) {
@@ -347,7 +393,11 @@ export class ManageMaintenanceHandlers {
347
393
  }
348
394
  async handleSyncCodebase(args) {
349
395
  const codebasePath = typeof args.path === "string" ? args.path : "";
350
- const requestedPath = ensureAbsolutePath(codebasePath);
396
+ const absolutePathResult = requireAbsoluteFilesystemPath(codebasePath, "path");
397
+ if (!absolutePathResult.ok) {
398
+ return this.host.manageResponse("sync", codebasePath, "error", absolutePathResult.message);
399
+ }
400
+ const requestedPath = absolutePathResult.absolutePath;
351
401
  try {
352
402
  const absolutePath = requestedPath;
353
403
  if (!fs.existsSync(absolutePath)) {
@@ -1,5 +1,8 @@
1
+ import type { SymbolQualitySummary } from "@zokizuan/satori-core";
1
2
  import { WarningCode } from "./warnings.js";
2
- export type ManageIndexAction = "create" | "reindex" | "sync" | "status" | "clear" | "repair";
3
+ /** Public manage_index action set (SSOT for schema, docs, and contract tests). */
4
+ export declare const MANAGE_INDEX_ACTIONS: readonly ["create", "reindex", "sync", "status", "clear", "repair"];
5
+ export type ManageIndexAction = (typeof MANAGE_INDEX_ACTIONS)[number];
3
6
  export type ManageIndexStatus = "ok" | "not_ready" | "not_indexed" | "requires_reindex" | "blocked" | "error";
4
7
  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" | "needs_create";
5
8
  export type VectorBackendResponseCode = "ZILLIZ_CLUSTER_STOPPED" | "VECTOR_BACKEND_AUTH_FAILED" | "VECTOR_BACKEND_UNREACHABLE" | "VECTOR_BACKEND_TIMEOUT" | "VECTOR_BACKEND_CONNECTION_CLOSED";
@@ -25,5 +28,7 @@ export interface ManageIndexResponseEnvelope {
25
28
  confidence: "high" | "low";
26
29
  probeFailed?: boolean;
27
30
  };
31
+ /** Observed symbol quality from registry (F9); not parser-cause diagnosis. */
32
+ symbolQuality?: SymbolQualitySummary;
28
33
  }
29
34
  //# sourceMappingURL=manage-types.d.ts.map
@@ -1,2 +1,10 @@
1
- export {};
1
+ /** Public manage_index action set (SSOT for schema, docs, and contract tests). */
2
+ export const MANAGE_INDEX_ACTIONS = [
3
+ "create",
4
+ "reindex",
5
+ "sync",
6
+ "status",
7
+ "clear",
8
+ "repair",
9
+ ];
2
10
  //# sourceMappingURL=manage-types.js.map
@@ -1,4 +1,9 @@
1
1
  import { type NavigationStore, type SymbolRecord } from "@zokizuan/satori-core";
2
+ /**
3
+ * Non-blocking outline warning: count + optional sample keys + agent action.
4
+ * Does not dump every registry diagnostic string into the outline payload.
5
+ */
6
+ export declare function formatOutlineSymbolRegistryWarnings(registryWarnings: readonly string[]): string | undefined;
2
7
  import { type PythonSourceBackedSpanRepair } from "./python-call-fallback.js";
3
8
  import type { CompletionProbeDebugHint, TrackedRootReadiness } from "./tracked-root-readiness.js";
4
9
  import type { CallGraphDirection, CallGraphEdge, CallGraphNode, CallGraphNote, CallGraphSymbolRef, CallGraphTestReference } from "./call-graph.js";
@@ -126,6 +131,7 @@ type NavigationHandlersHost = {
126
131
  nodeCount: number;
127
132
  edgeCount: number;
128
133
  };
134
+ hints?: Record<string, unknown>;
129
135
  } | null>;
130
136
  };
131
137
  export declare class NavigationHandlers {
@@ -1,9 +1,29 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
- import { getSupportedExtensionsForCapability, } from "@zokizuan/satori-core";
3
+ import { compareContractStrings, getSupportedExtensionsForCapability, } from "@zokizuan/satori-core";
4
+ const OUTLINE_DUPLICATE_SYMBOL_KEY_RE = /^Duplicate symbolKey '([^']+)' has \d+ candidates$/;
5
+ /**
6
+ * Non-blocking outline warning: count + optional sample keys + agent action.
7
+ * Does not dump every registry diagnostic string into the outline payload.
8
+ */
9
+ export function formatOutlineSymbolRegistryWarnings(registryWarnings) {
10
+ if (registryWarnings.length === 0) {
11
+ return undefined;
12
+ }
13
+ const samples = [];
14
+ for (const warning of registryWarnings) {
15
+ const match = OUTLINE_DUPLICATE_SYMBOL_KEY_RE.exec(warning);
16
+ if (match) {
17
+ samples.push(match[1]);
18
+ }
19
+ }
20
+ samples.sort(compareContractStrings);
21
+ const sample = samples.slice(0, 3).join(",");
22
+ return `OUTLINE_SYMBOL_REGISTRY_WARNINGS:${registryWarnings.length} action=treat_outline_as_degraded_identity${sample ? ` sample=${sample}` : ""}`;
23
+ }
4
24
  import { repairSourceBackedPythonSpan, } from "./python-call-fallback.js";
5
25
  import { buildRegistryFileOutlinePayload, findExactRegistrySymbols, } from "./registry-file-outline.js";
6
- import { ensureAbsolutePath, trackCodebasePath } from "../utils.js";
26
+ import { requireAbsoluteFilesystemPath, requireRepoRelativeFilePath, trackCodebasePath } from "../utils.js";
7
27
  const OUTLINE_SUPPORTED_EXTENSIONS = getSupportedExtensionsForCapability("fileOutline");
8
28
  const PARTIAL_INDEX_NAVIGATION_UNAVAILABLE_DETAIL = "Partial index/search data may exist, but navigation sidecars were not published because indexing stopped before completion.";
9
29
  function collectErrorFragments(value, output, visited, depth = 0) {
@@ -81,8 +101,24 @@ export class NavigationHandlers {
81
101
  const symbolIdExact = typeof args?.symbolIdExact === "string" ? args.symbolIdExact.trim() : undefined;
82
102
  const symbolLabelExact = typeof args?.symbolLabelExact === "string" ? args.symbolLabelExact.trim() : undefined;
83
103
  try {
84
- const absoluteRoot = ensureAbsolutePath(args.path);
85
- const normalizedFile = this.host.normalizeRelativeFilePath(args.file);
104
+ const absoluteRootResult = requireAbsoluteFilesystemPath(args.path, "path");
105
+ if (!absoluteRootResult.ok) {
106
+ const payload = this.host.buildInvalidFileOutlineRequestPayload(absoluteRootResult.path, typeof args.file === "string" ? args.file : "", absoluteRootResult.message, "not_indexed", "not_indexed");
107
+ return {
108
+ content: [{ type: "text", text: this.host.stringifyToolJson(payload) }],
109
+ isError: true,
110
+ };
111
+ }
112
+ const absoluteRoot = absoluteRootResult.absolutePath;
113
+ const relativeFileResult = requireRepoRelativeFilePath(typeof args.file === "string" ? args.file : "", "file");
114
+ if (!relativeFileResult.ok) {
115
+ const payload = this.host.buildInvalidFileOutlineRequestPayload(absoluteRoot, relativeFileResult.path, relativeFileResult.message, "not_found");
116
+ return {
117
+ content: [{ type: "text", text: this.host.stringifyToolJson(payload) }],
118
+ isError: true,
119
+ };
120
+ }
121
+ const normalizedFile = this.host.normalizeRelativeFilePath(relativeFileResult.relativePath);
86
122
  if (!fs.existsSync(absoluteRoot)) {
87
123
  const payload = this.host.buildInvalidFileOutlineRequestPayload(absoluteRoot, normalizedFile, `Path '${absoluteRoot}' does not exist. file_outline requires an indexed codebase directory root.`, "not_indexed", "not_indexed");
88
124
  return {
@@ -227,9 +263,11 @@ export class NavigationHandlers {
227
263
  codebaseRoot: effectiveRoot,
228
264
  registryManifestHash: registryState.manifestHash,
229
265
  });
230
- const outlineWarnings = registryState.warnings.length > 0
231
- ? [`OUTLINE_SYMBOL_REGISTRY_WARNINGS:${registryState.warnings.length}`]
232
- : [];
266
+ const outlineWarnings = [];
267
+ const registryWarning = formatOutlineSymbolRegistryWarnings(registryState.warnings);
268
+ if (registryWarning) {
269
+ outlineWarnings.push(registryWarning);
270
+ }
233
271
  if (relationshipGraph.warning) {
234
272
  outlineWarnings.push(`OUTLINE_${relationshipGraph.warning}`);
235
273
  }
@@ -307,7 +345,11 @@ export class NavigationHandlers {
307
345
  };
308
346
  }
309
347
  catch (error) {
310
- const payload = this.host.buildInvalidFileOutlineRequestPayload(typeof args?.path === "string" ? ensureAbsolutePath(args.path) : "", typeof args?.file === "string" ? this.host.normalizeRelativeFilePath(args.file) : "", `Unexpected file_outline failure: ${formatUnknownError(error)}`, "not_ready");
348
+ const pathResult = typeof args?.path === "string"
349
+ ? requireAbsoluteFilesystemPath(args.path)
350
+ : null;
351
+ const pathForError = pathResult?.ok ? pathResult.absolutePath : (typeof args?.path === "string" ? args.path : "");
352
+ const payload = this.host.buildInvalidFileOutlineRequestPayload(pathForError, typeof args?.file === "string" ? this.host.normalizeRelativeFilePath(args.file) : "", `Unexpected file_outline failure: ${formatUnknownError(error)}`, "not_ready");
311
353
  return {
312
354
  content: [{ type: "text", text: this.host.stringifyToolJson(payload) }],
313
355
  isError: true,
@@ -322,19 +364,41 @@ export class NavigationHandlers {
322
364
  const depth = Number.isFinite(args?.depth) ? Math.max(1, Math.min(3, Number(args.depth))) : 1;
323
365
  const limit = Number.isFinite(args?.limit) ? Math.max(1, Number(args.limit)) : 20;
324
366
  const symbolRef = args?.symbolRef;
367
+ const symbolFileResult = typeof symbolRef?.file === "string"
368
+ ? requireRepoRelativeFilePath(symbolRef.file, "symbolRef.file")
369
+ : { ok: false, path: "", message: "symbolRef.file is required." };
325
370
  const normalizedSymbolRef = {
326
- file: typeof symbolRef?.file === "string" ? this.host.normalizeRelativeFilePath(symbolRef.file) : "",
371
+ file: symbolFileResult.ok
372
+ ? this.host.normalizeRelativeFilePath(symbolFileResult.relativePath)
373
+ : (typeof symbolRef?.file === "string" ? this.host.normalizeRelativeFilePath(symbolRef.file) : ""),
327
374
  symbolId: typeof symbolRef?.symbolId === "string" ? symbolRef.symbolId : "",
328
375
  ...(typeof symbolRef?.symbolLabel === "string" ? { symbolLabel: symbolRef.symbolLabel } : {}),
329
376
  ...(symbolRef?.span ? { span: symbolRef.span } : {}),
330
377
  };
378
+ const absolutePathResult = typeof args?.path === "string"
379
+ ? requireAbsoluteFilesystemPath(args.path, "path")
380
+ : { ok: false, path: "", message: "path is required." };
331
381
  const invalidSymbolRefContext = {
332
- path: typeof args?.path === "string" ? ensureAbsolutePath(args.path) : "",
382
+ path: absolutePathResult.ok ? absolutePathResult.absolutePath : (typeof args?.path === "string" ? args.path : ""),
333
383
  symbolRef: normalizedSymbolRef,
334
384
  direction,
335
385
  depth,
336
386
  limit,
337
387
  };
388
+ if (!absolutePathResult.ok) {
389
+ const payload = this.host.buildInvalidCallGraphRequestPayload(invalidSymbolRefContext, absolutePathResult.message, "not_indexed", "not_indexed");
390
+ return {
391
+ content: [{ type: "text", text: this.host.stringifyToolJson(payload) }],
392
+ isError: true,
393
+ };
394
+ }
395
+ if (!symbolFileResult.ok) {
396
+ const payload = this.host.buildInvalidCallGraphRequestPayload(invalidSymbolRefContext, symbolFileResult.message, "not_found", "invalid_symbol_ref");
397
+ return {
398
+ content: [{ type: "text", text: this.host.stringifyToolJson(payload) }],
399
+ isError: true,
400
+ };
401
+ }
338
402
  if (!symbolRef || typeof symbolRef.file !== "string" || typeof symbolRef.symbolId !== "string") {
339
403
  const payload = this.host.buildInvalidCallGraphRequestPayload(invalidSymbolRefContext, "symbolRef with { file, symbolId } is required.", "not_found", "invalid_symbol_ref");
340
404
  return {
@@ -343,7 +407,7 @@ export class NavigationHandlers {
343
407
  };
344
408
  }
345
409
  try {
346
- const absolutePath = ensureAbsolutePath(typeof args.path === "string" ? args.path : "");
410
+ const absolutePath = absolutePathResult.absolutePath;
347
411
  if (!fs.existsSync(absolutePath)) {
348
412
  const payload = this.host.buildInvalidCallGraphRequestPayload({
349
413
  path: absolutePath,
@@ -658,8 +722,11 @@ export class NavigationHandlers {
658
722
  };
659
723
  }
660
724
  catch (error) {
725
+ const pathResult = typeof args?.path === "string"
726
+ ? requireAbsoluteFilesystemPath(args.path)
727
+ : null;
661
728
  const payload = this.host.buildInvalidCallGraphRequestPayload({
662
- path: typeof args?.path === "string" ? ensureAbsolutePath(args.path) : "",
729
+ path: pathResult?.ok ? pathResult.absolutePath : (typeof args?.path === "string" ? args.path : ""),
663
730
  symbolRef: normalizedSymbolRef,
664
731
  direction,
665
732
  depth,
@@ -1,4 +1,4 @@
1
- import type { SymbolRecord } from "@zokizuan/satori-core";
1
+ import { type SymbolRecord } from "@zokizuan/satori-core";
2
2
  import type { CallGraphHint } from "./search-types.js";
3
3
  import type { FileOutlineResponseEnvelope } from "./search-types.js";
4
4
  import { type PythonSourceBackedSpanRepair } from "./python-call-fallback.js";
@@ -1,3 +1,4 @@
1
+ import { compareContractStrings } from "@zokizuan/satori-core";
1
2
  import { repairSourceBackedPythonSpans, } from "./python-call-fallback.js";
2
3
  function compareNullableNumbersAsc(a, b) {
3
4
  const left = a ?? Number.POSITIVE_INFINITY;
@@ -7,7 +8,7 @@ function compareNullableNumbersAsc(a, b) {
7
8
  function compareNullableStringsAsc(a, b) {
8
9
  const left = a ?? "\uffff";
9
10
  const right = b ?? "\uffff";
10
- return left.localeCompare(right);
11
+ return compareContractStrings(left, right);
11
12
  }
12
13
  function sortFileOutlineSymbols(symbols) {
13
14
  return [...symbols].sort((a, b) => {
@@ -106,7 +107,7 @@ export function buildRegistryFileOutlinePayload(input) {
106
107
  if (!visibleState.hasExtractedSymbols && symbols.length > 0) {
107
108
  warningSet.add("OUTLINE_SYNTHESIZED_FILE_SYMBOL");
108
109
  }
109
- return [...warningSet].sort((a, b) => a.localeCompare(b));
110
+ return [...warningSet].sort(compareContractStrings);
110
111
  };
111
112
  if (input.resolveMode === "exact") {
112
113
  const exactMatchIds = new Set(findExactRegistrySymbols({
@@ -2,6 +2,11 @@ import { type NavigationStore, type SymbolRecord, type SymbolRegistry } from "@z
2
2
  import type { SnapshotManager } from "./snapshot.js";
3
3
  import type { CallGraphDirection, CallGraphEdge, CallGraphNode, CallGraphNote, CallGraphSidecarManager, CallGraphTestReference } from "./call-graph.js";
4
4
  import { type PythonSourceBackedSpanRepair } from "./python-call-fallback.js";
5
+ /**
6
+ * Collapse per-key registry duplicate warnings into one count + sample line.
7
+ * Presentation-only: registry build still retains full diagnostics.
8
+ */
9
+ export declare function collapseRegistryDuplicateKeyWarnings(warnings: readonly string[]): string[];
5
10
  type RelationshipBackedCallGraphHost = {
6
11
  navigationStore: NavigationStore;
7
12
  callGraphManager: CallGraphSidecarManager;
@@ -37,7 +42,20 @@ type RelationshipBackedCallGraphResult = {
37
42
  nodeCount: number;
38
43
  edgeCount: number;
39
44
  };
45
+ hints?: Record<string, unknown>;
40
46
  };
47
+ /** True when a repo-relative path looks like a test/fixture site (not production call graph signal). */
48
+ export declare function isTestOrFixtureCallerFile(file: string): boolean;
49
+ /**
50
+ * Prefer a single unique suppressed inbound caller *site* file for recovery search.
51
+ * Prefer production files when both production and test sites exist.
52
+ * Never use the callee defining file when sites disagree or are multi-file.
53
+ */
54
+ export declare function uniqueInboundCallerSiteFile(notes: readonly CallGraphNote[]): string | undefined;
55
+ /**
56
+ * Production suppressed callers first; collapse excess test/fixture caller notes into one summary.
57
+ */
58
+ export declare function prioritizeInboundSuppressedNotes(notes: readonly CallGraphNote[]): CallGraphNote[];
41
59
  export declare class RelationshipBackedCallGraph {
42
60
  private readonly host;
43
61
  constructor(host: RelationshipBackedCallGraphHost);