Skip to main content

projectatlas/
mcp.rs

1//! Purpose: Serve `ProjectAtlas` repository intelligence over MCP.
2//! Native MCP adapter for `ProjectAtlas` agent integrations.
3
4mod task_registry;
5
6use task_registry::{
7    McpTaskOperation, McpTaskProgress, McpTaskRecord, McpTaskRegistry, McpTaskState,
8};
9
10use crate::atlas_map::{
11    AtlasMapConfig, IgnoreEntryKind, LintOptions, add_ignore_entry, effective_config_report,
12    init_gitignore, init_project_with_config, list_ignore_entries, load_atlas_config,
13    load_atlas_config_for_root, remove_ignore_entry, write_map,
14};
15use crate::runtime::{
16    DEFAULT_HEALTH_LIMIT, INDEX_WORKER_SAFE_CEILING, IndexInitRequired, IndexProjectMismatch,
17    IndexRefreshRequired, IndexVerificationIncomplete, InitBootstrapOptions, InitHydrationPhase,
18    InitHydrationStatus, InitPhaseStatus, InitScanPhase, InitSetupReport, MAX_HEALTH_LIMIT,
19    MAX_SYMBOL_FILE_BYTES, ProjectWorktreeRequired, PurposeCuratorHandoff, PurposeLintLevel,
20    PurposeReviewRequest, ResetIndexReport, ScanReport, ScanRuntimePlan,
21    SettingsClassifiedNavigationReport, SourceObservationRegistry, SymbolBuildOptions,
22    UsageRuntimeInstance, VerifiedReadOutcome, VerifiedReadStamp, build_settings_report,
23    byte_count_to_tokens, canonical_project_root, canonical_source_project_root,
24    classified_navigation_capabilities, classified_ranked_file_nodes_with_reasons,
25    config_root_mismatch_error, default_mcp_project_root,
26    estimated_source_tokens_for_indexed_files, estimated_source_tokens_for_paths,
27    federated_worktree_error, index_init_required, index_work_control, init_config_path,
28    init_next_steps, lint_project, load_synchronized_repository_token_report,
29    lossless_native_path_display, lossless_project_root_display, next_step_report_payload,
30    next_step_report_with_selection, normalized_folder_filter, open_atlas_store_for_project,
31    open_atlas_store_read_only_for_project, open_federated_atlas_stores_for_project,
32    preflight_existing_project_binding, purpose_curation_page, purpose_curator_handoff,
33    ranked_file_nodes_with_reasons, ranked_folder_nodes_with_reasons, read_indexed_file_content,
34    reconcile_hydrated_index_controlled, record_directory_walk_usage_estimate,
35    record_usage_estimate, record_usage_text, render_classified_ranked_file_rows,
36    render_classified_symbol_rows, render_health_page, render_purpose_curation_page,
37    render_purpose_review_report, require_current_worktree_usage_snapshot,
38    require_registered_worktree_lifecycle, reset_index_files, reset_index_files_with_revalidation,
39    review_purposes, run_init_bootstrap, run_scan_pipeline_controlled,
40    run_single_watch_refresh_controlled, run_symbol_build_pipeline_controlled,
41    strip_legacy_purpose, telemetry_disabled, validate_purpose_review_admission,
42    validated_indexed_file_key, watcher_status_report,
43};
44#[cfg(all(test, unix))]
45use crate::runtime::{IndexReadStatus, IndexRefreshReason, IndexRefreshScope};
46#[cfg(test)]
47use crate::runtime::{
48    PURPOSE_CURATOR_RECOMMENDED_REASONING, db_sidecar_path, mcp_config_path_for_db,
49    run_scan_pipeline, synchronize_registered_worktree_usage,
50};
51use crate::token_tui::{
52    TokenDashboardTheme, render_token_dashboard_plain_with_theme,
53    render_token_trend_dashboard_plain_with_theme,
54};
55use crate::{
56    AgentErrorKind, CliError, DEFAULT_FILE_SUMMARY_LIMIT, DatabaseFilesystemErrorPayload,
57    HarnessConfig, OutputFormat, RootTransition, RuntimeInfoReport, SchemaMigrationRequiredPayload,
58    SchemaVersionMismatchPayload, SearchRetrievalModeArg, build_harness_mcp_config_report,
59    build_parity_report, build_repository_control_report, build_root_report, build_runtime_info,
60    controlled_named_output, database_filesystem_error_payload, finalize_coverage_output,
61    render_code_slice, render_file_summary, render_parity_report, render_repository_control_report,
62    render_root_report, render_runtime_info, render_search_report, render_watch_status,
63    schema_migration_required_payload, schema_version_mismatch_payload,
64};
65use projectatlas_core::graph::{
66    Completeness, ConfidenceClass, CoverageRecord, DocumentTargetUnresolvedReason, EntitySelector,
67    ExternalSelector, GraphIdentityText, GraphLimitKind, GraphLimits, GraphRelationKind,
68    ProjectInstanceId, RelationOccurrence, RelationResolution, RepositoryFilePath,
69    ReusableTargetSelector, SourceSpan,
70};
71use projectatlas_core::health::Severity;
72use projectatlas_core::language::{ContentClassification, ContentSelection};
73use projectatlas_core::outline::build_outline;
74use projectatlas_core::symbols::ParserKind;
75use projectatlas_core::telemetry::{
76    TOKEN_BASELINE_DIRECTORY_WALK, TOKEN_BASELINE_SELECTED_CANDIDATES,
77    TOKEN_BUCKET_NAVIGATION_AVOIDANCE, TOKEN_CONFIDENCE_INFERRED, TOKEN_CONFIDENCE_POLICY_ESTIMATE,
78    TokenTrendWindow, UsageInstanceOwner, usage_from_estimates_with_context, usage_from_text,
79};
80use projectatlas_core::toon::{
81    encode_agent_payload, render_outline, render_overview, render_ranked_nodes,
82    render_symbol_relations, render_token_overview, render_token_trends,
83};
84use projectatlas_core::{
85    CanonicalProjectRoot, IndexGeneration, IndexWorkControl, IndexWorkFailure, IndexWorkStage,
86    MAX_GIT_WORKTREE_REGISTRATIONS, NavigationNextCall, NavigationNextCapability, Overview,
87    PurposeSource, PurposeStatus, RankedConnection, RankedConnectionCount, RankedConnectionKind,
88    RankedConnectionTarget, RankedNode, RankedReasonCode, normalize_native_path_display,
89    normalize_native_path_display_str, normalize_repo_path, normalize_repo_path_prefix,
90    validated_repo_file_key, validated_repo_node_key,
91};
92use projectatlas_db::{
93    ActiveWorktreeRegistrationGuard, AtlasStore, DbError, HealthQuery, HealthResolution,
94    HealthScope, PreparedWorktreeHydrationCandidate, RepositoryCoverageQuery, WorktreeAlias,
95    WorktreeHydrationActivation, WorktreeRegistration, WorktreeRegistrationState,
96    WorktreeUsageSnapshot, WorktreeUsageSyncState, read_legacy_project_root_candidate_read_only,
97    read_project_root_identity_read_only, verify_project_database,
98};
99use projectatlas_fs::worktree::{
100    GitRepositoryStructure, GitWorktreeEntry, GitWorktreeRole, GitWorktreeState,
101    RepositoryStructure, discover_repository_structure, git_administrative_identity,
102};
103#[cfg(test)]
104use projectatlas_service::build_file_summary_from_source;
105use projectatlas_service::{
106    COVERAGE_PAGE_MAX_LIMIT, CodeSliceBudget, CoverageDigest, CoverageTrustState,
107    DetailedRelationBudget, DetailedRelationNode, DetailedRelationQuery, DetailedRelationReport,
108    DetailedRelationRow, DetailedRelationWork, EntrypointProfile, FederatedDetailedRelationReport,
109    FederatedParticipant, FederatedRelationWork, FederatedRendezvous, FederatedStore,
110    FileCallSummary, FileSummaryReport, FileSymbolSummary, GitImpactSelection,
111    RelationAnalysisMode, RelationAnalysisQuery, RelationAnchor, RelationDirection,
112    RelationNextCall, RelationPurpose, RelationTotalState, SearchQuery, ServiceError,
113    SymbolSliceSelector, TokenReport, TokenReportRequest,
114    build_file_summary_from_source_with_selection, load_coverage_discovery_controlled,
115    load_detailed_relation_page, load_federated_detailed_relations,
116    load_federated_relation_analysis, load_relation_analysis, load_token_report,
117    parse_coverage_parser, parse_coverage_relation, parse_coverage_state,
118    parse_relation_confidence, parse_relation_direction, parse_relation_resolution,
119    parse_symbol_kind, read_indexed_code_slice_from_source_bounded_with_selection,
120    read_symbol_slice_from_source_bounded_with_selection, search_indexed_files_with_control,
121    validate_federated_root_count,
122};
123use rmcp::handler::server::{
124    router::tool::ToolRouter, tool::IntoCallToolResult, wrapper::Parameters,
125};
126use rmcp::model::{CallToolResponse, Implementation, ServerCapabilities, ServerInfo};
127use rmcp::schemars;
128use rmcp::service::RequestContext;
129use rmcp::{RoleServer, ServerHandler, ServiceExt, tool, tool_handler, tool_router};
130use serde::{Deserialize, Serialize};
131use std::collections::{HashSet, VecDeque};
132use std::fs;
133use std::path::{Component, Path, PathBuf};
134use std::sync::{
135    Arc, Mutex, RwLock,
136    atomic::{AtomicU64, Ordering},
137};
138use std::thread;
139use std::time::{Duration, SystemTime, UNIX_EPOCH};
140
141/// Text response routed through `rmcp`'s native tool-success/tool-error conversion.
142#[derive(Debug, PartialEq, Eq)]
143struct McpToolTextResult(Result<String, String>);
144
145impl std::ops::Deref for McpToolTextResult {
146    type Target = str;
147
148    fn deref(&self) -> &Self::Target {
149        match &self.0 {
150            Ok(text) | Err(text) => text,
151        }
152    }
153}
154
155impl std::fmt::Display for McpToolTextResult {
156    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157        formatter.write_str(self)
158    }
159}
160
161impl IntoCallToolResult for McpToolTextResult {
162    fn into_call_tool_result(self) -> Result<CallToolResponse, rmcp::ErrorData> {
163        self.0.into_call_tool_result()
164    }
165}
166
167/// MCP tools required for the agent-first repository-intelligence surface.
168pub(crate) const REQUIRED_MCP_TOOL_NAMES: &[&str] = &[
169    MCP_TOOL_ATLAS_SET_PROJECT_PATH,
170    MCP_TOOL_ATLAS_WORKTREE_LIST,
171    MCP_TOOL_ATLAS_WORKTREE_ADD,
172    MCP_TOOL_ATLAS_WORKTREE_REMOVE,
173    MCP_TOOL_ATLAS_INIT,
174    MCP_TOOL_ATLAS_MAP,
175    MCP_TOOL_ATLAS_ROOT,
176    MCP_TOOL_ATLAS_ROOT_SET,
177    MCP_TOOL_ATLAS_CONFIG,
178    MCP_TOOL_ATLAS_IGNORE_LIST,
179    MCP_TOOL_ATLAS_IGNORE_INIT_GITIGNORE,
180    MCP_TOOL_ATLAS_IGNORE_ADD,
181    MCP_TOOL_ATLAS_IGNORE_REMOVE,
182    MCP_TOOL_ATLAS_SCAN,
183    MCP_TOOL_ATLAS_OVERVIEW,
184    MCP_TOOL_ATLAS_FOLDERS,
185    MCP_TOOL_ATLAS_FILES,
186    MCP_TOOL_ATLAS_NEXT,
187    MCP_TOOL_ATLAS_OUTLINE,
188    MCP_TOOL_ATLAS_FILE_SUMMARY,
189    MCP_TOOL_ATLAS_SEARCH,
190    MCP_TOOL_ATLAS_SLICE,
191    MCP_TOOL_ATLAS_SYMBOLS_BUILD,
192    MCP_TOOL_ATLAS_SYMBOLS,
193    MCP_TOOL_ATLAS_SYMBOL_RELATIONS,
194    MCP_TOOL_ATLAS_HEALTH,
195    MCP_TOOL_ATLAS_HEALTH_RESOLVE,
196    MCP_TOOL_ATLAS_LINT,
197    MCP_TOOL_ATLAS_TOKEN_REPORT,
198    MCP_TOOL_ATLAS_PARITY_REPORT,
199    MCP_TOOL_ATLAS_SETTINGS,
200    MCP_TOOL_ATLAS_WATCH_STATUS,
201    MCP_TOOL_ATLAS_WATCH_ONCE,
202    MCP_TOOL_ATLAS_STRIP_LEGACY_PURPOSE,
203    MCP_TOOL_ATLAS_RESET_INDEX,
204    MCP_TOOL_ATLAS_MCP_CONFIG,
205    MCP_TOOL_ATLAS_RUNTIME_INFO,
206    MCP_TOOL_ATLAS_SESSION_BRIEF,
207    MCP_TOOL_ATLAS_TASK_STATUS,
208    MCP_TOOL_ATLAS_TASK_CANCEL,
209    MCP_TOOL_ATLAS_PURPOSE_QUEUE,
210    MCP_TOOL_ATLAS_PURPOSE_SET,
211    MCP_TOOL_ATLAS_PURPOSE_REVIEW,
212];
213
214/// MCP tool name for active project selection.
215const MCP_TOOL_ATLAS_SET_PROJECT_PATH: &str = "atlas_set_project_path";
216/// MCP tool name for bounded structural worktree inventory.
217const MCP_TOOL_ATLAS_WORKTREE_LIST: &str = "atlas_worktree_list";
218/// MCP tool name for control-atlas worktree registration.
219const MCP_TOOL_ATLAS_WORKTREE_ADD: &str = "atlas_worktree_add";
220/// MCP tool name for control-atlas worktree retirement.
221const MCP_TOOL_ATLAS_WORKTREE_REMOVE: &str = "atlas_worktree_remove";
222/// MCP tool name for project initialization.
223const MCP_TOOL_ATLAS_INIT: &str = "atlas_init";
224/// MCP tool name for compatibility map exports.
225const MCP_TOOL_ATLAS_MAP: &str = "atlas_map";
226/// MCP tool name for root diagnostics.
227const MCP_TOOL_ATLAS_ROOT: &str = "atlas_root";
228/// MCP tool name for binding a project root.
229const MCP_TOOL_ATLAS_ROOT_SET: &str = "atlas_root_set";
230/// MCP tool name for effective config reports.
231const MCP_TOOL_ATLAS_CONFIG: &str = "atlas_config";
232/// MCP tool name for ignore policy reports.
233const MCP_TOOL_ATLAS_IGNORE_LIST: &str = "atlas_ignore_list";
234/// MCP tool name for project `.gitignore` initialization.
235const MCP_TOOL_ATLAS_IGNORE_INIT_GITIGNORE: &str = "atlas_ignore_init_gitignore";
236/// MCP tool name for adding manual ignore entries.
237const MCP_TOOL_ATLAS_IGNORE_ADD: &str = "atlas_ignore_add";
238/// MCP tool name for removing manual ignore entries.
239const MCP_TOOL_ATLAS_IGNORE_REMOVE: &str = "atlas_ignore_remove";
240/// MCP tool name for repository scans.
241const MCP_TOOL_ATLAS_SCAN: &str = "atlas_scan";
242/// MCP tool name for repository overviews.
243const MCP_TOOL_ATLAS_OVERVIEW: &str = "atlas_overview";
244/// MCP tool name for folder ranking.
245const MCP_TOOL_ATLAS_FOLDERS: &str = "atlas_folders";
246/// MCP tool name for file ranking.
247const MCP_TOOL_ATLAS_FILES: &str = "atlas_files";
248/// MCP tool name for next-step recommendations.
249const MCP_TOOL_ATLAS_NEXT: &str = "atlas_next";
250/// MCP tool name for file outlines.
251const MCP_TOOL_ATLAS_OUTLINE: &str = "atlas_outline";
252/// MCP tool name for file summaries.
253const MCP_TOOL_ATLAS_FILE_SUMMARY: &str = "atlas_file_summary";
254/// MCP tool name for indexed search.
255const MCP_TOOL_ATLAS_SEARCH: &str = "atlas_search";
256/// MCP tool name for source slices.
257const MCP_TOOL_ATLAS_SLICE: &str = "atlas_slice";
258/// MCP tool name for symbol builds.
259const MCP_TOOL_ATLAS_SYMBOLS_BUILD: &str = "atlas_symbols_build";
260/// MCP tool name for symbol lookup.
261const MCP_TOOL_ATLAS_SYMBOLS: &str = "atlas_symbols";
262/// MCP tool name for symbol relation lookup.
263const MCP_TOOL_ATLAS_SYMBOL_RELATIONS: &str = "atlas_symbol_relations";
264/// MCP tool name for health pages.
265const MCP_TOOL_ATLAS_HEALTH: &str = "atlas_health";
266/// MCP tool name for health resolutions.
267const MCP_TOOL_ATLAS_HEALTH_RESOLVE: &str = "atlas_health_resolve";
268/// MCP tool name for lint reports.
269const MCP_TOOL_ATLAS_LINT: &str = "atlas_lint";
270/// MCP tool name for token reports.
271const MCP_TOOL_ATLAS_TOKEN_REPORT: &str = "atlas_token_report";
272/// MCP tool name for parity reports.
273const MCP_TOOL_ATLAS_PARITY_REPORT: &str = "atlas_parity_report";
274/// MCP tool name for settings reports.
275const MCP_TOOL_ATLAS_SETTINGS: &str = "atlas_settings";
276/// MCP tool name for watcher status.
277const MCP_TOOL_ATLAS_WATCH_STATUS: &str = "atlas_watch_status";
278/// MCP tool name for one-shot watcher refreshes.
279const MCP_TOOL_ATLAS_WATCH_ONCE: &str = "atlas_watch_once";
280/// MCP tool name for legacy purpose cleanup.
281const MCP_TOOL_ATLAS_STRIP_LEGACY_PURPOSE: &str = "atlas_strip_legacy_purpose";
282/// MCP tool name for runtime index reset.
283const MCP_TOOL_ATLAS_RESET_INDEX: &str = "atlas_reset_index";
284/// MCP tool name for generated MCP config documents.
285const MCP_TOOL_ATLAS_MCP_CONFIG: &str = "atlas_mcp_config";
286/// MCP tool name for runtime identity reports.
287const MCP_TOOL_ATLAS_RUNTIME_INFO: &str = "atlas_runtime_info";
288/// MCP tool name for compact agent startup briefs.
289const MCP_TOOL_ATLAS_SESSION_BRIEF: &str = "atlas_session_brief";
290/// MCP tool name for task-progress status lookup.
291const MCP_TOOL_ATLAS_TASK_STATUS: &str = "atlas_task_status";
292/// MCP tool name for task-progress cancellation.
293const MCP_TOOL_ATLAS_TASK_CANCEL: &str = "atlas_task_cancel";
294/// MCP tool name for purpose queue lookup.
295const MCP_TOOL_ATLAS_PURPOSE_QUEUE: &str = "atlas_purpose_queue";
296/// MCP tool name for purpose updates.
297const MCP_TOOL_ATLAS_PURPOSE_SET: &str = "atlas_purpose_set";
298/// MCP tool name for purpose reviews.
299const MCP_TOOL_ATLAS_PURPOSE_REVIEW: &str = "atlas_purpose_review";
300/// `ProjectAtlas` local state directory name.
301const PROJECTATLAS_DIR_NAME: &str = ".projectatlas";
302/// `ProjectAtlas` `SQLite` database filename.
303const PROJECTATLAS_DB_FILE_NAME: &str = "projectatlas.db";
304/// Project-local nested config filename.
305const PROJECTATLAS_CONFIG_FILE_NAME: &str = "config.toml";
306/// Project-local flat config filename.
307const PROJECTATLAS_FLAT_CONFIG_FILE_NAME: &str = "projectatlas.toml";
308/// Maximum distinct project bindings retained before inactive telemetry rotation.
309const MCP_TELEMETRY_PROJECT_BINDING_LIMIT: usize = 64;
310/// Hard ceiling for the default agent-facing settings diagnostic.
311const MCP_SETTINGS_RESPONSE_MAX_BYTES: usize = 64_000;
312/// Prefix for an oversized settings-response diagnostic.
313const MCP_SETTINGS_RESPONSE_LIMIT_PREFIX: &str = "settings response requires ";
314/// Separator for the observed and permitted settings-response byte counts.
315const MCP_SETTINGS_RESPONSE_LIMIT_SEPARATOR: &str = " bytes, exceeding the ";
316/// Suffix for an oversized settings-response diagnostic.
317const MCP_SETTINGS_RESPONSE_LIMIT_SUFFIX: &str = "-byte diagnostic limit";
318/// Maximum generation/filter token baselines retained by one MCP process.
319const MCP_SOURCE_TOKEN_BASELINE_LIMIT: usize = 128;
320/// Default MCP config server key.
321const MCP_DEFAULT_CONFIG_SERVER_NAME: &str = "projectatlas";
322/// Recovery guidance when a path names a subfolder rather than another selected root.
323const SELECTED_ROOT_ASSERTION_GUIDANCE: &str = "pass project_path or call atlas_set_project_path for another repository, or use normal filesystem tools such as Get-Content or rg for files inside the selected project";
324/// Recovery guidance when a path escapes the selected `ProjectAtlas` root.
325const OUTSIDE_SELECTED_PROJECT_GUIDANCE: &str = "pass project_path or call atlas_set_project_path for that repository, or use normal filesystem tools such as Get-Content or rg for files outside the selected ProjectAtlas project";
326/// Current-directory root alias.
327const CURRENT_DIR_ALIAS: &str = ".";
328/// `ProjectAtlas` MCP server display name.
329const MCP_SERVER_NAME: &str = "ProjectAtlas";
330/// Request-local MCP cancellation monitor thread name.
331const MCP_CANCELLATION_MONITOR_THREAD_NAME: &str = "projectatlas-mcp-cancel";
332/// Prefix for cancellation-monitor startup failures.
333const MCP_CANCELLATION_MONITOR_START_ERROR_PREFIX: &str =
334    "MCP request cancellation monitor could not start: ";
335/// Prefix for invalid registered worktree roots.
336const MCP_ERROR_REGISTERED_WORKTREE_ROOT_INVALID_PREFIX: &str =
337    "registered worktree root is invalid: ";
338/// MCP error lock-poison message.
339const MCP_PROJECT_STATE_LOCK_POISONED: &str = "MCP project state lock poisoned";
340/// Prefix used only if structured MCP error serialization fails.
341const MCP_ERROR_SERIALIZATION_FALLBACK_PREFIX: &str = "error: ";
342/// MCP payload key for scan reports.
343const MCP_PAYLOAD_SCAN: &str = "scan";
344/// MCP payload key for project initialization reports.
345const MCP_PAYLOAD_INIT: &str = "init";
346/// MCP payload key for structural worktree inventory.
347const MCP_PAYLOAD_WORKTREES: &str = "worktrees";
348/// MCP payload key for one worktree registration transition.
349const MCP_PAYLOAD_WORKTREE: &str = "worktree";
350/// MCP payload key for compatibility map reports.
351const MCP_PAYLOAD_MAP: &str = "map";
352/// MCP payload key for effective config reports.
353const MCP_PAYLOAD_CONFIG: &str = "config";
354/// MCP payload key for ignore policy reports.
355const MCP_PAYLOAD_IGNORE: &str = "ignore";
356/// MCP payload key for `.gitignore` initialization reports.
357const MCP_PAYLOAD_GITIGNORE: &str = "gitignore";
358/// MCP payload key for lint reports.
359const MCP_PAYLOAD_LINT: &str = "lint";
360/// MCP payload key for symbol-build reports.
361const MCP_PAYLOAD_SYMBOLS_BUILD: &str = "symbols_build";
362/// MCP payload key for detailed symbol-relation reports.
363const MCP_PAYLOAD_SYMBOL_RELATIONS: &str = "symbol_relations";
364/// MCP payload key for health-resolution reports.
365const MCP_PAYLOAD_HEALTH_RESOLUTION: &str = "health_resolution";
366/// MCP payload key for token trend reports.
367const MCP_PAYLOAD_TOKEN_TRENDS: &str = "token_trends";
368/// MCP payload key for token savings reports.
369const MCP_PAYLOAD_TOKEN_SAVINGS: &str = "token_savings";
370/// MCP payload key for optional chart strings.
371const MCP_PAYLOAD_CHART: &str = "chart";
372/// MCP payload key for watcher reports.
373const MCP_PAYLOAD_WATCH: &str = "watch";
374/// MCP payload key for legacy-purpose migration reports.
375const MCP_PAYLOAD_LEGACY_PURPOSE_MIGRATION: &str = "legacy_purpose_migration";
376/// MCP payload key for reset-index reports.
377const MCP_PAYLOAD_RESET_INDEX: &str = "reset_index";
378/// MCP payload key for generated MCP config reports.
379const MCP_PAYLOAD_MCP_CONFIG: &str = "mcp_config";
380/// MCP payload key for next-step recommendation reports.
381const MCP_PAYLOAD_NEXT: &str = "next";
382/// MCP payload key for selected-project audit metadata on routed reads.
383const MCP_PAYLOAD_SELECTED_PROJECT: &str = "selected_project";
384/// MCP payload key for settings reports.
385const MCP_PAYLOAD_SETTINGS: &str = "settings";
386/// MCP payload key for agent startup briefs.
387const MCP_PAYLOAD_SESSION_BRIEF: &str = "session_brief";
388/// Default source status omitted from compact file-summary payloads.
389const MCP_FILE_SOURCE_STATUS_LIVE: &str = "live-source";
390/// MCP payload key for accepted background tasks.
391const MCP_PAYLOAD_TASK_START: &str = "task_start";
392/// MCP payload key for task status lookups.
393const MCP_PAYLOAD_TASK_STATUS: &str = "task_status";
394/// MCP payload key for task cancellation responses.
395const MCP_PAYLOAD_TASK_CANCEL: &str = "task_cancel";
396/// MCP session capability payload key.
397const MCP_PAYLOAD_SESSION_CAPABILITIES: &str = "mcp_session";
398/// Session-brief argument key for per-call project roots.
399const MCP_BRIEF_ARG_PROJECT_PATH: &str = "project_path";
400/// Session-brief argument key for registered worktree aliases.
401const MCP_BRIEF_ARG_WORKTREE: &str = "worktree";
402/// Session-brief argument key for exact repository-relative files.
403const MCP_BRIEF_ARG_FILE: &str = "file";
404/// Session-brief argument key for indexed search patterns.
405const MCP_BRIEF_ARG_PATTERN: &str = "pattern";
406/// Session-brief argument key for relation view selection.
407const MCP_BRIEF_ARG_VIEW: &str = "view";
408/// Session-brief argument key for row limits.
409const MCP_BRIEF_ARG_LIMIT: &str = "limit";
410/// Session-brief argument key for host-owned purpose-curation tasks.
411const MCP_BRIEF_ARG_TASK: &str = "task";
412/// Compact-response argument key used by typed startup recommendations.
413const MCP_BRIEF_ARG_COMPACT: &str = "compact";
414/// Session-brief recommendation target for normal filesystem reads.
415const MCP_BRIEF_TARGET_FILESYSTEM_TOOLS: &str = "filesystem_tools";
416/// Session-brief reason for missing selected indexes.
417const MCP_BRIEF_REASON_SELECTED_INDEX_MISSING: &str = "selected_index_missing";
418/// Session-brief reason for filesystem fallback before an index exists.
419const MCP_BRIEF_REASON_FILESYSTEM_UNTIL_INDEX: &str =
420    "use_filesystem_until_projectatlas_index_exists";
421/// Session-brief reason for following the selected file into its summary.
422const MCP_BRIEF_REASON_RANKED_FILE_SUMMARY: &str = "ranked_file_ready_for_summary";
423/// Session-brief reason for following truncated graph evidence into detailed relations.
424const MCP_BRIEF_REASON_RANKED_FILE_RELATIONS: &str = "ranked_file_ready_for_relations";
425/// Session-brief reason for searching when ranking found no directly navigable file.
426const MCP_BRIEF_REASON_SEARCH_FALLBACK: &str = "no_ranked_file_candidate_search_index";
427/// Session-brief reason for normal filesystem orientation in an indexed empty project.
428const MCP_BRIEF_REASON_NO_FILE_CANDIDATE: &str = "no_ranked_file_candidate";
429/// Session-brief reason for health follow-up.
430const MCP_BRIEF_REASON_HEALTH_BLOCKERS: &str = "unresolved_health_blockers_present";
431/// Session-brief reason for following an actionable purpose-curator handoff.
432const MCP_BRIEF_REASON_PURPOSE_QUEUE: &str = "purpose_queue_ready";
433/// Built-in task-progress contract message.
434const MCP_TASK_PROGRESS_CONTRACT_MESSAGE: &str = "task progress contract available";
435/// MCP telemetry event for overview calls.
436const MCP_EVENT_ATLAS_OVERVIEW: &str = "mcp.atlas_overview";
437/// MCP telemetry event for folder calls.
438const MCP_EVENT_ATLAS_FOLDERS: &str = "mcp.atlas_folders";
439/// MCP telemetry event for file calls.
440const MCP_EVENT_ATLAS_FILES: &str = "mcp.atlas_files";
441/// MCP telemetry event for next-step recommendation calls.
442const MCP_EVENT_ATLAS_NEXT: &str = "mcp.atlas_next";
443/// MCP telemetry event for outline calls.
444const MCP_EVENT_ATLAS_OUTLINE: &str = "mcp.atlas_outline";
445/// MCP telemetry event for file-summary calls.
446const MCP_EVENT_ATLAS_FILE_SUMMARY: &str = "mcp.atlas_file_summary";
447/// MCP telemetry event for search calls.
448const MCP_EVENT_ATLAS_SEARCH: &str = "mcp.atlas_search";
449/// MCP telemetry event for slice calls.
450const MCP_EVENT_ATLAS_SLICE: &str = "mcp.atlas_slice";
451/// MCP telemetry event for symbol calls.
452const MCP_EVENT_ATLAS_SYMBOLS: &str = "mcp.atlas_symbols";
453/// MCP telemetry event for symbol-relation calls.
454const MCP_EVENT_ATLAS_SYMBOL_RELATIONS: &str = "mcp.atlas_symbol_relations";
455/// Compatible MCP symbol-relation view name.
456const MCP_SYMBOL_RELATION_VIEW_LEGACY: &str = "legacy";
457/// Additive detailed MCP symbol-relation view name.
458const MCP_SYMBOL_RELATION_VIEW_DETAILED: &str = "detailed";
459/// Additive closed analysis view on the existing relation route.
460const MCP_SYMBOL_RELATION_VIEW_ANALYSIS: &str = "analysis";
461/// Default closed relation-analysis mode.
462const MCP_RELATION_ANALYSIS_MODE_ARCHITECTURE: &str = "architecture";
463/// VCS impact relation-analysis mode.
464const MCP_RELATION_ANALYSIS_MODE_IMPACT: &str = "impact";
465/// Static trace relation-analysis mode.
466const MCP_RELATION_ANALYSIS_MODE_TRACE: &str = "trace";
467/// Explicit entrypoint-profile reachability mode.
468const MCP_RELATION_ANALYSIS_MODE_ENTRYPOINT: &str = "entrypoint";
469/// Default entrypoint profile name.
470const MCP_ENTRYPOINT_PROFILE_DEFAULT_NAME: &str = "entrypoint-profile";
471/// MCP validation error when entrypoint analysis receives federated stores.
472const MCP_ERROR_ENTRYPOINT_FEDERATED: &str = "entrypoint profiles require one project root";
473/// Prefix for invalid entrypoint anchor JSON.
474const MCP_ERROR_ENTRYPOINT_ANCHORS_PREFIX: &str =
475    "entrypoints must contain exact RelationAnchor JSON objects: ";
476/// MCP validation error for entrypoint controls on another analysis mode.
477const MCP_ERROR_ENTRYPOINT_CONTROLS_MODE: &str =
478    "entrypoint profile controls require analysis_mode=entrypoint";
479/// MCP validation error for contradictory entrypoint and symbol selectors.
480const MCP_ERROR_ENTRYPOINT_SYMBOL_SELECTOR: &str =
481    "entrypoint anchors cannot be combined with detailed symbol selectors";
482/// Default working-tree VCS impact selection.
483const MCP_RELATION_ANALYSIS_VCS_WORKING_TREE: &str = "working_tree";
484/// Staged-index VCS impact selection.
485const MCP_RELATION_ANALYSIS_VCS_INDEX: &str = "index";
486/// Explicit revision-range VCS impact selection.
487const MCP_RELATION_ANALYSIS_VCS_REVISION_RANGE: &str = "revision_range";
488/// Default direction for detailed MCP symbol-relation requests.
489const MCP_SYMBOL_RELATION_DIRECTION_DEFAULT: &str = "outbound";
490/// Default minimum confidence for detailed MCP symbol-relation requests.
491const MCP_SYMBOL_RELATION_CONFIDENCE_DEFAULT: &str = "low";
492/// Default resolution filter for detailed MCP symbol-relation requests.
493const MCP_SYMBOL_RELATION_RESOLUTION_DEFAULT: &str = "any";
494/// MCP validation error for an unsupported relation view.
495const MCP_ERROR_SYMBOL_RELATION_VIEW: &str = "unsupported symbol relation view";
496/// MCP validation error for legacy query fields in detailed requests.
497const MCP_ERROR_DETAILED_RELATION_QUERY: &str =
498    "detailed symbol relations use exact symbol selectors, not query";
499/// MCP validation error for a detailed request without a file anchor.
500const MCP_ERROR_DETAILED_RELATION_FILE: &str = "detailed symbol relations require file";
501/// MCP validation error for an empty detailed symbol anchor.
502const MCP_ERROR_DETAILED_RELATION_SYMBOL: &str = "detailed relation symbol must not be empty";
503/// MCP validation error for symbol disambiguators without a symbol.
504const MCP_ERROR_DETAILED_RELATION_DISAMBIGUATOR: &str = "symbol disambiguators require symbol";
505/// MCP validation error for a relation limit outside the service range.
506const MCP_ERROR_DETAILED_RELATION_LIMIT: &str = "detailed relation limit exceeds the u32 range";
507/// MCP validation error for compact projection on a non-detailed relation view.
508const MCP_ERROR_COMPACT_DETAILED_RELATION_VIEW: &str =
509    "compact symbol relations require view=detailed";
510/// MCP validation error for classified traversal on the legacy relation view.
511const MCP_ERROR_CONTENT_SELECTION_RELATION_VIEW: &str =
512    "content_selection requires view=detailed or view=analysis";
513/// MCP validation error for analysis controls on another relation view.
514const MCP_ERROR_ANALYSIS_VIEW_REQUIRED: &str = "analysis controls require view=analysis";
515/// MCP validation error for federation on the legacy relation view.
516const MCP_ERROR_FEDERATED_RELATION_VIEW: &str =
517    "roots or worktrees require the detailed or analysis relation view";
518/// MCP validation error for two federation selector families in one request.
519const MCP_ERROR_FEDERATED_SELECTOR_CONFLICT: &str =
520    "roots and worktrees are mutually exclusive federation selectors";
521/// MCP validation error for combining alias federation with a legacy root path.
522const MCP_ERROR_FEDERATED_PROJECT_PATH_CONFLICT: &str =
523    "worktrees federation cannot be combined with project_path";
524/// MCP validation error for a mismatched explicit primary alias.
525const MCP_ERROR_FEDERATED_PRIMARY_CONFLICT: &str =
526    "worktree must match the first ordered worktrees alias";
527/// MCP validation error for a symbol trace without a symbol-kind selector.
528const MCP_ERROR_TRACE_TARGET_KIND_REQUIRED: &str = "symbol trace targets require trace_target_kind";
529/// MCP validation error for a symbol trace without a signature selector.
530const MCP_ERROR_TRACE_TARGET_SIGNATURE_REQUIRED: &str =
531    "symbol trace targets require trace_target_signature";
532/// MCP validation error for symbol disambiguators without a trace symbol.
533const MCP_ERROR_TRACE_TARGET_REQUIRED: &str =
534    "trace target symbol disambiguators require trace_target";
535/// MCP validation error for a trace target without its owning file.
536const MCP_ERROR_TRACE_TARGET_FILE_REQUIRED: &str = "trace_target requires trace_target_file";
537/// MCP validation error for revision fields outside revision-range selection.
538const MCP_ERROR_VCS_REVISION_FIELDS: &str = "vcs_base and vcs_head require vcs=revision_range";
539/// MCP validation error for a revision-range selection without its base.
540const MCP_ERROR_VCS_BASE_REQUIRED: &str = "vcs=revision_range requires vcs_base";
541/// MCP validation error for a revision-range selection without its head.
542const MCP_ERROR_VCS_HEAD_REQUIRED: &str = "vcs=revision_range requires vcs_head";
543/// MCP validation error for an unsupported VCS impact selector.
544const MCP_ERROR_UNSUPPORTED_ANALYSIS_VCS: &str = "unsupported analysis VCS selection";
545/// MCP validation error for an unsupported closed relation-analysis mode.
546const MCP_ERROR_UNSUPPORTED_ANALYSIS_MODE: &str = "unsupported relation analysis mode";
547/// MCP telemetry event for health calls.
548const MCP_EVENT_ATLAS_HEALTH: &str = "mcp.atlas_health";
549/// MCP telemetry event for purpose-queue calls.
550const MCP_EVENT_ATLAS_PURPOSE_QUEUE: &str = "mcp.atlas_purpose_queue";
551/// MCP ignore kind token for directory names.
552const MCP_IGNORE_KIND_DIR_NAME: &str = "dir-name";
553/// MCP alternate ignore kind token for directory names.
554const MCP_IGNORE_KIND_DIR_NAME_ALIAS: &str = "dir_name";
555/// MCP ignore kind token for path prefixes.
556const MCP_IGNORE_KIND_PATH_PREFIX: &str = "path-prefix";
557/// MCP alternate ignore kind token for path prefixes.
558const MCP_IGNORE_KIND_PATH_PREFIX_ALIAS: &str = "path_prefix";
559/// MCP purpose lint level token for low strictness.
560const MCP_PURPOSE_LEVEL_LOW: &str = "low";
561/// MCP purpose lint level token for medium strictness.
562const MCP_PURPOSE_LEVEL_MEDIUM: &str = "medium";
563/// MCP purpose lint level token for strict mode.
564const MCP_PURPOSE_LEVEL_STRICT: &str = "strict";
565/// Default purpose-curation task used by session startup briefs.
566const MCP_PURPOSE_TASK_SESSION_STARTUP: &str = "session-startup";
567/// Default purpose-curation task used by direct queue requests.
568const MCP_PURPOSE_TASK_QUEUE: &str = "purpose-curation";
569/// MCP harness token for standard MCP JSON config.
570const MCP_HARNESS_MCP_JSON: &str = "mcp-json";
571/// MCP alternate harness token for standard MCP JSON config.
572const MCP_HARNESS_MCP_JSON_ALIAS: &str = "mcp_json";
573/// MCP harness token for Codex config.
574const MCP_HARNESS_CODEX: &str = "codex";
575/// MCP harness token for Claude Code config.
576const MCP_HARNESS_CLAUDE_CODE: &str = "claude-code";
577/// MCP alternate harness token for Claude Code config.
578const MCP_HARNESS_CLAUDE_CODE_ALIAS: &str = "claude_code";
579/// MCP harness token for `OpenCode` config.
580const MCP_HARNESS_OPENCODE: &str = "opencode";
581/// Required ignore kind diagnostic.
582const MCP_ERROR_IGNORE_KIND_REQUIRED: &str =
583    "ignore kind is required; expected dir-name or path-prefix";
584/// Required ignore kind diagnostic for mutation tools.
585const MCP_ERROR_IGNORE_KIND_REQUIRED_FOR_ADD: &str = "ignore kind is required for atlas_ignore_add";
586/// Error for mutually exclusive exact-root and structural-control selection.
587const MCP_ERROR_ROOT_CONTROL_CONFLICT: &str =
588    "control_root cannot be combined with project_path or verify";
589/// Invalid coverage start-index diagnostic prefix.
590const MCP_ERROR_COVERAGE_START_INDEX_TOO_LARGE_PREFIX: &str = "coverage start index is too large: ";
591/// Invalid coverage limit diagnostic prefix.
592const MCP_ERROR_COVERAGE_LIMIT_TOO_LARGE_PREFIX: &str = "coverage limit is too large: ";
593/// Coverage-filter diagnostic for the default structural-health mode.
594const MCP_ERROR_COVERAGE_FILTERS_REQUIRE_COVERAGE: &str = "coverage filters require coverage=true";
595/// CI environment variable used by MCP map export safeguards.
596const MCP_ENV_CI: &str = "CI";
597/// GitHub Actions environment variable used by MCP map export safeguards.
598const MCP_ENV_GITHUB_ACTIONS: &str = "GITHUB_ACTIONS";
599/// Compatibility map skip reason in CI.
600const MCP_MAP_SKIPPED_IN_CI_REASON: &str =
601    "skipped in CI; pass force=true to write the compatibility map";
602/// Placeholder when no routed root is available for a diagnostic.
603const MCP_NO_ROOT_PLACEHOLDER: &str = "none";
604/// Invalid ignore kind diagnostic prefix.
605const MCP_ERROR_INVALID_IGNORE_KIND_PREFIX: &str = "invalid ignore kind '";
606/// Invalid ignore kind diagnostic suffix.
607const MCP_ERROR_INVALID_IGNORE_KIND_SUFFIX: &str = "'; expected dir-name or path-prefix";
608/// Invalid purpose lint level diagnostic prefix.
609const MCP_ERROR_INVALID_PURPOSE_LEVEL_PREFIX: &str = "invalid purpose_level '";
610/// Invalid purpose lint level diagnostic suffix.
611const MCP_ERROR_INVALID_PURPOSE_LEVEL_SUFFIX: &str = "'; expected low, medium, or strict";
612/// Invalid harness diagnostic prefix.
613const MCP_ERROR_INVALID_HARNESS_PREFIX: &str = "invalid harness '";
614/// Invalid harness diagnostic suffix.
615const MCP_ERROR_INVALID_HARNESS_SUFFIX: &str =
616    "'; expected mcp-json, codex, claude-code, or opencode";
617/// Ambiguous route diagnostic fragment.
618const MCP_ERROR_FOR_PATH_FRAGMENT: &str = " for '";
619/// Ambiguous route diagnostic fragment.
620const MCP_ERROR_LEXICAL_ROOT_FRAGMENT: &str = "'; lexical root: '";
621/// Ambiguous route diagnostic fragment.
622const MCP_ERROR_RESOLVED_ROOT_FRAGMENT: &str = "'; resolved root: '";
623/// Ambiguous route diagnostic fragment.
624const MCP_ERROR_GUIDANCE_FRAGMENT: &str = "'; ";
625/// Node payload label for rendered folder rows.
626const NODE_LABEL_FOLDERS: &str = "folders";
627/// Node payload label for rendered file rows.
628const NODE_LABEL_FILES: &str = "files";
629/// Stable MCP payload key for classified symbol rows.
630const NODE_LABEL_SYMBOLS: &str = "symbols";
631/// Error when a symbol disambiguator is supplied without a symbol name.
632const SYMBOL_DISAMBIGUATOR_WITHOUT_SYMBOL_ERROR: &str = "symbol disambiguators require symbol";
633/// Error when a line slice omits its start line.
634const START_LINE_REQUIRED_ERROR: &str = "start_line is required unless symbol is provided";
635/// Error when an absolute MCP file path has no valid indexed project ancestor.
636const PATH_NOT_INSIDE_INDEXED_PROJECT_ERROR: &str =
637    "path is not inside an indexed ProjectAtlas project";
638/// Error when an absolute MCP folder path has no valid indexed project ancestor.
639const FOLDER_NOT_INSIDE_INDEXED_PROJECT_ERROR: &str =
640    "folder is not inside an indexed ProjectAtlas project";
641/// Error when lexical and canonical path routing disagree.
642const AMBIGUOUS_NEAREST_PROJECT_PATH_ERROR: &str = "absolute MCP path resolves through a symlink or junction with multiple plausible ProjectAtlas roots";
643/// Separator for diagnostic lists of accepted severity names.
644const SEVERITY_EXPECTED_SEPARATOR: &str = ", ";
645/// Final separator for diagnostic lists of accepted severity names.
646const SEVERITY_EXPECTED_FINAL_SEPARATOR: &str = ", or ";
647/// Token trend validation error suffix.
648const TOKEN_TREND_WINDOW_ERROR_SUFFIX: &str = "expected day, week, month, or year";
649/// Validation error for benchmark evidence on token trend requests.
650const TOKEN_TREND_BENCHMARK_ERROR: &str =
651    "benchmark_results is only supported for token overview reports";
652/// Internal mismatch when a trend request returns the overview variant.
653const TOKEN_TRENDS_RESULT_VARIANT_MISMATCH: &str = "token trend request returned an overview";
654/// Internal mismatch when an overview request returns the trends variant.
655const TOKEN_OVERVIEW_RESULT_VARIANT_MISMATCH: &str = "token overview request returned trends";
656/// Token chart theme validation error prefix.
657const TOKEN_CHART_THEME_ERROR_PREFIX: &str = "unsupported token chart theme ";
658/// Token chart theme validation error suffix.
659const TOKEN_CHART_THEME_ERROR_SUFFIX: &str = "; expected dark or light";
660/// Watch-status recommendation when no index exists.
661const WATCH_STATUS_SCAN_RECOMMENDATION: &str =
662    " Run `atlas_scan` first when no ProjectAtlas index exists for this project.";
663/// Default number of rows in an agent startup brief section.
664const SESSION_BRIEF_DEFAULT_LIMIT: usize = 5;
665/// Default number of rows in an explicitly compact startup brief section.
666const COMPACT_SESSION_BRIEF_DEFAULT_LIMIT: usize = 3;
667/// Maximum number of rows in an agent startup brief section.
668const SESSION_BRIEF_MAX_LIMIT: usize = 8;
669/// Bounded MCP task registry capacity.
670const MCP_TASK_REGISTRY_CAPACITY: usize = 32;
671/// Maximum concise task failure text retained in the session registry.
672const MCP_TASK_ERROR_MAX_CHARS: usize = 512;
673/// Built-in task id that exposes the task-progress contract itself.
674const MCP_TASK_CONTRACT_ID: &str = "task-progress-contract";
675/// Task-registry synchronization failure diagnostic.
676const MCP_TASK_REGISTRY_LOCK_POISONED: &str = "MCP task registry lock is poisoned";
677/// Prefix for generated background index task identifiers.
678const MCP_INDEX_TASK_ID_PREFIX: &str = "index-";
679/// Prefix for named background index worker threads.
680const MCP_INDEX_WORKER_NAME_PREFIX: &str = "projectatlas-";
681/// Prefix for active background task limit diagnostics.
682const MCP_INDEX_TASK_LIMIT_PREFIX: &str = "background indexing task limit ";
683/// Suffix for active background task limit diagnostics.
684const MCP_INDEX_TASK_LIMIT_SUFFIX: &str = " is already active";
685/// Maximum background indexing tasks admitted by one MCP server session.
686const MCP_BACKGROUND_TASK_SAFE_CEILING: usize = 4;
687/// Terminal diagnostic when a background worker panics.
688const MCP_INDEX_WORKER_PANIC_ERROR: &str = "background indexing worker panicked";
689/// Prefix for background worker spawn failures.
690const MCP_INDEX_WORKER_SPAWN_ERROR_PREFIX: &str = "failed to start background indexing: ";
691/// Progress message recorded after task admission.
692const MCP_TASK_PROGRESS_ACCEPTED: &str = "accepted";
693/// Progress message recorded while task work is active.
694const MCP_TASK_PROGRESS_RUNNING: &str = "running";
695/// Progress message recorded after successful task completion.
696const MCP_TASK_PROGRESS_COMPLETE: &str = "complete";
697/// Progress message recorded after task failure.
698const MCP_TASK_PROGRESS_FAILED: &str = "failed";
699/// Progress message recorded after cooperative cancellation completes.
700const MCP_TASK_PROGRESS_CANCELED: &str = "canceled";
701/// Progress message recorded after cancellation is requested.
702const MCP_TASK_PROGRESS_CANCELLATION_REQUESTED: &str = "cancellation_requested";
703/// Agent-facing MCP server instructions.
704const MCP_SERVER_INSTRUCTIONS: &str = "ProjectAtlas provides TOON-first repository orientation, folder/file ranking, structured file summaries, symbol graph lookup, exact slices, health checks, and token telemetry for coding agents.";
705/// Root-scoped selector conflict returned before filesystem or database access.
706const MCP_WORKTREE_PROJECT_PATH_CONFLICT: &str =
707    "worktree and project_path are mutually exclusive; choose one target selector";
708/// Reserved alias for the immutable MCP control authority.
709const MCP_MAIN_WORKTREE_ALIAS: &str = "main";
710/// Maximum combined structural and unmatched registered rows in one MCP response.
711const MCP_WORKTREE_LIST_MAX_ROWS: usize = (MAX_GIT_WORKTREE_REGISTRATIONS * 2) + 1;
712/// Stable prefix for structural worktree candidates returned to agents.
713const MCP_WORKTREE_SELECTOR_PREFIX: &str = "wt-";
714/// Hex characters retained from the administrative-path digest.
715const MCP_WORKTREE_SELECTOR_DIGEST_CHARS: usize = 16;
716/// Init-owned non-source metadata file copied or removed during hydration fallback.
717const MCP_NONSOURCE_FILE_NAME: &str = "projectatlas-nonsource-files.toon";
718/// Typed fallback when a caller explicitly suppresses the reconciliation scan.
719const MCP_HYDRATION_NO_SCAN_REASON: &str =
720    "hydration requires source reconciliation; ordinary no-scan init was requested";
721/// Worktree aliases require one structurally verified Git control repository.
722const MCP_ERROR_WORKTREE_CONTROL_REPOSITORY_REQUIRED: &str =
723    "worktree aliases require a structurally valid Git control repository";
724/// UTF-8-only display terminals cannot render some native worktree paths.
725const MCP_ERROR_WORKTREE_PATH_NON_UTF8: &str =
726    "native worktree identity is not available as UTF-8; use its stable alias or selector";
727/// Active registrations must agree with their local atlas identity.
728const MCP_ERROR_WORKTREE_IDENTITY_CONFLICT: &str =
729    "local atlas identity conflicts with its active registration";
730/// Deferred alias work must retain the exact control catalog that owns its registration ID.
731const MCP_ERROR_WORKTREE_CONTROL_IDENTITY_CONFLICT: &str =
732    "control atlas identity changed after worktree alias selection";
733/// Recovery guidance when an initialized registration loses its exact atlas.
734const MCP_ERROR_BOUND_WORKTREE_ATLAS_MISSING: &str = "registered worktree atlas is missing; restore it before retrying or retiring the alias so final token totals can be synchronized";
735/// Resetting a bound atlas would strand its control-catalog identity.
736const MCP_ERROR_BOUND_WORKTREE_RESET_UNSUPPORTED: &str = "bound worktree atlas cannot be reset in place; retire the alias to synchronize final totals, then register and initialize it again";
737/// A reused administrative path cannot inherit an earlier registration.
738const MCP_ERROR_WORKTREE_LIFECYCLE_CHANGED: &str =
739    "registered worktree administrative lifecycle changed; unregister and register it again";
740/// Federation must retain the exact alias captured during target resolution.
741const MCP_ERROR_FEDERATED_ALIAS_MISSING: &str =
742    "federated worktree resolution lost its captured alias";
743/// Federation cannot address the same alias or root twice.
744const MCP_ERROR_FEDERATED_TARGET_DUPLICATE: &str =
745    "federated worktree aliases and roots must be unique";
746/// The immutable control checkout is addressed only through its reserved alias.
747const MCP_ERROR_CONTROL_ALIAS_REQUIRED: &str =
748    "the control checkout is selected through reserved alias main";
749/// Worktree registration requires one non-empty structural selector.
750const MCP_ERROR_WORKTREE_SELECTOR_EMPTY: &str = "worktree selector must not be empty";
751/// Structural evidence can disappear between selection and registration.
752const MCP_ERROR_WORKTREE_NO_LONGER_ACTIVE: &str = "selected worktree is no longer active";
753/// Retiring a missing registration preserves its last accepted aggregate.
754const MCP_WORKTREE_MISSING_RETENTION_REASON: &str =
755    "worktree is structurally missing; the last accepted telemetry total is retained";
756
757/// Optional active-project override accepted by MCP tools.
758#[derive(Debug, Deserialize, schemars::JsonSchema)]
759struct AtlasProjectParams {
760    /// Optional project root for this call. Defaults to the active MCP project.
761    project_path: Option<String>,
762    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
763    worktree: Option<String>,
764}
765
766/// MCP parameter payload for compact agent startup briefs.
767#[derive(Debug, Deserialize, schemars::JsonSchema)]
768struct AtlasSessionBriefParams {
769    /// Optional project root for this call. Defaults to the active MCP project.
770    project_path: Option<String>,
771    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
772    worktree: Option<String>,
773    /// Optional task query used for folder and file ranking.
774    query: Option<String>,
775    /// Optional host-owned task label for the purpose-curator handoff.
776    purpose_task: Option<String>,
777    /// Return the additive compact startup projection when true.
778    compact: Option<bool>,
779    /// Maximum folder candidates to return.
780    folder_limit: Option<usize>,
781    /// Maximum file candidates to return.
782    file_limit: Option<usize>,
783    /// Maximum health blockers to return.
784    blocker_limit: Option<usize>,
785    /// Maximum actionable low-scope purpose rows in the startup handoff.
786    purpose_limit: Option<usize>,
787}
788
789/// MCP parameter payload for task-progress tools.
790#[derive(Debug, Deserialize, schemars::JsonSchema)]
791struct AtlasTaskParams {
792    /// Opaque MCP-session-local task id.
793    task_id: String,
794}
795
796/// MCP parameter payload for selecting the active project.
797#[derive(Debug, Deserialize, schemars::JsonSchema)]
798struct AtlasSetProjectPathParams {
799    /// Project root to make active for calls that omit `project_path`.
800    project_path: String,
801}
802
803/// MCP parameter payload for bounded structural worktree inventory.
804#[derive(Debug, Deserialize, schemars::JsonSchema)]
805struct AtlasWorktreeListParams {
806    /// Include retired `ProjectAtlas` registrations after active structural rows.
807    include_retired: Option<bool>,
808}
809
810/// MCP parameter payload for registering one structurally discovered worktree.
811#[derive(Debug, Deserialize, schemars::JsonSchema)]
812struct AtlasWorktreeAddParams {
813    /// Stable or uniquely matching short selector returned by `atlas_worktree_list`.
814    worktree: String,
815    /// Optional short alias; defaults to the selected worktree directory name when valid.
816    alias: Option<String>,
817}
818
819/// MCP parameter payload for retiring one active `ProjectAtlas` worktree alias.
820#[derive(Debug, Deserialize, schemars::JsonSchema)]
821struct AtlasWorktreeRemoveParams {
822    /// Active registered alias to remove from `ProjectAtlas` selection.
823    worktree: String,
824}
825
826/// MCP parameter payload for initializing a project.
827#[derive(Debug, Deserialize, schemars::JsonSchema)]
828struct AtlasInitParams {
829    /// Optional project root for this call. Defaults to the active MCP project.
830    project_path: Option<String>,
831    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
832    worktree: Option<String>,
833    /// Create/verify the project surface without running the scan/index pipeline.
834    no_scan: Option<bool>,
835    /// Run the scan/index phase even when a future freshness check could skip it.
836    force_rescan: Option<bool>,
837    /// Maximum UTF-8 file size persisted into `SQLite` text search during the init scan.
838    text_index_max_bytes: Option<u64>,
839}
840
841/// MCP parameter payload for compatibility map exports.
842#[derive(Debug, Deserialize, schemars::JsonSchema)]
843struct AtlasMapParams {
844    /// Optional project root for this call. Defaults to the active MCP project.
845    project_path: Option<String>,
846    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
847    worktree: Option<String>,
848    /// Write JSON compatibility content when true.
849    json: Option<bool>,
850    /// Force map generation even in CI-like environments.
851    force: Option<bool>,
852}
853
854/// MCP parameter payload for root diagnostics.
855#[derive(Debug, Deserialize, schemars::JsonSchema)]
856struct AtlasRootParams {
857    /// Optional project root for this call. Defaults to the active MCP project.
858    project_path: Option<String>,
859    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
860    worktree: Option<String>,
861    /// Optional checkout or Git common directory for mutation-free worktree status.
862    control_root: Option<String>,
863    /// Return the same report shape with `verified` available for gating.
864    verify: Option<bool>,
865}
866
867/// MCP parameter payload for binding a root.
868#[derive(Debug, Deserialize, schemars::JsonSchema)]
869struct AtlasRootSetParams {
870    /// Project root to bind and make active for later calls.
871    root: String,
872    /// Explicit durable transition. Omitted requests retain bind behavior.
873    transition: Option<RootTransition>,
874    /// Include mcp --nearest-project in generated project-local MCP configs.
875    nearest_project: Option<bool>,
876}
877
878/// MCP parameter payload for ignore mutations.
879#[derive(Debug, Deserialize, schemars::JsonSchema)]
880struct AtlasIgnoreMutationParams {
881    /// Optional project root for this call. Defaults to the active MCP project.
882    project_path: Option<String>,
883    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
884    worktree: Option<String>,
885    /// Ignore kind: dir-name or path-prefix. Omit only for broad remove.
886    kind: Option<String>,
887    /// Directory name or repository-relative path prefix.
888    value: String,
889}
890
891/// MCP parameter payload for lint reports.
892#[derive(Debug, Deserialize, schemars::JsonSchema)]
893struct AtlasLintParams {
894    /// Optional project root for this call. Defaults to the active MCP project.
895    project_path: Option<String>,
896    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
897    worktree: Option<String>,
898    /// Deprecated compatibility flag matching the CLI.
899    strict_folders: Option<bool>,
900    /// Purpose lint strictness: low, medium, or strict.
901    purpose_level: Option<String>,
902    /// Include untracked-file report.
903    report_untracked: Option<bool>,
904    /// Make untracked-file findings fail lint.
905    strict_untracked: Option<bool>,
906}
907
908/// MCP parameter payload for generated MCP config documents.
909#[derive(Debug, Deserialize, schemars::JsonSchema)]
910struct AtlasMcpConfigParams {
911    /// Optional project root for this call. Defaults to the active MCP project.
912    project_path: Option<String>,
913    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
914    worktree: Option<String>,
915    /// MCP server name to emit. Defaults to projectatlas.
916    server_name: Option<String>,
917    /// Harness config shape: mcp-json, codex, claude-code, or opencode.
918    harness: Option<String>,
919    /// Include mcp --nearest-project in generated startup args.
920    nearest_project: Option<bool>,
921}
922
923/// Run the official RMCP stdio server.
924pub(crate) fn run_mcp_server(
925    db_path: PathBuf,
926    config_path: Option<PathBuf>,
927    session: String,
928    allow_nearest_project: bool,
929) -> Result<(), CliError> {
930    let server = ProjectAtlasMcpServer::new(db_path, config_path, session, allow_nearest_project);
931    let shutdown_server = server.clone();
932    let runtime = tokio::runtime::Builder::new_multi_thread()
933        .enable_all()
934        .build()
935        .map_err(|source| CliError::Mcp(source.to_string()))?;
936    let result = runtime.block_on(async move {
937        server
938            .serve(rmcp::transport::stdio())
939            .await
940            .map_err(|source| CliError::Mcp(source.to_string()))?
941            .waiting()
942            .await
943            .map_err(|source| CliError::Mcp(source.to_string()))
944            .map(|_| ())
945    });
946    shutdown_server.seal_usage_instances_for_projects();
947    result
948}
949
950/// Return whether the generated RMCP router contains required tool families.
951pub(crate) fn required_mcp_surface_present() -> bool {
952    REQUIRED_MCP_TOOL_NAMES
953        .iter()
954        .all(|name| mcp_tool_route_present(name))
955}
956
957/// Return whether the generated RMCP router has a concrete tool route.
958pub(crate) fn mcp_tool_route_present(name: &str) -> bool {
959    ProjectAtlasMcpServer::tool_router().has_route(name)
960}
961
962/// MCP parameter payload for scanning and symbol refresh.
963#[derive(Debug, Deserialize, schemars::JsonSchema)]
964struct AtlasScanParams {
965    /// Optional project root for this call. Defaults to the active MCP project.
966    project_path: Option<String>,
967    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
968    worktree: Option<String>,
969    /// Repository root path. Defaults to the configured or indexed project root.
970    path: Option<String>,
971    /// Opt in to nearest indexed `ProjectAtlas` project discovery for absolute paths.
972    nearest_project: Option<bool>,
973    /// Maximum file size to parse for symbols.
974    max_bytes: Option<u64>,
975    /// Maximum parser worker threads.
976    max_workers: Option<usize>,
977    /// Stop starting parser work after this many seconds.
978    timeout_seconds: Option<u64>,
979    /// Maximum UTF-8 file size persisted into `SQLite` text search.
980    text_index_max_bytes: Option<u64>,
981    /// Run indexing in the bounded session task registry and return immediately.
982    background: Option<bool>,
983}
984
985/// MCP parameter payload for one-shot watcher refresh.
986#[derive(Debug, Deserialize, schemars::JsonSchema)]
987struct AtlasWatchOnceParams {
988    /// Optional project root for this call. Defaults to the active MCP project.
989    project_path: Option<String>,
990    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
991    worktree: Option<String>,
992    /// Repository root path. Defaults to the configured or indexed project root.
993    path: Option<String>,
994    /// Opt in to nearest indexed `ProjectAtlas` project discovery for absolute paths.
995    nearest_project: Option<bool>,
996    /// Maximum parser worker threads.
997    max_workers: Option<usize>,
998    /// Stop starting parser work after this many seconds.
999    timeout_seconds: Option<u64>,
1000    /// Maximum UTF-8 file size persisted into `SQLite` text search.
1001    text_index_max_bytes: Option<u64>,
1002    /// Run indexing in the bounded session task registry and return immediately.
1003    background: Option<bool>,
1004}
1005
1006/// MCP parameter payload for ranked node lookup.
1007#[derive(Debug, Deserialize, schemars::JsonSchema)]
1008struct AtlasQueryParams {
1009    /// Optional project root for this call. Defaults to the active MCP project.
1010    project_path: Option<String>,
1011    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
1012    worktree: Option<String>,
1013    /// Search query for path and purpose matching.
1014    query: Option<String>,
1015    /// Maximum number of rows to return.
1016    limit: Option<usize>,
1017}
1018
1019/// MCP parameter payload for classified next-step recommendations.
1020#[derive(Debug, Deserialize, schemars::JsonSchema)]
1021struct AtlasNextParams {
1022    /// Optional project root for this call. Defaults to the active MCP project.
1023    project_path: Option<String>,
1024    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
1025    worktree: Option<String>,
1026    /// Search query for path and purpose matching.
1027    query: Option<String>,
1028    /// Optional classified-content selection: source, documentation, or both.
1029    content_selection: Option<String>,
1030    /// Maximum number of rows to return.
1031    limit: Option<usize>,
1032}
1033
1034/// MCP parameter payload for ranked file lookup with optional absolute folder routing.
1035#[derive(Debug, Deserialize, schemars::JsonSchema)]
1036struct AtlasFilesParams {
1037    /// Optional project root for this call. Defaults to the active MCP project.
1038    project_path: Option<String>,
1039    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
1040    worktree: Option<String>,
1041    /// Search query for path and purpose matching.
1042    query: Option<String>,
1043    /// Folder path to constrain file lookup.
1044    folder: Option<String>,
1045    /// Opt in to nearest indexed `ProjectAtlas` project discovery for absolute folder paths.
1046    nearest_project: Option<bool>,
1047    /// Optional repository-relative glob filter.
1048    file_pattern: Option<String>,
1049    /// Include indexed file text as a bounded fallback ranking signal.
1050    include_content: Option<bool>,
1051    /// Optional classified-content selection: source, documentation, or both.
1052    content_selection: Option<String>,
1053    /// Maximum number of rows to return.
1054    limit: Option<usize>,
1055}
1056
1057/// MCP parameter payload for outlining a file.
1058#[derive(Debug, Deserialize, schemars::JsonSchema)]
1059struct AtlasOutlineParams {
1060    /// Optional project root for this call. Defaults to the active MCP project.
1061    project_path: Option<String>,
1062    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
1063    worktree: Option<String>,
1064    /// Repository-relative file path.
1065    file: String,
1066    /// Opt in to nearest indexed `ProjectAtlas` project discovery for absolute file paths.
1067    nearest_project: Option<bool>,
1068    /// Number of non-empty preview lines to include.
1069    lines: Option<usize>,
1070}
1071
1072/// MCP parameter payload for deterministic file summaries.
1073#[derive(Debug, Deserialize, schemars::JsonSchema)]
1074struct AtlasFileSummaryParams {
1075    /// Optional project root for this call. Defaults to the active MCP project.
1076    project_path: Option<String>,
1077    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
1078    worktree: Option<String>,
1079    /// Repository-relative file path.
1080    file: String,
1081    /// Opt in to nearest indexed `ProjectAtlas` project discovery for absolute file paths.
1082    nearest_project: Option<bool>,
1083    /// Return the additive compact summary projection when true.
1084    compact: Option<bool>,
1085    /// Optional classified-content selection: source, documentation, or both.
1086    content_selection: Option<String>,
1087    /// Maximum rows per functions/methods/classes/types/calls section.
1088    limit: Option<usize>,
1089}
1090
1091/// MCP parameter payload for text search.
1092#[derive(Debug, Deserialize, schemars::JsonSchema)]
1093struct AtlasSearchParams {
1094    /// Optional project root for this call. Defaults to the active MCP project.
1095    project_path: Option<String>,
1096    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
1097    worktree: Option<String>,
1098    /// Literal, regex, or fuzzy pattern to search for.
1099    pattern: String,
1100    /// Retrieval family; lexical is the default and always-available mode.
1101    retrieval_mode: Option<SearchRetrievalModeArg>,
1102    /// Treat the pattern as a regex.
1103    regex: Option<bool>,
1104    /// Treat the pattern as a fuzzy subsequence.
1105    fuzzy: Option<bool>,
1106    /// Match case-sensitively.
1107    case_sensitive: Option<bool>,
1108    /// Optional repository-relative glob filter.
1109    file_pattern: Option<String>,
1110    /// Number of context lines before and after a match.
1111    context_lines: Option<usize>,
1112    /// Pagination start index.
1113    start_index: Option<usize>,
1114    /// Optional classified-content selection: source, documentation, or both.
1115    content_selection: Option<String>,
1116    /// Maximum matches to return.
1117    limit: Option<usize>,
1118}
1119
1120/// MCP parameter payload for exact source slices.
1121#[derive(Debug, Deserialize, schemars::JsonSchema)]
1122struct AtlasSliceParams {
1123    /// Optional project root for this call. Defaults to the active MCP project.
1124    project_path: Option<String>,
1125    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
1126    worktree: Option<String>,
1127    /// Repository-relative file path.
1128    file: String,
1129    /// Opt in to nearest indexed `ProjectAtlas` project discovery for absolute file paths.
1130    nearest_project: Option<bool>,
1131    /// One-based start line when no symbol is supplied.
1132    start_line: Option<usize>,
1133    /// Optional one-based end line.
1134    end_line: Option<usize>,
1135    /// Symbol name to slice instead of line numbers.
1136    symbol: Option<String>,
1137    /// Optional parent symbol for disambiguating `symbol`.
1138    symbol_parent: Option<String>,
1139    /// Optional symbol kind for disambiguating `symbol`.
1140    symbol_kind: Option<String>,
1141    /// Optional exact signature for disambiguating `symbol`.
1142    symbol_signature: Option<String>,
1143    /// Optional source line for disambiguating `symbol`.
1144    symbol_line: Option<usize>,
1145    /// Optional classified-content selection: source, documentation, or both.
1146    content_selection: Option<String>,
1147    /// Maximum encoded bytes admitted to the slice response.
1148    output_bytes: Option<u32>,
1149}
1150
1151/// MCP parameter payload for symbol and relation lookup.
1152#[derive(Debug, Deserialize, schemars::JsonSchema)]
1153struct AtlasSymbolsParams {
1154    /// Optional project root for this call. Defaults to the active MCP project.
1155    project_path: Option<String>,
1156    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
1157    worktree: Option<String>,
1158    /// Optional repository-relative file path.
1159    file: Option<String>,
1160    /// Opt in to nearest indexed `ProjectAtlas` project discovery for absolute file paths.
1161    nearest_project: Option<bool>,
1162    /// Optional symbol, signature, relation, or path query.
1163    query: Option<String>,
1164    /// Optional classified-content selection: source, documentation, or both.
1165    content_selection: Option<String>,
1166    /// Maximum rows to return.
1167    limit: Option<usize>,
1168}
1169
1170/// MCP parameters for legacy, detailed, or closed-analysis relation navigation.
1171#[derive(Debug, Default, Deserialize, schemars::JsonSchema)]
1172struct AtlasSymbolRelationsParams {
1173    /// Optional project root for this call. Defaults to the active MCP project.
1174    project_path: Option<String>,
1175    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
1176    worktree: Option<String>,
1177    /// Optional repository-relative file path.
1178    file: Option<String>,
1179    /// Opt in to nearest indexed `ProjectAtlas` project discovery for absolute file paths.
1180    nearest_project: Option<bool>,
1181    /// Optional symbol, signature, relation, or path query.
1182    query: Option<String>,
1183    /// Preserve `legacy`, opt in to `detailed`, or select closed `analysis`.
1184    view: Option<String>,
1185    /// Optional classified-content selection: source, documentation, or both.
1186    content_selection: Option<String>,
1187    /// Return the opt-in compact detailed projection while preserving exact selectors and trust.
1188    compact: Option<bool>,
1189    /// Resume one exact generation- and purpose-bound detailed page.
1190    cursor: Option<String>,
1191    /// Complete ordered project-root set for one read-only federated call.
1192    roots: Option<Vec<String>>,
1193    /// Complete ordered registered worktree aliases for one read-only federated call.
1194    worktrees: Option<Vec<String>>,
1195    /// Exact symbol name used as the detailed anchor; omit for a file anchor.
1196    symbol: Option<String>,
1197    /// Optional exact parent used to disambiguate the detailed symbol anchor.
1198    symbol_parent: Option<String>,
1199    /// Optional exact kind used to disambiguate the detailed symbol anchor.
1200    symbol_kind: Option<String>,
1201    /// Optional exact signature used to disambiguate the detailed symbol anchor.
1202    symbol_signature: Option<String>,
1203    /// Detailed traversal direction: `outbound` or `inbound`.
1204    direction: Option<String>,
1205    /// Optional exact legacy or extended relation family.
1206    relation: Option<String>,
1207    /// Detailed confidence floor: `exact`, `high`, `medium`, or `low`.
1208    minimum_confidence: Option<String>,
1209    /// Detailed resolution filter.
1210    resolution: Option<String>,
1211    /// Maximum detailed traversal depth.
1212    depth: Option<u32>,
1213    /// Retain bounded exact source occurrences in detailed rows.
1214    include_occurrences: Option<bool>,
1215    /// Maximum exact occurrences retained per detailed relation.
1216    occurrence_limit: Option<u32>,
1217    /// Maximum adjacency rows inspected by one detailed page.
1218    edge_limit: Option<u32>,
1219    /// Maximum unique traversal nodes retained across continuation state.
1220    node_limit: Option<u32>,
1221    /// Maximum unique visited-node state retained across continuation state.
1222    visited_limit: Option<u32>,
1223    /// Maximum exact occurrences retained across the complete page.
1224    occurrence_total_limit: Option<u32>,
1225    /// Maximum decoded, cursor, and service-composition intermediate bytes.
1226    intermediate_bytes: Option<u64>,
1227    /// Maximum service-owned elapsed milliseconds.
1228    deadline_ms: Option<u64>,
1229    /// Maximum encoded bytes admitted to the detailed response.
1230    output_bytes: Option<u32>,
1231    /// Closed analysis mode: `architecture`, `impact`, `trace`, or `entrypoint`.
1232    analysis_mode: Option<String>,
1233    /// Stable name for an explicit entrypoint profile.
1234    profile_name: Option<String>,
1235    /// JSON `RelationAnchor` values; repeat for multiple entrypoints.
1236    entrypoints: Option<Vec<String>>,
1237    /// Relation families admitted by an entrypoint profile.
1238    profile_relations: Option<Vec<String>>,
1239    /// Exact target symbol name for trace mode.
1240    trace_target: Option<String>,
1241    /// Exact target file for trace mode; alone selects a file target.
1242    trace_target_file: Option<String>,
1243    /// Exact target parent for symbol trace mode.
1244    trace_target_parent: Option<String>,
1245    /// Exact target kind required by symbol trace mode.
1246    trace_target_kind: Option<String>,
1247    /// Exact target signature required by symbol trace mode.
1248    trace_target_signature: Option<String>,
1249    /// Impact VCS scope: `working_tree`, `index`, or `revision_range`.
1250    vcs: Option<String>,
1251    /// Older Git revision used by `revision_range`.
1252    vcs_base: Option<String>,
1253    /// Newer Git revision used by `revision_range`.
1254    vcs_head: Option<String>,
1255    /// Include communities with containment excluded.
1256    include_communities: Option<bool>,
1257    /// Include dependency SCC findings.
1258    include_cycles: Option<bool>,
1259    /// Include conservative dead-code candidates.
1260    include_dead_code: Option<bool>,
1261    /// Maximum rows to return.
1262    limit: Option<usize>,
1263}
1264
1265/// Return whether any closed analysis-only control was supplied.
1266fn relation_analysis_controls_present(params: &AtlasSymbolRelationsParams) -> bool {
1267    params.analysis_mode.is_some()
1268        || params.profile_name.is_some()
1269        || params
1270            .entrypoints
1271            .as_ref()
1272            .is_some_and(|items| !items.is_empty())
1273        || params
1274            .profile_relations
1275            .as_ref()
1276            .is_some_and(|items| !items.is_empty())
1277        || params.trace_target.is_some()
1278        || params.trace_target_file.is_some()
1279        || params.trace_target_parent.is_some()
1280        || params.trace_target_kind.is_some()
1281        || params.trace_target_signature.is_some()
1282        || params.vcs.is_some()
1283        || params.vcs_base.is_some()
1284        || params.vcs_head.is_some()
1285        || params.include_communities.is_some()
1286        || params.include_cycles.is_some()
1287        || params.include_dead_code.is_some()
1288}
1289
1290/// Decode and validate the optional exact trace target.
1291fn relation_analysis_trace_target(
1292    store: &AtlasStore,
1293    params: &AtlasSymbolRelationsParams,
1294) -> Result<Option<RelationAnchor>, CliError> {
1295    match (&params.trace_target, &params.trace_target_file) {
1296        (Some(name), Some(file)) => {
1297            let file = validated_indexed_file_key(store, Path::new(file))?;
1298            let kind = params.trace_target_kind.as_deref().ok_or_else(|| {
1299                CliError::Service(ServiceError::InvalidInput(
1300                    MCP_ERROR_TRACE_TARGET_KIND_REQUIRED.to_string(),
1301                ))
1302            })?;
1303            let signature = params.trace_target_signature.clone().ok_or_else(|| {
1304                CliError::Service(ServiceError::InvalidInput(
1305                    MCP_ERROR_TRACE_TARGET_SIGNATURE_REQUIRED.to_string(),
1306                ))
1307            })?;
1308            Ok(Some(RelationAnchor::Symbol {
1309                file: RepositoryFilePath::new(Path::new(&file)).map_err(|error| {
1310                    CliError::Service(ServiceError::InvalidInput(error.to_string()))
1311                })?,
1312                name: name.clone(),
1313                symbol_kind: Some(parse_symbol_kind(kind)?),
1314                parent: params.trace_target_parent.clone(),
1315                signature: Some(signature),
1316            }))
1317        }
1318        (None, Some(file)) => {
1319            if params.trace_target_parent.is_some()
1320                || params.trace_target_kind.is_some()
1321                || params.trace_target_signature.is_some()
1322            {
1323                return Err(CliError::Service(ServiceError::InvalidInput(
1324                    MCP_ERROR_TRACE_TARGET_REQUIRED.to_string(),
1325                )));
1326            }
1327            let file = validated_indexed_file_key(store, Path::new(file))?;
1328            Ok(Some(RelationAnchor::File {
1329                file: RepositoryFilePath::new(Path::new(&file)).map_err(|error| {
1330                    CliError::Service(ServiceError::InvalidInput(error.to_string()))
1331                })?,
1332            }))
1333        }
1334        (Some(_), None) => Err(CliError::Service(ServiceError::InvalidInput(
1335            MCP_ERROR_TRACE_TARGET_FILE_REQUIRED.to_string(),
1336        ))),
1337        (None, None) => Ok(None),
1338    }
1339}
1340
1341/// Decode and validate the optional VCS impact selector.
1342fn relation_analysis_vcs(
1343    params: &AtlasSymbolRelationsParams,
1344) -> Result<GitImpactSelection, CliError> {
1345    match params
1346        .vcs
1347        .as_deref()
1348        .unwrap_or(MCP_RELATION_ANALYSIS_VCS_WORKING_TREE)
1349    {
1350        MCP_RELATION_ANALYSIS_VCS_WORKING_TREE => {
1351            if params.vcs_base.is_some() || params.vcs_head.is_some() {
1352                return Err(CliError::Service(ServiceError::InvalidInput(
1353                    MCP_ERROR_VCS_REVISION_FIELDS.to_string(),
1354                )));
1355            }
1356            Ok(GitImpactSelection::WorkingTree)
1357        }
1358        MCP_RELATION_ANALYSIS_VCS_INDEX => {
1359            if params.vcs_base.is_some() || params.vcs_head.is_some() {
1360                return Err(CliError::Service(ServiceError::InvalidInput(
1361                    MCP_ERROR_VCS_REVISION_FIELDS.to_string(),
1362                )));
1363            }
1364            Ok(GitImpactSelection::Index)
1365        }
1366        MCP_RELATION_ANALYSIS_VCS_REVISION_RANGE => Ok(GitImpactSelection::RevisionRange {
1367            base: params.vcs_base.clone().ok_or_else(|| {
1368                CliError::Service(ServiceError::InvalidInput(
1369                    MCP_ERROR_VCS_BASE_REQUIRED.to_string(),
1370                ))
1371            })?,
1372            head: params.vcs_head.clone().ok_or_else(|| {
1373                CliError::Service(ServiceError::InvalidInput(
1374                    MCP_ERROR_VCS_HEAD_REQUIRED.to_string(),
1375                ))
1376            })?,
1377        }),
1378        _unsupported => Err(CliError::Service(ServiceError::InvalidInput(
1379            MCP_ERROR_UNSUPPORTED_ANALYSIS_VCS.to_string(),
1380        ))),
1381    }
1382}
1383
1384/// MCP parameter payload for token savings reports.
1385#[derive(Debug, Deserialize, schemars::JsonSchema)]
1386struct AtlasTokenParams {
1387    /// Optional project root for this call. Defaults to the active MCP project.
1388    project_path: Option<String>,
1389    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
1390    worktree: Option<String>,
1391    /// Optional session id filter.
1392    session: Option<String>,
1393    /// Include a readable ASCII chart in the MCP result.
1394    include_chart: Option<bool>,
1395    /// Optional trend grouping window: day, week, month, or year.
1396    trend_window: Option<String>,
1397    /// Optional repository-relative agent-navigation benchmark result.
1398    benchmark_results: Option<String>,
1399    /// Optional chart theme for TUI output: dark or light.
1400    theme: Option<String>,
1401}
1402
1403/// MCP parameter payload for bounded health finding lookup.
1404#[derive(Debug, Deserialize, schemars::JsonSchema)]
1405struct AtlasHealthParams {
1406    /// Optional project root for this call. Defaults to the active MCP project.
1407    project_path: Option<String>,
1408    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
1409    worktree: Option<String>,
1410    /// Pagination start index after filters are applied.
1411    start_index: Option<usize>,
1412    /// Maximum findings to return, capped to a safe MCP page size.
1413    limit: Option<usize>,
1414    /// Optional finding category filter.
1415    category: Option<String>,
1416    /// Optional severity filter: info, warning, or error.
1417    severity: Option<String>,
1418    /// Optional repository-relative primary or related path prefix.
1419    path_prefix: Option<String>,
1420    /// Return counts and paging metadata without finding rows.
1421    summary_only: Option<bool>,
1422    /// Restrict findings to source files and folders that contain source files.
1423    source_only: Option<bool>,
1424    /// Include non-source files and asset-only folders in the purpose queue.
1425    include_assets: Option<bool>,
1426    /// Include low-priority files in the purpose queue.
1427    include_low_priority_files: Option<bool>,
1428    /// Opt in to bounded current coverage discovery instead of structural findings.
1429    coverage: Option<bool>,
1430    /// Optional source parser coverage filter.
1431    parser: Option<String>,
1432    /// Optional derived-fact provider coverage filter.
1433    provider: Option<String>,
1434    /// Optional relationship-family coverage filter.
1435    relation: Option<String>,
1436    /// Optional complete, partial, failed, ignored, oversized, quarantined, or stale filter.
1437    coverage_state: Option<String>,
1438    /// Optional exact coverage reason filter.
1439    reason: Option<String>,
1440}
1441
1442/// MCP parameter payload for bounded task-scoped purpose curation.
1443#[derive(Debug, Deserialize, schemars::JsonSchema)]
1444struct AtlasPurposeQueueParams {
1445    /// Shared health paging and purpose-scope filters.
1446    #[serde(flatten)]
1447    health: AtlasHealthParams,
1448    /// Host-owned task label for deterministic purpose-curator work identity.
1449    task: Option<String>,
1450}
1451
1452/// MCP parameter payload for parity reports.
1453#[derive(Debug, Deserialize, schemars::JsonSchema)]
1454struct AtlasParityParams {
1455    /// Optional project root for this call. Defaults to the active MCP project.
1456    project_path: Option<String>,
1457    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
1458    worktree: Option<String>,
1459    /// Parity profile. Defaults to repository-intelligence.
1460    profile: Option<String>,
1461}
1462
1463/// MCP parameter payload for legacy purpose cleanup.
1464#[derive(Debug, Deserialize, schemars::JsonSchema)]
1465struct AtlasStripLegacyParams {
1466    /// Optional project root for this call. Defaults to the active MCP project.
1467    project_path: Option<String>,
1468    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
1469    worktree: Option<String>,
1470    /// Repository root path. Defaults to the configured or indexed project root.
1471    path: Option<String>,
1472    /// Opt in to nearest indexed `ProjectAtlas` project discovery for absolute paths.
1473    nearest_project: Option<bool>,
1474    /// Remove legacy `.purpose` files when true.
1475    apply: Option<bool>,
1476    /// Preview cleanup without modifying files.
1477    dry_run: Option<bool>,
1478    /// Also report conservative source Purpose header candidates.
1479    strip_source_headers: Option<bool>,
1480}
1481
1482/// MCP parameter payload for runtime index cleanup.
1483#[derive(Debug, Deserialize, schemars::JsonSchema)]
1484struct AtlasResetIndexParams {
1485    /// Optional project root for this call. Defaults to the active MCP project.
1486    project_path: Option<String>,
1487    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
1488    worktree: Option<String>,
1489    /// Remove runtime index/cache files when true.
1490    apply: Option<bool>,
1491    /// Preview cleanup without modifying files.
1492    dry_run: Option<bool>,
1493    /// Also remove generated project-local MCP config.
1494    include_mcp_config: Option<bool>,
1495}
1496
1497/// MCP parameter payload for setting purpose metadata.
1498#[derive(Debug, Deserialize, schemars::JsonSchema)]
1499struct AtlasPurposeSetParams {
1500    /// Optional project root for this call. Defaults to the active MCP project.
1501    project_path: Option<String>,
1502    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
1503    worktree: Option<String>,
1504    /// Indexed repository-relative path.
1505    path: String,
1506    /// Agent-approved purpose one-liner.
1507    purpose: String,
1508}
1509
1510/// MCP payload for one batch purpose review item.
1511#[derive(Debug, Deserialize, schemars::JsonSchema)]
1512#[schemars(inline)]
1513struct AtlasPurposeReviewItem {
1514    /// Indexed repository-relative path.
1515    path: String,
1516    /// Agent-reviewed purpose one-liner. Required for generated suggestions.
1517    purpose: Option<String>,
1518    /// Confirm the existing non-generated purpose after inspection.
1519    confirm_existing: Option<bool>,
1520    /// Queue task copied from the purpose-curation item.
1521    task: Option<String>,
1522    /// Queue work key copied from the purpose-curation item.
1523    work_key: Option<String>,
1524    /// Queue state token copied from the purpose-curation item.
1525    state_token: Option<String>,
1526}
1527
1528/// MCP parameter payload for batch purpose review.
1529#[derive(Debug, Deserialize, schemars::JsonSchema)]
1530struct AtlasPurposeReviewParams {
1531    /// Optional project root for this call. Defaults to the active MCP project.
1532    project_path: Option<String>,
1533    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
1534    worktree: Option<String>,
1535    /// Purpose records to agent-review.
1536    items: Vec<AtlasPurposeReviewItem>,
1537    /// Apply reviewed purposes. Defaults to false for preview.
1538    apply: Option<bool>,
1539}
1540
1541/// MCP parameter payload for resolving health findings.
1542#[derive(Debug, Deserialize, schemars::JsonSchema)]
1543struct AtlasHealthResolveParams {
1544    /// Optional project root for this call. Defaults to the active MCP project.
1545    project_path: Option<String>,
1546    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
1547    worktree: Option<String>,
1548    /// Stable finding id from `atlas_health`.
1549    finding_id: String,
1550    /// Finding category.
1551    category: String,
1552    /// Primary path.
1553    path: String,
1554    /// Optional related path.
1555    related_path: Option<String>,
1556    /// Agent rationale for resolving the finding.
1557    rationale: String,
1558}
1559
1560/// Active `ProjectAtlas` database and configuration selected for MCP calls.
1561#[derive(Debug, Clone)]
1562struct McpProjectState {
1563    /// Canonical selected repository root.
1564    root: PathBuf,
1565    /// Selected durable `SQLite` index path.
1566    db_path: PathBuf,
1567    /// Selected scan/import configuration path.
1568    config_path: Option<PathBuf>,
1569    /// Registered alias captured for this call, when alias routing selected the state.
1570    worktree: Option<McpWorktreeSelection>,
1571}
1572
1573/// Stable control-catalog identity captured with one alias-routed project state.
1574#[derive(Debug, Clone, Eq, PartialEq)]
1575struct McpWorktreeSelection {
1576    /// Reserved `main` or registered short alias.
1577    alias: String,
1578    /// Durable registration identity; absent only for reserved `main`.
1579    registration_id: Option<i64>,
1580    /// Project identity captured for the selected alias target, when present.
1581    project_instance_id: Option<ProjectInstanceId>,
1582    /// Control-atlas identity that owns the captured registration ID.
1583    control_project_instance_id: Option<ProjectInstanceId>,
1584}
1585
1586/// Outcome of evaluating the control atlas as an init hydration source.
1587enum McpWorktreeHydration {
1588    /// One target-exact candidate was reconciled and activated.
1589    Activated {
1590        /// Public init diagnostic.
1591        hydration: InitHydrationPhase,
1592        /// Normal scan result produced against the private candidate.
1593        scan: Box<ScanReport>,
1594    },
1595    /// Ordinary init must be used and disclose this reason.
1596    Fallback(String),
1597}
1598
1599/// Store ownership selected by the existing relation tool request shape.
1600enum SymbolRelationStores<'a> {
1601    /// Compatibility-preserving selected-project query.
1602    Single(&'a AtlasStore),
1603    /// Explicit call-owned ordered read snapshots.
1604    Federated(Vec<FederatedStore>),
1605}
1606
1607impl SymbolRelationStores<'_> {
1608    /// Borrow the selected first project while validating local selectors.
1609    fn primary(&self) -> &AtlasStore {
1610        match self {
1611            Self::Single(store) => store,
1612            Self::Federated(stores) => stores[0].store(),
1613        }
1614    }
1615}
1616
1617/// Exact root/database/project identity where this MCP process recorded telemetry.
1618#[derive(Debug, Clone, Eq, Hash, PartialEq)]
1619struct McpUsageProjectBinding {
1620    /// Canonical selected repository root.
1621    root: PathBuf,
1622    /// Exact authoritative database used for the telemetry write.
1623    db_path: PathBuf,
1624    /// Project identity captured by the already-open selected store.
1625    project_instance_id: ProjectInstanceId,
1626    /// Captured routed origin; absent for native events and source baselines.
1627    worktree_registration_id: Option<i64>,
1628}
1629
1630/// One bounded broad-source token baseline keyed to a complete generation.
1631#[derive(Clone, Debug, Eq, Hash, PartialEq)]
1632struct McpSourceTokenBaselineKey {
1633    /// Exact project binding whose source files supplied the baseline.
1634    binding: McpUsageProjectBinding,
1635    /// Complete publication generation represented by the baseline.
1636    generation: projectatlas_core::IndexGeneration,
1637    /// Optional repository folder filter applied to the baseline.
1638    folder: Option<String>,
1639    /// Optional repository file-pattern filter applied to the baseline.
1640    file_pattern: Option<String>,
1641}
1642
1643/// Deferred telemetry payload recorded only after a verified result is accepted.
1644#[derive(Debug)]
1645struct McpUsageIntent {
1646    /// Stable MCP event family.
1647    command: &'static str,
1648    /// Optional selected path or filter.
1649    path: Option<String>,
1650    /// Optional caller query.
1651    query: Option<String>,
1652    /// Baseline used by the existing usage accounting contract.
1653    baseline: McpUsageBaseline,
1654}
1655
1656/// Existing telemetry baseline variants retained across verified-read acceptance.
1657#[derive(Debug)]
1658enum McpUsageBaseline {
1659    /// Modeled selected-candidate token count.
1660    Estimate(usize),
1661    /// Modeled avoided directory-walk token count.
1662    DirectoryWalk(usize),
1663    /// Exact source text replaced by the accepted response.
1664    Text(String),
1665}
1666
1667impl McpUsageIntent {
1668    /// Defer one selected-candidate token estimate until result acceptance.
1669    fn estimate(
1670        command: &'static str,
1671        path: Option<String>,
1672        query: Option<String>,
1673        baseline_tokens: usize,
1674    ) -> Self {
1675        Self {
1676            command,
1677            path,
1678            query,
1679            baseline: McpUsageBaseline::Estimate(baseline_tokens),
1680        }
1681    }
1682
1683    /// Defer one avoided directory-walk estimate until result acceptance.
1684    fn directory_walk(
1685        command: &'static str,
1686        path: Option<String>,
1687        query: Option<String>,
1688        baseline_tokens: usize,
1689    ) -> Self {
1690        Self {
1691            command,
1692            path,
1693            query,
1694            baseline: McpUsageBaseline::DirectoryWalk(baseline_tokens),
1695        }
1696    }
1697
1698    /// Defer one exact source-text replacement event until result acceptance.
1699    fn text(command: &'static str, path: Option<String>, baseline_text: String) -> Self {
1700        Self {
1701            command,
1702            path,
1703            query: None,
1704            baseline: McpUsageBaseline::Text(baseline_text),
1705        }
1706    }
1707}
1708
1709impl McpUsageProjectBinding {
1710    /// Capture one native telemetry binding in focused tests.
1711    #[cfg(test)]
1712    fn capture(state: &McpProjectState, store: &AtlasStore) -> Result<Self, DbError> {
1713        Self::capture_with_origin(state, store, None)
1714    }
1715
1716    /// Capture one exact telemetry authority and optional registered origin.
1717    fn capture_with_origin(
1718        state: &McpProjectState,
1719        store: &AtlasStore,
1720        worktree_registration_id: Option<i64>,
1721    ) -> Result<Self, DbError> {
1722        let captured = store.captured_project_binding()?;
1723        Ok(Self {
1724            root: state.root.clone(),
1725            db_path: state.db_path.clone(),
1726            project_instance_id: captured.project_instance_id,
1727            worktree_registration_id,
1728        })
1729    }
1730
1731    /// Return whether two origin bindings share one exact project database authority.
1732    fn same_project(&self, other: &Self) -> bool {
1733        self.root == other.root
1734            && self.db_path == other.db_path
1735            && self.project_instance_id == other.project_instance_id
1736    }
1737}
1738
1739/// Current telemetry identity for one exact selected project binding.
1740#[derive(Clone, Debug)]
1741struct McpUsageProjectRuntime {
1742    /// Exact root/database authority that owns this identity.
1743    binding: McpUsageProjectBinding,
1744    /// Current bounded identity for this process/project pair.
1745    instance: Arc<Mutex<UsageRuntimeInstance>>,
1746}
1747
1748/// Mutable bounded telemetry lifecycles shared by all MCP server clones.
1749#[derive(Debug, Default)]
1750struct McpUsageRuntime {
1751    /// Distinct selected-project identities owned by this MCP process.
1752    entries: Vec<McpUsageProjectRuntime>,
1753    /// Bounded modeled baselines reused without decoding every indexed file per call.
1754    source_token_baselines: VecDeque<(McpSourceTokenBaselineKey, usize)>,
1755}
1756
1757impl McpUsageRuntime {
1758    /// Return or create the identity for one selected project binding.
1759    fn instance_for_binding(
1760        &mut self,
1761        binding: McpUsageProjectBinding,
1762        selected_store: &AtlasStore,
1763    ) -> Option<Arc<Mutex<UsageRuntimeInstance>>> {
1764        if let Some(index) = self
1765            .entries
1766            .iter()
1767            .position(|entry| entry.binding == binding)
1768        {
1769            let entry = self.entries.remove(index);
1770            let instance = Arc::clone(&entry.instance);
1771            self.entries.push(entry);
1772            return Some(instance);
1773        }
1774        let instance = Arc::new(Mutex::new(UsageRuntimeInstance::new(
1775            UsageInstanceOwner::McpProcess,
1776        )?));
1777        if self.entries.len() >= MCP_TELEMETRY_PROJECT_BINDING_LIMIT {
1778            let index = (0..self.entries.len()).find(|index| {
1779                Self::seal_inactive_entry(&self.entries[*index], &binding, selected_store)
1780            })?;
1781            self.entries.remove(index);
1782        }
1783        self.entries.push(McpUsageProjectRuntime {
1784            binding,
1785            instance: Arc::clone(&instance),
1786        });
1787        Some(instance)
1788    }
1789
1790    /// Seal one unborrowed least-recent binding before bounded replacement.
1791    fn seal_inactive_entry(
1792        entry: &McpUsageProjectRuntime,
1793        selected_binding: &McpUsageProjectBinding,
1794        selected_store: &AtlasStore,
1795    ) -> bool {
1796        if Arc::strong_count(&entry.instance) != 1 {
1797            return false;
1798        }
1799        let Ok(instance) = entry.instance.try_lock() else {
1800            return false;
1801        };
1802        let seal = |store: &AtlasStore| {
1803            store.captured_project_binding().is_ok_and(|binding| {
1804                binding.project_instance_id == entry.binding.project_instance_id
1805            }) && matches!(
1806                (*instance).seal(store),
1807                Ok(()) | Err(CliError::Db(DbError::TelemetryInstanceInactive))
1808            )
1809        };
1810        if entry.binding.same_project(selected_binding) {
1811            return seal(selected_store);
1812        }
1813        open_atlas_store_for_project(&entry.binding.db_path, &entry.binding.root)
1814            .is_ok_and(|store| seal(&store))
1815    }
1816
1817    /// Clone the bounded project/runtime set before shutdown database I/O.
1818    fn snapshot(&self) -> Vec<McpUsageProjectRuntime> {
1819        self.entries.clone()
1820    }
1821
1822    /// Return a cached generation-bound broad-source token baseline.
1823    fn source_token_baseline(&self, key: &McpSourceTokenBaselineKey) -> Option<usize> {
1824        self.source_token_baselines
1825            .iter()
1826            .find_map(|(candidate, value)| (candidate == key).then_some(*value))
1827    }
1828
1829    /// Retain one baseline without allowing arbitrary filter keys to grow memory.
1830    fn insert_source_token_baseline(&mut self, key: McpSourceTokenBaselineKey, value: usize) {
1831        if let Some(index) = self
1832            .source_token_baselines
1833            .iter()
1834            .position(|(candidate, _value)| candidate == &key)
1835        {
1836            let _removed = self.source_token_baselines.remove(index);
1837        }
1838        while self.source_token_baselines.len() >= MCP_SOURCE_TOKEN_BASELINE_LIMIT {
1839            let _removed = self.source_token_baselines.pop_front();
1840        }
1841        self.source_token_baselines.push_back((key, value));
1842    }
1843}
1844
1845/// Select whether project-state discovery validates configuration content immediately.
1846#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1847enum McpConfigValidation {
1848    /// Validate configuration before returning selected project state.
1849    Immediate,
1850    /// Defer configuration reads to the admitted operation-owned work boundary.
1851    Deferred,
1852}
1853
1854/// MCP response for compatibility map export.
1855#[derive(Debug, Serialize)]
1856struct McpMapReport {
1857    /// Canonical project root used for map generation.
1858    root: Option<String>,
1859    /// Map path from the effective config.
1860    map_path: Option<String>,
1861    /// Whether a map file was written by this call.
1862    written: bool,
1863    /// Whether JSON compatibility output was requested.
1864    json: bool,
1865    /// Human-readable reason when no file was written.
1866    skipped_reason: Option<String>,
1867}
1868
1869/// Bounded control-atlas view of Git and `ProjectAtlas` worktree state.
1870#[derive(Debug, Serialize)]
1871struct McpWorktreeListReport {
1872    /// Reserved alias for the selected control checkout.
1873    control_alias: &'static str,
1874    /// Exact selected control checkout root.
1875    control_root: Option<String>,
1876    /// Structurally validated Git common directory.
1877    common_directory: Option<String>,
1878    /// Active Git inventory, bounded for one agent response.
1879    worktrees: Vec<McpWorktreeRow>,
1880    /// Optional retired `ProjectAtlas` registrations retained for telemetry history.
1881    retired: Vec<McpRetiredWorktreeRow>,
1882    /// Complete structural row count before response truncation.
1883    total_worktrees: usize,
1884    /// Whether structural rows exceeded the public response bound.
1885    truncated: bool,
1886}
1887
1888/// One structurally discovered Git worktree joined to `ProjectAtlas` state.
1889#[derive(Debug, Serialize)]
1890struct McpWorktreeRow {
1891    /// Stable selector when the structural row is representable and registrable.
1892    selector: Option<String>,
1893    /// Reserved `main` or active registered alias when present.
1894    alias: Option<String>,
1895    /// Structural primary/linked role.
1896    role: McpGitWorktreeRole,
1897    /// Whether all displayed native paths have a lossless UTF-8 projection.
1898    path_display: McpWorktreePathDisplayState,
1899    /// Current reciprocal Git state.
1900    git_state: McpGitWorktreeState,
1901    /// `ProjectAtlas` registration relationship.
1902    registration: McpWorktreeRegistrationState,
1903    /// Stable Git administrative identity.
1904    administrative_directory: Option<String>,
1905    /// Exact current source root when active.
1906    root: Option<String>,
1907    /// Exact atlas availability for the current source root.
1908    atlas_state: McpWorktreeAtlasState,
1909    /// Local aggregate relationship to the accepted control snapshot.
1910    telemetry_state: McpWorktreeTelemetryState,
1911    /// Last local aggregate revision accepted by the control atlas.
1912    accepted_telemetry_revision: Option<u64>,
1913    /// Current local aggregate revision when safely readable.
1914    local_telemetry_revision: Option<u64>,
1915    /// Exact initialized project identity when safely readable.
1916    project_instance_id: Option<String>,
1917    /// Typed structural or atlas diagnostic for unavailable rows.
1918    blocker: Option<String>,
1919}
1920
1921/// Retired `ProjectAtlas` registration retained outside source selection.
1922#[derive(Debug, Serialize)]
1923struct McpRetiredWorktreeRow {
1924    /// Last human/agent-facing alias.
1925    alias: String,
1926    /// Last structurally validated source root.
1927    last_root: Option<String>,
1928    /// Whether the retained root has a lossless UTF-8 projection.
1929    path_display: McpWorktreePathDisplayState,
1930    /// Exact initialized project identity when one was bound.
1931    project_instance_id: Option<String>,
1932    /// Last local aggregate revision accepted before retirement.
1933    accepted_telemetry_revision: u64,
1934}
1935
1936/// Compact candidate returned for ambiguous or missing add selection.
1937#[derive(Debug, Serialize)]
1938struct McpWorktreeCandidate {
1939    /// Stable selector accepted by a later add call.
1940    selector: String,
1941    /// Exact active source root.
1942    root: Option<String>,
1943    /// Whether the candidate root has a lossless UTF-8 projection.
1944    path_display: McpWorktreePathDisplayState,
1945    /// Structural primary/linked role.
1946    role: McpGitWorktreeRole,
1947}
1948
1949/// Result of one ProjectAtlas-only worktree registration transition.
1950#[derive(Debug, Serialize)]
1951struct McpWorktreeMutationReport {
1952    /// Registration operation that was requested.
1953    operation: McpWorktreeMutationOperation,
1954    /// Mutation or bounded selection outcome.
1955    status: McpWorktreeMutationStatus,
1956    /// Stable structural selector when one candidate was selected.
1957    selector: Option<String>,
1958    /// Active or retired `ProjectAtlas` alias when known.
1959    alias: Option<String>,
1960    /// Exact source root when known.
1961    root: Option<String>,
1962    /// Display state for the selected native source root.
1963    path_display: Option<McpWorktreePathDisplayState>,
1964    /// Stable control-database registration identity when committed.
1965    registration_id: Option<i64>,
1966    /// Final local telemetry synchronization outcome when attempted.
1967    telemetry_sync: Option<WorktreeUsageSyncState>,
1968    /// Bounded candidates when a human selector was ambiguous.
1969    candidates: Vec<McpWorktreeCandidate>,
1970    /// Typed synchronization or structural note when the transition is partial by design.
1971    blocker: Option<String>,
1972    /// Git lifecycle is never changed by these operations.
1973    git_unchanged: bool,
1974    /// Source, `.projectatlas`, and database files are never deleted by unregister.
1975    files_unchanged: bool,
1976}
1977
1978/// Structural role serialized without leaking debug vocabulary.
1979#[derive(Clone, Copy, Debug, Serialize)]
1980#[serde(rename_all = "snake_case")]
1981enum McpGitWorktreeRole {
1982    /// Primary checkout for the Git common directory.
1983    Primary,
1984    /// Linked checkout with reciprocal registration evidence.
1985    Linked,
1986}
1987
1988impl From<GitWorktreeRole> for McpGitWorktreeRole {
1989    fn from(value: GitWorktreeRole) -> Self {
1990        match value {
1991            GitWorktreeRole::Primary => Self::Primary,
1992            GitWorktreeRole::Linked => Self::Linked,
1993        }
1994    }
1995}
1996
1997/// Git state exposed by structural worktree inventory.
1998#[derive(Clone, Copy, Debug, Serialize)]
1999#[serde(rename_all = "snake_case")]
2000enum McpGitWorktreeState {
2001    /// Reciprocal evidence resolves an exact source root.
2002    Active,
2003    /// Git retains registration evidence but the checkout is absent.
2004    Missing,
2005    /// Structural evidence is malformed or unsafe.
2006    Invalid,
2007}
2008
2009/// `ProjectAtlas` registration relationship for one structural row.
2010#[derive(Clone, Copy, Debug, Serialize)]
2011#[serde(rename_all = "snake_case")]
2012enum McpWorktreeRegistrationState {
2013    /// Reserved selected control checkout.
2014    Control,
2015    /// Active short alias is stored in the control atlas.
2016    Registered,
2017    /// Git knows the worktree but `ProjectAtlas` does not.
2018    Unregistered,
2019}
2020
2021/// Lossless UTF-8 availability for native paths at the display boundary.
2022#[derive(Clone, Copy, Debug, Serialize)]
2023#[serde(rename_all = "snake_case")]
2024enum McpWorktreePathDisplayState {
2025    /// The native path can be rendered as UTF-8 without changing bytes.
2026    Available,
2027    /// The native path remains routable by identity but cannot be rendered as UTF-8.
2028    Unavailable,
2029}
2030
2031/// Exact local atlas availability without modifying its database.
2032#[derive(Clone, Copy, Debug, Serialize)]
2033#[serde(rename_all = "snake_case")]
2034enum McpWorktreeAtlasState {
2035    /// A compatible atlas is bound to the exact current root.
2036    Initialized,
2037    /// No target-local database exists.
2038    Missing,
2039    /// A database exists but cannot be admitted for this exact root.
2040    Invalid,
2041    /// Git does not expose an active source root.
2042    Unavailable,
2043}
2044
2045/// Relationship between independent local and durable control telemetry.
2046#[derive(Clone, Copy, Debug, Serialize)]
2047#[serde(rename_all = "snake_case")]
2048enum McpWorktreeTelemetryState {
2049    /// Native telemetry belongs to the selected control atlas.
2050    Control,
2051    /// The latest readable local snapshot has been accepted.
2052    Current,
2053    /// A newer readable local snapshot awaits synchronization.
2054    Pending,
2055    /// No local atlas exists yet.
2056    MissingAtlas,
2057    /// No `ProjectAtlas` registration owns this structural row.
2058    Unregistered,
2059    /// Structural or atlas state prevents a trustworthy comparison.
2060    Unavailable,
2061}
2062
2063/// ProjectAtlas-only registry operation.
2064#[derive(Clone, Copy, Debug, Serialize)]
2065#[serde(rename_all = "snake_case")]
2066enum McpWorktreeMutationOperation {
2067    /// Add or reactivate one alias.
2068    Add,
2069    /// Retire one active alias after final synchronization.
2070    Remove,
2071}
2072
2073/// Result state for one worktree registry operation.
2074#[derive(Clone, Copy, Debug, Serialize)]
2075#[serde(rename_all = "snake_case")]
2076enum McpWorktreeMutationStatus {
2077    /// One active registration was committed.
2078    Registered,
2079    /// One active registration was retired.
2080    Retired,
2081    /// No active structural candidate matched.
2082    NotFound,
2083    /// More than one active structural candidate matched.
2084    Ambiguous,
2085}
2086
2087/// Read-only local atlas facts used by list/add/remove orchestration.
2088struct LocalWorktreeAtlas {
2089    /// Exact atlas identity.
2090    project_instance_id: ProjectInstanceId,
2091    /// Bounded local aggregate snapshot.
2092    snapshot: WorktreeUsageSnapshot,
2093}
2094
2095/// Role-typed selected root for MCP path routing.
2096#[derive(Debug, Clone)]
2097struct McpSelectedRoot(PathBuf);
2098
2099impl McpSelectedRoot {
2100    /// Build the selected root wrapper from active project state.
2101    fn from_state(state: &McpProjectState) -> Self {
2102        Self(state.root.clone())
2103    }
2104
2105    /// Convert an absolute path inside this root into a repository key.
2106    fn repo_key_for(&self, path: &McpAbsolutePath) -> Result<Option<McpRepoKey>, CliError> {
2107        if !path.as_path().starts_with(&self.0) {
2108            return Ok(None);
2109        }
2110        normalize_repo_path(&self.0, path.as_path())
2111            .map(McpRepoKey)
2112            .map(Some)
2113            .map_err(ProjectAtlasMcpServer::selected_project_path_error)
2114    }
2115}
2116
2117/// Role-typed indexed project root discovered by nearest-project routing.
2118#[derive(Debug, Clone)]
2119struct McpIndexedRoot {
2120    /// Canonical indexed project root.
2121    root: PathBuf,
2122    /// Durable index path that records the same root.
2123    db_path: PathBuf,
2124}
2125
2126/// Canonical absolute path supplied to an MCP path-bearing tool.
2127#[derive(Debug, Clone)]
2128struct McpAbsolutePath(PathBuf);
2129
2130impl McpAbsolutePath {
2131    /// Canonicalize an absolute path through its nearest existing ancestor.
2132    ///
2133    /// File selectors may refer to an indexed path that was deleted offline.
2134    /// Canonicalizing the existing ancestor preserves symlink/root-escape
2135    /// checks without requiring the selected leaf to still exist.
2136    fn canonicalize(path: &Path) -> Result<Self, CliError> {
2137        let mut existing = path;
2138        let mut missing_suffix = Vec::new();
2139        while !existing.exists() {
2140            let file_name = existing
2141                .file_name()
2142                .ok_or_else(|| missing_ancestor_error(path))?;
2143            missing_suffix.push(PathBuf::from(file_name));
2144            existing = existing
2145                .parent()
2146                .ok_or_else(|| missing_ancestor_error(path))?;
2147        }
2148        let mut canonical = canonical_project_root(existing)?;
2149        for component in missing_suffix.into_iter().rev() {
2150            canonical.push(component);
2151        }
2152        Ok(Self(canonical))
2153    }
2154
2155    /// Borrow the canonical path.
2156    fn as_path(&self) -> &Path {
2157        &self.0
2158    }
2159
2160    /// Return the directory where nearest-indexed-root discovery should begin.
2161    fn nearest_search_start(&self) -> &Path {
2162        if self.0.is_dir() {
2163            &self.0
2164        } else {
2165            self.0.parent().unwrap_or(self.as_path())
2166        }
2167    }
2168}
2169
2170/// Build the adapter error for an absolute selector without an inspectable ancestor.
2171fn missing_ancestor_error(path: &Path) -> CliError {
2172    CliError::InvalidInput(format!(
2173        "absolute path '{}' has no existing ancestor",
2174        path.display()
2175    ))
2176}
2177
2178/// Repository-relative path key derived from a typed root/path conversion.
2179#[derive(Debug, Clone)]
2180struct McpRepoKey(String);
2181
2182impl McpRepoKey {
2183    /// Consume the wrapper into the repository key string.
2184    fn into_string(self) -> String {
2185        self.0
2186    }
2187}
2188
2189/// MCP path resolution result with selected-project audit state.
2190#[derive(Debug, Clone)]
2191struct McpResolvedRepoPath {
2192    /// Project state selected for the request.
2193    state: McpProjectState,
2194    /// Repository-relative path key inside the selected project.
2195    key: String,
2196    /// Whether nearest-project routing changed the selected root/DB.
2197    routed_project: bool,
2198}
2199
2200/// Agent-facing error payload for failed MCP calls.
2201#[derive(Debug, Serialize)]
2202struct McpErrorResponse {
2203    /// Structured MCP error details.
2204    error: McpErrorPayload,
2205}
2206
2207/// Stable serialized schema for MCP error details.
2208#[derive(Debug, Serialize)]
2209struct McpErrorPayload {
2210    /// Stable machine-readable error kind.
2211    kind: AgentErrorKind,
2212    /// Human-readable error and recovery guidance.
2213    message: String,
2214    /// Bounded local-source mismatch details when refresh is required.
2215    #[serde(skip_serializing_if = "Option::is_none")]
2216    refresh_required: Option<IndexRefreshRequired>,
2217    /// Exact selected-root initialization handoff.
2218    #[serde(skip_serializing_if = "Option::is_none")]
2219    init_required: Option<IndexInitRequired>,
2220    /// Bare/common Git root selection diagnostic.
2221    #[serde(skip_serializing_if = "Option::is_none")]
2222    worktree_required: Option<ProjectWorktreeRequired>,
2223    /// Bounded source/policy diagnostic when verification cannot complete.
2224    #[serde(skip_serializing_if = "Option::is_none")]
2225    verification_incomplete: Option<IndexVerificationIncomplete>,
2226    /// Project/index identity mismatch details.
2227    #[serde(skip_serializing_if = "Option::is_none")]
2228    project_mismatch: Option<IndexProjectMismatch>,
2229    /// Content-free database placement details for a rejected `SQLite` profile.
2230    #[serde(skip_serializing_if = "Option::is_none")]
2231    database_filesystem: Option<DatabaseFilesystemErrorPayload>,
2232    /// Content-free incompatible schema details.
2233    #[serde(skip_serializing_if = "Option::is_none")]
2234    schema_version_mismatch: Option<SchemaVersionMismatchPayload>,
2235    /// Content-free supported migration handoff.
2236    #[serde(skip_serializing_if = "Option::is_none")]
2237    schema_migration_required: Option<SchemaMigrationRequiredPayload>,
2238    /// Optional retrieval capability state and recovery guidance.
2239    #[serde(skip_serializing_if = "Option::is_none")]
2240    search_capability: Option<crate::SearchCapabilityErrorPayload>,
2241    /// Reusable recovery call when refresh is required.
2242    #[serde(skip_serializing_if = "Option::is_none")]
2243    next: Option<McpNextCall>,
2244}
2245
2246/// Direct MCP recovery selector.
2247#[derive(Debug, Serialize)]
2248struct McpNextCall {
2249    /// Existing MCP tool that safely recovers the selected project.
2250    tool: &'static str,
2251    /// Canonical project root for legacy per-call isolation.
2252    #[serde(skip_serializing_if = "Option::is_none")]
2253    project_path: Option<String>,
2254    /// Registered alias preserved by worktree-routed recovery.
2255    #[serde(skip_serializing_if = "Option::is_none")]
2256    worktree: Option<String>,
2257}
2258
2259/// Agent-facing payload for the selected MCP project.
2260#[derive(Debug, Serialize)]
2261struct McpProjectStateResponse {
2262    /// Selected project details.
2263    project: McpProjectStatePayload,
2264}
2265
2266/// Stable serialized schema for the selected MCP project.
2267#[derive(Debug, Serialize)]
2268struct McpProjectStatePayload {
2269    /// Registered alias used to capture this project state.
2270    #[serde(skip_serializing_if = "Option::is_none")]
2271    worktree: Option<String>,
2272    /// Durable control-catalog registration identity.
2273    #[serde(skip_serializing_if = "Option::is_none")]
2274    registration_id: Option<i64>,
2275    /// Canonical repository root.
2276    root: Option<String>,
2277    /// Selected durable `SQLite` index path.
2278    db: Option<String>,
2279    /// Selected configuration path when present.
2280    config: Option<String>,
2281    /// Active selection status.
2282    status: McpProjectStatus,
2283}
2284
2285/// Status values for MCP project selection responses.
2286#[derive(Debug, Serialize)]
2287#[serde(rename_all = "snake_case")]
2288enum McpProjectStatus {
2289    /// The selected project is active for defaulted MCP calls.
2290    Active,
2291}
2292
2293/// Agent-facing payload for an MCP purpose update.
2294#[derive(Debug, Serialize)]
2295struct McpPurposeSetResponse {
2296    /// Purpose update result details.
2297    purpose_set: McpPurposeSetPayload,
2298}
2299
2300/// Stable serialized schema for an MCP purpose update.
2301#[derive(Debug, Serialize)]
2302struct McpPurposeSetPayload {
2303    /// Indexed repository-relative path whose purpose was updated.
2304    path: String,
2305    /// Registry-owned content role when the selected path is a file.
2306    #[serde(skip_serializing_if = "Option::is_none")]
2307    classification: Option<ContentClassification>,
2308    /// Durable purpose status after the update.
2309    status: PurposeStatus,
2310    /// Source of the durable purpose after the update.
2311    source: PurposeSource,
2312    /// Whether the purpose has been agent-reviewed.
2313    agent_reviewed: bool,
2314}
2315
2316/// MCP-session capability/settings payload.
2317#[derive(Debug, Serialize)]
2318struct McpSessionCapabilities {
2319    /// Runtime identity and compiled tool surface.
2320    runtime: RuntimeInfoReport,
2321    /// Selected project identity and index status.
2322    selected_project: McpSelectedProjectCapability,
2323    /// Route-affecting startup policy.
2324    startup_policy: McpStartupPolicy,
2325    /// Absolute-path routing scope.
2326    path_scope: McpPathScope,
2327    /// Scan behavior visible to harnesses.
2328    scan_policy: McpScanPolicy,
2329    /// Closed classified-content navigation surface.
2330    classified_navigation: SettingsClassifiedNavigationReport,
2331    /// Token telemetry write mode for this process.
2332    telemetry: McpTelemetryPolicy,
2333    /// Privacy guarantees for this payload.
2334    privacy: McpPrivacyPolicy,
2335}
2336
2337/// Selected project identity inside capability/settings payloads.
2338#[derive(Debug, Serialize)]
2339struct McpSelectedProjectCapability {
2340    /// Registered alias used to capture this project state.
2341    #[serde(skip_serializing_if = "Option::is_none")]
2342    worktree: Option<String>,
2343    /// Durable control-catalog registration identity.
2344    #[serde(skip_serializing_if = "Option::is_none")]
2345    registration_id: Option<i64>,
2346    /// Canonical repository root.
2347    root: Option<String>,
2348    /// Selected durable `SQLite` index path.
2349    db: Option<String>,
2350    /// Selected configuration path when present.
2351    config: Option<String>,
2352    /// Whether the selected durable index exists.
2353    index_status: McpIndexStatus,
2354}
2355
2356/// Startup policy fields for MCP sessions.
2357#[derive(Debug, Serialize)]
2358struct McpStartupPolicy {
2359    /// Whether nearest indexed project routing is enabled by default.
2360    nearest_project: McpPolicyState,
2361}
2362
2363/// Scan policy fields relevant before source reads.
2364#[derive(Debug, Serialize)]
2365struct McpScanPolicy {
2366    /// Settings calls and session briefs never scan implicitly.
2367    implicit_scan: McpPolicyState,
2368    /// Maximum `UTF-8` file size persisted into `SQLite` text search.
2369    text_index_max_bytes: u64,
2370}
2371
2372/// Telemetry write policy for this MCP process.
2373#[derive(Debug, Serialize)]
2374struct McpTelemetryPolicy {
2375    /// Whether token telemetry writes are enabled.
2376    mode: McpPolicyState,
2377}
2378
2379/// Privacy contract for capability/settings payloads.
2380#[derive(Debug, Serialize)]
2381struct McpPrivacyPolicy {
2382    /// No arbitrary process environment dump is included.
2383    environment_dump: bool,
2384    /// No secret or token values are included.
2385    secret_values: bool,
2386    /// Host paths are limited to `ProjectAtlas` runtime/root/DB/config paths.
2387    projectatlas_paths_only: bool,
2388}
2389
2390/// Two-state policy enum serialized for MCP contracts.
2391#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
2392#[serde(rename_all = "snake_case")]
2393enum McpPolicyState {
2394    /// Policy is enabled.
2395    Enabled,
2396    /// Policy is disabled.
2397    Disabled,
2398}
2399
2400/// Selected index availability.
2401#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
2402#[serde(rename_all = "snake_case")]
2403enum McpIndexStatus {
2404    /// The selected index file exists.
2405    Available,
2406    /// The selected index file is missing.
2407    Missing,
2408}
2409
2410/// Absolute path routing scope.
2411#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
2412#[serde(rename_all = "snake_case")]
2413enum McpPathScope {
2414    /// Calls stay within the selected project.
2415    SelectedProject,
2416    /// Absolute paths may route to the nearest indexed project.
2417    NearestIndexedProject,
2418}
2419
2420/// Explicit compact file-summary payload with actionable facts and redundant state removed.
2421#[derive(Debug, Serialize)]
2422struct McpFileSummaryPayload<'a> {
2423    /// Explicit compact file intelligence.
2424    file_summary: McpFileSummary<'a>,
2425}
2426
2427/// Compact projection used when an agent follows a default startup recommendation.
2428#[derive(Debug, Serialize)]
2429struct McpFileSummary<'a> {
2430    /// Repository-relative file path.
2431    file_path: &'a str,
2432    /// Persisted content role for the selected file.
2433    classification: projectatlas_core::language::ContentClassification,
2434    /// Detected language or file family.
2435    language: &'a str,
2436    /// Source line count.
2437    line_count: usize,
2438    /// Non-default source state when live source was unavailable.
2439    #[serde(skip_serializing_if = "Option::is_none")]
2440    source_status: Option<&'a str>,
2441    /// Source read diagnostic when one exists.
2442    #[serde(skip_serializing_if = "Option::is_none")]
2443    source_error: Option<&'a str>,
2444    /// Parser family that produced the summary.
2445    parser_kind: &'a str,
2446    /// Summary quality state agents must inspect before trusting generated prose.
2447    summary_status: &'a str,
2448    /// Durable file responsibility when present.
2449    #[serde(skip_serializing_if = "Option::is_none")]
2450    file_purpose: Option<&'a str>,
2451    /// Purpose status for suggestions or other unreviewed rows.
2452    #[serde(skip_serializing_if = "Option::is_none")]
2453    file_purpose_status: Option<&'a str>,
2454    /// Purpose source for suggestions or other unreviewed rows.
2455    #[serde(skip_serializing_if = "Option::is_none")]
2456    file_purpose_source: Option<&'a str>,
2457    /// Whether an agent approved the retained responsibility.
2458    #[serde(skip_serializing_if = "is_false")]
2459    file_purpose_agent_reviewed: bool,
2460    /// Current deterministic content summary.
2461    content_summary: &'a str,
2462    /// Package or module name when present.
2463    #[serde(skip_serializing_if = "Option::is_none")]
2464    package: Option<&'a str>,
2465    /// File documentation when present.
2466    #[serde(skip_serializing_if = "Option::is_none")]
2467    docstring: Option<&'a str>,
2468    /// Whether the default bounded repeated sections omitted rows.
2469    #[serde(skip_serializing_if = "is_false")]
2470    truncated: bool,
2471    /// Indexed functions when present.
2472    #[serde(skip_serializing_if = "Option::is_none")]
2473    functions: Option<Vec<McpFileSymbolSummary<'a>>>,
2474    /// Indexed methods when present.
2475    #[serde(skip_serializing_if = "Option::is_none")]
2476    methods: Option<Vec<McpFileSymbolSummary<'a>>>,
2477    /// Indexed classes when present.
2478    #[serde(skip_serializing_if = "Option::is_none")]
2479    classes: Option<Vec<McpFileSymbolSummary<'a>>>,
2480    /// Indexed type declarations when present.
2481    #[serde(skip_serializing_if = "Option::is_none")]
2482    types: Option<Vec<McpFileSymbolSummary<'a>>>,
2483    /// Imports when present.
2484    #[serde(skip_serializing_if = "Option::is_none")]
2485    imports: Option<&'a [String]>,
2486    /// Manifest dependencies when present.
2487    #[serde(skip_serializing_if = "Option::is_none")]
2488    dependencies: Option<&'a [String]>,
2489    /// Exported declarations when present.
2490    #[serde(skip_serializing_if = "Option::is_none")]
2491    exports: Option<&'a [String]>,
2492    /// Call rows when present.
2493    #[serde(skip_serializing_if = "Option::is_none")]
2494    calls: Option<&'a [FileCallSummary]>,
2495    /// Coverage details only when they require agent attention.
2496    #[serde(skip_serializing_if = "Option::is_none")]
2497    coverage: Option<McpCompactCoverageDigest<'a>>,
2498}
2499
2500/// Compact symbol row that omits empty legacy fields.
2501#[derive(Debug, Serialize)]
2502struct McpFileSymbolSummary<'a> {
2503    /// Symbol name.
2504    name: &'a str,
2505    /// Symbol kind.
2506    kind: &'a str,
2507    /// One-based start line.
2508    line: usize,
2509    /// One-based end line.
2510    end_line: usize,
2511    /// Declaration signature.
2512    signature: &'a str,
2513    /// Whether the declaration is externally visible.
2514    exported: bool,
2515    /// Extracted documentation when present.
2516    #[serde(skip_serializing_if = "Option::is_none")]
2517    documentation: Option<&'a str>,
2518    /// Parent declaration when present.
2519    #[serde(skip_serializing_if = "Option::is_none")]
2520    parent: Option<&'a str>,
2521    /// Indexed callers when present.
2522    #[serde(skip_serializing_if = "Option::is_none")]
2523    called_by: Option<&'a [String]>,
2524}
2525
2526impl<'a> From<&'a FileSymbolSummary> for McpFileSymbolSummary<'a> {
2527    fn from(symbol: &'a FileSymbolSummary) -> Self {
2528        Self {
2529            name: &symbol.name,
2530            kind: &symbol.kind,
2531            line: symbol.line,
2532            end_line: symbol.end_line,
2533            signature: &symbol.signature,
2534            exported: symbol.exported,
2535            documentation: nonempty_str(&symbol.documentation),
2536            parent: nonempty_str(&symbol.parent),
2537            called_by: nonempty_slice(&symbol.called_by),
2538        }
2539    }
2540}
2541
2542/// Project nonempty symbol rows into their compact MCP representation.
2543fn compact_file_symbols(symbols: &[FileSymbolSummary]) -> Option<Vec<McpFileSymbolSummary<'_>>> {
2544    (!symbols.is_empty()).then(|| symbols.iter().map(McpFileSymbolSummary::from).collect())
2545}
2546
2547impl<'a> From<&'a FileSummaryReport> for McpFileSummary<'a> {
2548    fn from(report: &'a FileSummaryReport) -> Self {
2549        let reviewed_purpose = report.file_purpose_agent_reviewed;
2550        let coverage_requires_attention = !report.coverage.available
2551            || report.coverage.trust != CoverageTrustState::Trusted
2552            || report.coverage.omitted > 0
2553            || report.coverage.truncated;
2554        Self {
2555            file_path: &report.file_path,
2556            classification: report.classification,
2557            language: &report.language,
2558            line_count: report.line_count,
2559            source_status: (report.source_status != MCP_FILE_SOURCE_STATUS_LIVE)
2560                .then_some(report.source_status.as_str()),
2561            source_error: nonempty_str(&report.source_error),
2562            parser_kind: &report.parser_kind,
2563            summary_status: &report.summary_status,
2564            file_purpose: nonempty_str(&report.file_purpose),
2565            file_purpose_status: (!reviewed_purpose).then_some(report.file_purpose_status.as_str()),
2566            file_purpose_source: (!reviewed_purpose).then_some(report.file_purpose_source.as_str()),
2567            file_purpose_agent_reviewed: reviewed_purpose,
2568            content_summary: &report.content_summary,
2569            package: nonempty_str(&report.package),
2570            docstring: nonempty_str(&report.docstring),
2571            truncated: report.truncated,
2572            functions: compact_file_symbols(&report.functions),
2573            methods: compact_file_symbols(&report.methods),
2574            classes: compact_file_symbols(&report.classes),
2575            types: compact_file_symbols(&report.types),
2576            imports: nonempty_slice(&report.imports),
2577            dependencies: nonempty_slice(&report.dependencies),
2578            exports: nonempty_slice(&report.exports),
2579            calls: nonempty_slice(&report.calls),
2580            coverage: coverage_requires_attention
2581                .then(|| McpCompactCoverageDigest::from(&report.coverage)),
2582        }
2583    }
2584}
2585
2586/// Adapter-local sparse coverage counts for the explicit compact summary.
2587#[derive(Debug, Serialize)]
2588struct McpCompactCoverageStateCounts {
2589    /// Complete coverage rows.
2590    #[serde(skip_serializing_if = "is_zero_u32")]
2591    complete: u32,
2592    /// Complete extraction scopes containing no supported candidates.
2593    #[serde(skip_serializing_if = "is_zero_u32")]
2594    no_candidates: u32,
2595    /// Partial coverage rows.
2596    #[serde(skip_serializing_if = "is_zero_u32")]
2597    partial: u32,
2598    /// Failed coverage rows.
2599    #[serde(skip_serializing_if = "is_zero_u32")]
2600    failed: u32,
2601    /// Intentionally ignored coverage rows.
2602    #[serde(skip_serializing_if = "is_zero_u32")]
2603    ignored: u32,
2604    /// Oversized coverage rows.
2605    #[serde(skip_serializing_if = "is_zero_u32")]
2606    oversized: u32,
2607    /// Quarantined coverage rows.
2608    #[serde(skip_serializing_if = "is_zero_u32")]
2609    quarantined: u32,
2610    /// Stale coverage rows.
2611    #[serde(skip_serializing_if = "is_zero_u32")]
2612    stale: u32,
2613}
2614
2615/// Adapter-local compact coverage projection that leaves shared serialization unchanged.
2616#[derive(Debug, Serialize)]
2617struct McpCompactCoverageDigest<'a> {
2618    /// False when no current coverage rows exist.
2619    #[serde(skip_serializing_if = "is_true")]
2620    available: bool,
2621    /// Active generation shared by retained rows.
2622    active_generation: &'a IndexGeneration,
2623    /// Source parser pass recorded for the file.
2624    #[serde(skip_serializing_if = "Option::is_none")]
2625    parser: Option<&'a ParserKind>,
2626    /// Fact provider pass recorded for the file.
2627    #[serde(skip_serializing_if = "Option::is_none")]
2628    provider: Option<&'a ParserKind>,
2629    /// Sparse per-state counts.
2630    states: McpCompactCoverageStateCounts,
2631    /// Total items declared by retained rows.
2632    total: u64,
2633    /// Covered items declared by retained rows.
2634    covered: u64,
2635    /// Omitted or untrusted items declared by retained rows.
2636    #[serde(skip_serializing_if = "is_zero_u64")]
2637    omitted: u64,
2638    /// Number of retained relation-family rows.
2639    #[serde(skip_serializing_if = "is_zero_u32")]
2640    relation_rows: u32,
2641    /// Whether digest bounds omitted additional selected-file rows.
2642    #[serde(skip_serializing_if = "is_false")]
2643    truncated: bool,
2644    /// Conservative trust state across retained rows.
2645    trust: &'a CoverageTrustState,
2646    /// Existing opt-in health call for deeper coverage discovery.
2647    next_call: &'a NavigationNextCall,
2648}
2649
2650impl<'a> From<&'a CoverageDigest> for McpCompactCoverageDigest<'a> {
2651    fn from(coverage: &'a CoverageDigest) -> Self {
2652        Self {
2653            available: coverage.available,
2654            active_generation: &coverage.active_generation,
2655            parser: coverage.parser.as_ref(),
2656            provider: coverage.provider.as_ref(),
2657            states: McpCompactCoverageStateCounts {
2658                complete: coverage.states.complete,
2659                no_candidates: coverage.states.no_candidates,
2660                partial: coverage.states.partial,
2661                failed: coverage.states.failed,
2662                ignored: coverage.states.ignored,
2663                oversized: coverage.states.oversized,
2664                quarantined: coverage.states.quarantined,
2665                stale: coverage.states.stale,
2666            },
2667            total: coverage.total,
2668            covered: coverage.covered,
2669            omitted: coverage.omitted,
2670            relation_rows: coverage.relation_rows,
2671            truncated: coverage.truncated,
2672            trust: &coverage.trust,
2673            next_call: &coverage.next_call,
2674        }
2675    }
2676}
2677
2678/// Adapter-local compact detailed-relation node without stable-key duplication.
2679#[derive(Debug, Serialize)]
2680struct McpCompactDetailedRelationNode<'a> {
2681    /// Exact selector accepted by later relation, summary, and slice calls.
2682    selector: &'a EntitySelector,
2683    /// Accepted, unavailable, or non-local purpose state.
2684    purpose: &'a RelationPurpose,
2685    /// Authoritative coverage rows for the selected local owner.
2686    #[serde(skip_serializing_if = "Option::is_none")]
2687    coverage: Option<&'a [CoverageRecord]>,
2688}
2689
2690impl<'a> From<&'a DetailedRelationNode> for McpCompactDetailedRelationNode<'a> {
2691    fn from(node: &'a DetailedRelationNode) -> Self {
2692        Self {
2693            selector: node.entity.selector(),
2694            purpose: &node.purpose,
2695            coverage: nonempty_slice(&node.coverage),
2696        }
2697    }
2698}
2699
2700/// Resolution facts retained by the compact relation projection.
2701#[derive(Debug, Serialize)]
2702#[serde(tag = "status", rename_all = "snake_case")]
2703enum McpCompactRelationResolution<'a> {
2704    /// Exactly one reusable local target was resolved.
2705    Resolved {
2706        /// Exact selector accepted by later calls.
2707        selector: &'a ReusableTargetSelector,
2708        /// Complete generation containing the target.
2709        generation: IndexGeneration,
2710    },
2711    /// More than one valid target remains.
2712    Ambiguous {
2713        /// Original normalized reference.
2714        reference: &'a GraphIdentityText,
2715        /// Number of retained candidates before limits.
2716        candidates: u32,
2717    },
2718    /// No supported static target was found.
2719    Unresolved {
2720        /// Original normalized reference.
2721        reference: &'a GraphIdentityText,
2722    },
2723    /// The target is intentionally outside the selected project.
2724    External {
2725        /// Typed external identity.
2726        external: &'a ExternalSelector,
2727        /// Complete generation containing the external record.
2728        generation: IndexGeneration,
2729    },
2730}
2731
2732impl<'a> From<&'a RelationResolution> for McpCompactRelationResolution<'a> {
2733    fn from(resolution: &'a RelationResolution) -> Self {
2734        match resolution {
2735            RelationResolution::Resolved {
2736                selector,
2737                generation,
2738                ..
2739            } => Self::Resolved {
2740                selector,
2741                generation: *generation,
2742            },
2743            RelationResolution::Ambiguous {
2744                reference,
2745                candidates,
2746            } => Self::Ambiguous {
2747                reference,
2748                candidates: candidates.get(),
2749            },
2750            RelationResolution::Unresolved { reference } => Self::Unresolved { reference },
2751            RelationResolution::External {
2752                external,
2753                generation,
2754                ..
2755            } => Self::External {
2756                external,
2757                generation: *generation,
2758            },
2759        }
2760    }
2761}
2762
2763/// Compact relation facts required for trust and direct navigation.
2764#[derive(Debug, Serialize)]
2765struct McpCompactLogicalRelation<'a> {
2766    /// Typed legacy or extended relation family.
2767    kind: GraphRelationKind,
2768    /// Resolution state and reusable target when local.
2769    resolution: McpCompactRelationResolution<'a>,
2770    /// Coarse trust class.
2771    confidence: ConfidenceClass,
2772    /// Producer completeness for this relation scope.
2773    completeness: Completeness,
2774    /// Complete generation containing the relation.
2775    generation: IndexGeneration,
2776}
2777
2778/// One compact detailed-relation row.
2779#[derive(Debug, Serialize)]
2780struct McpCompactDetailedRelationRow<'a> {
2781    /// One-based traversal depth.
2782    depth: u32,
2783    /// Direction relative to the selected frontier.
2784    direction: RelationDirection,
2785    /// Typed relation facts without stable-key duplication.
2786    relation: McpCompactLogicalRelation<'a>,
2787    /// Closed reason retained only for an unresolved canonical document relation.
2788    #[serde(skip_serializing_if = "Option::is_none")]
2789    document_unresolved_reason: Option<DocumentTargetUnresolvedReason>,
2790    /// Exact source selector, purpose, and coverage.
2791    source: McpCompactDetailedRelationNode<'a>,
2792    /// Retained local or external target when one exists.
2793    #[serde(skip_serializing_if = "Option::is_none")]
2794    target: Option<McpCompactDetailedRelationNode<'a>>,
2795    /// Purpose disposition when no target node exists.
2796    #[serde(skip_serializing_if = "Option::is_none")]
2797    target_purpose: Option<&'a RelationPurpose>,
2798    /// Exact selectors for a multi-hop path; direct source/target rows omit this duplicate.
2799    #[serde(skip_serializing_if = "Option::is_none")]
2800    path: Option<Vec<&'a EntitySelector>>,
2801    /// Exact supporting occurrences when requested.
2802    #[serde(skip_serializing_if = "Option::is_none")]
2803    occurrences: Option<Vec<McpCompactRelationOccurrence<'a>>>,
2804    /// Whether the per-relation occurrence ceiling omitted rows.
2805    #[serde(skip_serializing_if = "is_false")]
2806    occurrences_truncated: bool,
2807    /// Existing exact call that consumes the selected local endpoint.
2808    #[serde(skip_serializing_if = "Option::is_none")]
2809    next_call: Option<&'a RelationNextCall>,
2810}
2811
2812/// Compact source occurrence without another copy of the owning relation key.
2813#[derive(Debug, Serialize)]
2814struct McpCompactRelationOccurrence<'a> {
2815    /// Repository-local file containing the evidence.
2816    file: &'a RepositoryFilePath,
2817    /// Exact supporting source range.
2818    span: SourceSpan,
2819    /// Complete generation containing the occurrence.
2820    generation: IndexGeneration,
2821}
2822
2823impl<'a> From<&'a RelationOccurrence> for McpCompactRelationOccurrence<'a> {
2824    fn from(occurrence: &'a RelationOccurrence) -> Self {
2825        Self {
2826            file: occurrence.file(),
2827            span: occurrence.span(),
2828            generation: occurrence.generation(),
2829        }
2830    }
2831}
2832
2833impl<'a> From<&'a DetailedRelationRow> for McpCompactDetailedRelationRow<'a> {
2834    fn from(row: &'a DetailedRelationRow) -> Self {
2835        Self {
2836            depth: row.depth,
2837            direction: row.direction,
2838            relation: McpCompactLogicalRelation {
2839                kind: row.relation.kind(),
2840                resolution: McpCompactRelationResolution::from(row.relation.resolution()),
2841                confidence: row.relation.confidence(),
2842                completeness: row.relation.completeness(),
2843                generation: row.relation.generation(),
2844            },
2845            document_unresolved_reason: row.document_unresolved_reason,
2846            source: McpCompactDetailedRelationNode::from(&row.source),
2847            target: row
2848                .target
2849                .as_ref()
2850                .map(McpCompactDetailedRelationNode::from),
2851            target_purpose: row.target.is_none().then_some(&row.target_purpose),
2852            path: (row.path.len() > 2)
2853                .then(|| row.path.iter().map(|node| node.entity.selector()).collect()),
2854            occurrences: (!row.occurrences.is_empty()).then(|| {
2855                row.occurrences
2856                    .iter()
2857                    .map(McpCompactRelationOccurrence::from)
2858                    .collect()
2859            }),
2860            occurrences_truncated: row.occurrences_truncated,
2861            next_call: row.next_call.as_ref(),
2862        }
2863    }
2864}
2865
2866/// Opt-in compact projection of one detailed relation page.
2867#[derive(Debug, Serialize)]
2868struct McpCompactDetailedRelationReport<'a> {
2869    /// Exact selected anchor.
2870    anchor: McpCompactDetailedRelationNode<'a>,
2871    /// Complete graph generation captured by the page.
2872    generation: IndexGeneration,
2873    /// Accepted authored-purpose revision captured by the page.
2874    authored_purpose_revision: u64,
2875    /// Direction followed from the anchor.
2876    direction: RelationDirection,
2877    /// Number of retained relation steps.
2878    returned: u32,
2879    /// Number of cyclic or duplicate-node paths pruned.
2880    #[serde(skip_serializing_if = "is_zero_u64")]
2881    pruned_paths: u64,
2882    /// Whether a declared boundary stopped traversal.
2883    #[serde(skip_serializing_if = "is_false")]
2884    truncated: bool,
2885    /// Directly reusable continuation call with the exact original query and budget.
2886    #[serde(skip_serializing_if = "Option::is_none")]
2887    next_call: Option<McpCompactRelationContinuationCall<'a>>,
2888    /// Exact, lower-bound, or unknown cardinality.
2889    total: &'a RelationTotalState,
2890    /// Stable hard limits reached while constructing the response.
2891    #[serde(skip_serializing_if = "Option::is_none")]
2892    reached_limits: Option<&'a [GraphLimitKind]>,
2893    /// Aggregate page and retained-state work.
2894    work: &'a DetailedRelationWork,
2895    /// Ranked node-simple relation steps.
2896    rows: Vec<McpCompactDetailedRelationRow<'a>>,
2897}
2898
2899/// Existing MCP relation call that resumes one exact compact page.
2900#[derive(Debug, Serialize)]
2901struct McpCompactRelationContinuationCall<'a> {
2902    /// Existing MCP tool that owns relation continuation.
2903    tool: &'static str,
2904    /// Exact original request plus its generation-bound cursor.
2905    arguments: McpCompactRelationContinuationArguments<'a>,
2906}
2907
2908/// Exact result-defining arguments required to resume a detailed relation page.
2909#[derive(Debug, Serialize)]
2910struct McpCompactRelationContinuationArguments<'a> {
2911    /// Explicit project root when the original request supplied one.
2912    #[serde(skip_serializing_if = "Option::is_none")]
2913    project_path: Option<&'a str>,
2914    /// Registered primary worktree when the original request supplied one.
2915    #[serde(skip_serializing_if = "Option::is_none")]
2916    worktree: Option<&'a str>,
2917    /// Selected repository-relative anchor file.
2918    file: &'a str,
2919    /// Original nearest-project policy when explicitly supplied.
2920    #[serde(skip_serializing_if = "Option::is_none")]
2921    nearest_project: Option<bool>,
2922    /// Original ordered federated roots when supplied.
2923    #[serde(skip_serializing_if = "Option::is_none")]
2924    roots: Option<&'a [String]>,
2925    /// Original ordered registered worktree aliases when supplied.
2926    #[serde(skip_serializing_if = "Option::is_none")]
2927    worktrees: Option<&'a [String]>,
2928    /// Detailed relation view.
2929    view: &'static str,
2930    /// Preserve the compact response projection.
2931    compact: bool,
2932    /// Generation-, purpose-, query-, order-, and budget-bound cursor.
2933    cursor: &'a str,
2934    /// Exact symbol name when the anchor is a declaration.
2935    #[serde(skip_serializing_if = "Option::is_none")]
2936    symbol: Option<&'a str>,
2937    /// Exact nonempty symbol parent when supplied.
2938    #[serde(skip_serializing_if = "Option::is_none")]
2939    symbol_parent: Option<&'a str>,
2940    /// Exact nonempty symbol kind when supplied.
2941    #[serde(skip_serializing_if = "Option::is_none")]
2942    symbol_kind: Option<&'a str>,
2943    /// Exact nonempty symbol signature when supplied.
2944    #[serde(skip_serializing_if = "Option::is_none")]
2945    symbol_signature: Option<&'a str>,
2946    /// Original traversal direction when explicitly supplied.
2947    #[serde(skip_serializing_if = "Option::is_none")]
2948    direction: Option<&'a str>,
2949    /// Original relation filter when explicitly supplied.
2950    #[serde(skip_serializing_if = "Option::is_none")]
2951    relation: Option<&'a str>,
2952    /// Original confidence floor when explicitly supplied.
2953    #[serde(skip_serializing_if = "Option::is_none")]
2954    minimum_confidence: Option<&'a str>,
2955    /// Original resolution filter when explicitly supplied.
2956    #[serde(skip_serializing_if = "Option::is_none")]
2957    resolution: Option<&'a str>,
2958    /// Original traversal depth when explicitly supplied.
2959    #[serde(skip_serializing_if = "Option::is_none")]
2960    depth: Option<u32>,
2961    /// Original occurrence-retention choice when explicitly supplied.
2962    #[serde(skip_serializing_if = "Option::is_none")]
2963    include_occurrences: Option<bool>,
2964    /// Original returned-row limit when explicitly supplied.
2965    #[serde(skip_serializing_if = "Option::is_none")]
2966    limit: Option<usize>,
2967    /// Original per-relation occurrence limit when explicitly supplied.
2968    #[serde(skip_serializing_if = "Option::is_none")]
2969    occurrence_limit: Option<u32>,
2970    /// Original edge budget when explicitly supplied.
2971    #[serde(skip_serializing_if = "Option::is_none")]
2972    edge_limit: Option<u32>,
2973    /// Original node budget when explicitly supplied.
2974    #[serde(skip_serializing_if = "Option::is_none")]
2975    node_limit: Option<u32>,
2976    /// Original visited-node budget when explicitly supplied.
2977    #[serde(skip_serializing_if = "Option::is_none")]
2978    visited_limit: Option<u32>,
2979    /// Original aggregate occurrence budget when explicitly supplied.
2980    #[serde(skip_serializing_if = "Option::is_none")]
2981    occurrence_total_limit: Option<u32>,
2982    /// Original intermediate-byte budget when explicitly supplied.
2983    #[serde(skip_serializing_if = "Option::is_none")]
2984    intermediate_bytes: Option<u64>,
2985    /// Original service deadline when explicitly supplied.
2986    #[serde(skip_serializing_if = "Option::is_none")]
2987    deadline_ms: Option<u64>,
2988    /// Original rendered-output budget when explicitly supplied.
2989    #[serde(skip_serializing_if = "Option::is_none")]
2990    output_bytes: Option<u32>,
2991}
2992
2993impl<'a> McpCompactDetailedRelationReport<'a> {
2994    /// Project one detailed page and its directly reusable continuation.
2995    fn new(
2996        report: &'a DetailedRelationReport,
2997        file: &'a str,
2998        params: &'a AtlasSymbolRelationsParams,
2999    ) -> Self {
3000        Self {
3001            anchor: McpCompactDetailedRelationNode::from(&report.anchor),
3002            generation: report.generation,
3003            authored_purpose_revision: report.authored_purpose_revision,
3004            direction: report.direction,
3005            returned: report.returned,
3006            pruned_paths: report.pruned_paths,
3007            truncated: report.truncated,
3008            next_call: report.continuation.as_deref().map(|cursor| {
3009                McpCompactRelationContinuationCall {
3010                    tool: MCP_TOOL_ATLAS_SYMBOL_RELATIONS,
3011                    arguments: McpCompactRelationContinuationArguments {
3012                        project_path: params.project_path.as_deref(),
3013                        worktree: params.worktree.as_deref(),
3014                        file,
3015                        nearest_project: params.nearest_project,
3016                        roots: params.roots.as_deref(),
3017                        worktrees: params.worktrees.as_deref(),
3018                        view: MCP_SYMBOL_RELATION_VIEW_DETAILED,
3019                        compact: true,
3020                        cursor,
3021                        symbol: params.symbol.as_deref(),
3022                        symbol_parent: params.symbol_parent.as_deref().and_then(nonempty_str),
3023                        symbol_kind: params.symbol_kind.as_deref().and_then(nonempty_str),
3024                        symbol_signature: params.symbol_signature.as_deref().and_then(nonempty_str),
3025                        direction: params.direction.as_deref(),
3026                        relation: params.relation.as_deref(),
3027                        minimum_confidence: params.minimum_confidence.as_deref(),
3028                        resolution: params.resolution.as_deref(),
3029                        depth: params.depth,
3030                        include_occurrences: params.include_occurrences,
3031                        limit: params.limit,
3032                        occurrence_limit: params.occurrence_limit,
3033                        edge_limit: params.edge_limit,
3034                        node_limit: params.node_limit,
3035                        visited_limit: params.visited_limit,
3036                        occurrence_total_limit: params.occurrence_total_limit,
3037                        intermediate_bytes: params.intermediate_bytes,
3038                        deadline_ms: params.deadline_ms,
3039                        output_bytes: params.output_bytes,
3040                    },
3041                }
3042            }),
3043            total: &report.total,
3044            reached_limits: nonempty_slice(&report.reached_limits),
3045            work: &report.work,
3046            rows: report
3047                .rows
3048                .iter()
3049                .map(McpCompactDetailedRelationRow::from)
3050                .collect(),
3051        }
3052    }
3053}
3054
3055/// Compact federated wrapper that preserves cross-root evidence and work.
3056#[derive(Debug, Serialize)]
3057struct McpCompactFederatedDetailedRelationReport<'a> {
3058    /// Ordered validated participants.
3059    participants: &'a [FederatedParticipant],
3060    /// Worktree alias of the first-root result when aliases selected federation.
3061    #[serde(skip_serializing_if = "Option::is_none")]
3062    primary_worktree: Option<&'a str>,
3063    /// Compact first-root detailed relation page.
3064    primary: McpCompactDetailedRelationReport<'a>,
3065    /// Exact cross-root external rendezvous evidence.
3066    #[serde(skip_serializing_if = "Option::is_none")]
3067    rendezvous: Option<&'a [FederatedRendezvous]>,
3068    /// Whether primary or rendezvous work was truncated.
3069    #[serde(skip_serializing_if = "is_false")]
3070    truncated: bool,
3071    /// Stable aggregate limits reached by either stage.
3072    #[serde(skip_serializing_if = "Option::is_none")]
3073    reached_limits: Option<&'a [GraphLimitKind]>,
3074    /// Exact aggregate work.
3075    work: &'a FederatedRelationWork,
3076}
3077
3078impl<'a> McpCompactFederatedDetailedRelationReport<'a> {
3079    /// Project a federated page while preserving its exact continuation call.
3080    fn new(
3081        report: &'a FederatedDetailedRelationReport,
3082        file: &'a str,
3083        params: &'a AtlasSymbolRelationsParams,
3084    ) -> Self {
3085        Self {
3086            participants: &report.participants,
3087            primary_worktree: report.primary_worktree.as_deref(),
3088            primary: McpCompactDetailedRelationReport::new(&report.primary, file, params),
3089            rendezvous: nonempty_slice(&report.rendezvous),
3090            truncated: report.truncated,
3091            reached_limits: nonempty_slice(&report.reached_limits),
3092            work: &report.work,
3093        }
3094    }
3095}
3096
3097/// Borrow non-empty text into one optional compact field.
3098fn nonempty_str(value: &str) -> Option<&str> {
3099    (!value.is_empty()).then_some(value)
3100}
3101
3102/// Parse an optional adapter selection before any project or database work.
3103fn parse_content_selection(value: Option<&str>) -> Result<ContentSelection, CliError> {
3104    value
3105        .map(str::parse::<ContentSelection>)
3106        .transpose()
3107        .map(Option::unwrap_or_default)
3108        .map_err(|error| CliError::InvalidInput(error.to_string()))
3109}
3110
3111/// Borrow non-empty rows into one optional compact section.
3112fn nonempty_slice<T>(value: &[T]) -> Option<&[T]> {
3113    (!value.is_empty()).then_some(value)
3114}
3115
3116/// Return whether a compact unsigned count is zero.
3117#[allow(clippy::trivially_copy_pass_by_ref)]
3118const fn is_zero_u32(value: &u32) -> bool {
3119    *value == 0
3120}
3121
3122/// Return whether a compact unsigned total is zero.
3123#[allow(clippy::trivially_copy_pass_by_ref)]
3124const fn is_zero_u64(value: &u64) -> bool {
3125    *value == 0
3126}
3127
3128/// Agent startup brief payload.
3129#[derive(Debug, Serialize)]
3130struct McpSessionBrief {
3131    /// Selected project and index identity.
3132    project: McpSelectedProjectCapability,
3133    /// Route-affecting startup policy.
3134    policy: McpBriefPolicy,
3135    /// Overview counts when an index exists.
3136    overview: Option<Overview>,
3137    /// Indexed candidate folder rows.
3138    folders: Vec<McpBriefCandidate>,
3139    /// Indexed candidate file rows.
3140    files: Vec<McpBriefCandidate>,
3141    /// Bounded health blockers.
3142    blockers: McpBriefBlockers,
3143    /// Actionable bounded purpose-curator handoff for the selected project.
3144    purpose_handoff: Option<PurposeCuratorHandoff>,
3145    /// Recommended next calls.
3146    recommendations: Vec<McpBriefRecommendation>,
3147    /// Effective limits and truncation metadata.
3148    limits: McpBriefLimits,
3149}
3150
3151/// Additive compact projection of the compatibility-preserving startup brief.
3152#[derive(Debug, Serialize)]
3153struct McpCompactSessionBrief {
3154    /// Selected project identity needed for routing.
3155    project: McpCompactBriefProject,
3156    /// Non-default route-affecting startup policy.
3157    #[serde(skip_serializing_if = "Option::is_none")]
3158    policy: Option<McpBriefPolicy>,
3159    /// Compact overview counts when an index exists.
3160    #[serde(skip_serializing_if = "Option::is_none")]
3161    overview: Option<McpCompactBriefOverview>,
3162    /// Folder candidates only when no ready file candidate exists.
3163    #[serde(skip_serializing_if = "Vec::is_empty")]
3164    folders: Vec<McpCompactBriefCandidate>,
3165    /// Ready file candidates for the task.
3166    #[serde(skip_serializing_if = "Vec::is_empty")]
3167    files: Vec<McpCompactBriefCandidate>,
3168    /// Unsafe health blocker count when any exist.
3169    #[serde(skip_serializing_if = "Option::is_none")]
3170    blockers: Option<McpCompactBriefBlockers>,
3171    /// Exact host-owned purpose-curator follow-up when work is actionable.
3172    #[serde(skip_serializing_if = "Option::is_none")]
3173    purpose_handoff: Option<McpCompactBriefPurposeHandoff>,
3174    /// Recommended next calls.
3175    recommendations: Vec<McpCompactBriefRecommendation>,
3176    /// Non-default limits and truncation metadata.
3177    #[serde(skip_serializing_if = "Option::is_none")]
3178    limits: Option<McpCompactBriefLimits>,
3179}
3180
3181/// Compact selected-project state for the startup path.
3182#[derive(Debug, Serialize)]
3183struct McpCompactBriefProject {
3184    /// Registered alias used to capture this project state.
3185    #[serde(skip_serializing_if = "Option::is_none")]
3186    worktree: Option<String>,
3187    /// Durable control-catalog registration identity.
3188    #[serde(skip_serializing_if = "Option::is_none")]
3189    registration_id: Option<i64>,
3190    /// Canonical repository root.
3191    root: Option<String>,
3192    /// Whether the durable index is available.
3193    index_status: McpIndexStatus,
3194}
3195
3196/// Compact project counts for task startup.
3197#[derive(Debug, Serialize)]
3198struct McpCompactBriefOverview {
3199    /// Number of indexed files.
3200    files: usize,
3201    /// Number of indexed folders.
3202    folders: usize,
3203}
3204
3205/// Brief policy fields.
3206#[derive(Clone, Copy, Debug, Serialize)]
3207struct McpBriefPolicy {
3208    /// Whether nearest indexed project routing is enabled by default.
3209    nearest_project: McpPolicyState,
3210    /// Absolute-path routing scope.
3211    path_scope: McpPathScope,
3212}
3213
3214impl McpBriefPolicy {
3215    /// Return whether routing uses the ordinary selected-project policy.
3216    fn is_default(self) -> bool {
3217        self.nearest_project == McpPolicyState::Disabled
3218            && self.path_scope == McpPathScope::SelectedProject
3219    }
3220}
3221
3222/// Bounded ranked candidate row for startup briefs.
3223#[derive(Debug, Serialize)]
3224struct McpBriefCandidate {
3225    /// Repository-relative path.
3226    path: String,
3227    /// Indexed node kind.
3228    kind: String,
3229    /// Purpose lifecycle status.
3230    purpose_status: PurposeStatus,
3231    /// Purpose source.
3232    purpose_source: PurposeSource,
3233    /// Whether the purpose is current agent-approved authored responsibility state.
3234    purpose_agent_reviewed: bool,
3235    /// Purpose one-liner when present.
3236    purpose: Option<String>,
3237    /// Observed content summary when present.
3238    summary: Option<String>,
3239    /// Bounded ranking reasons.
3240    reasons: Vec<String>,
3241    /// Bounded compact ranking reason codes.
3242    reason_codes: Vec<RankedReasonCode>,
3243    /// Sparse stable-order connection counts.
3244    connection_counts: Vec<RankedConnectionCount>,
3245    /// Bounded high-value current connection sample.
3246    connections: Vec<RankedConnection>,
3247    /// Whether the bounded sample omitted any validated relation through family or global overflow.
3248    connections_truncated: bool,
3249    /// Existing navigation capability recommended after this row.
3250    next_call: NavigationNextCall,
3251}
3252
3253/// Bounded compact ranked candidate row for task startup.
3254#[derive(Debug, Serialize)]
3255struct McpCompactBriefCandidate {
3256    /// Repository-relative path.
3257    path: String,
3258    /// Purpose lifecycle status when it is not already agent-approved.
3259    #[serde(skip_serializing_if = "Option::is_none")]
3260    purpose_status: Option<PurposeStatus>,
3261    /// Purpose source when it is not already agent-approved.
3262    #[serde(skip_serializing_if = "Option::is_none")]
3263    purpose_source: Option<PurposeSource>,
3264    /// Whether the purpose is current agent-approved authored responsibility state.
3265    #[serde(skip_serializing_if = "is_false")]
3266    purpose_agent_reviewed: bool,
3267    /// Purpose one-liner when present.
3268    #[serde(skip_serializing_if = "Option::is_none")]
3269    purpose: Option<String>,
3270    /// One high-value current connection when available.
3271    #[serde(skip_serializing_if = "Vec::is_empty")]
3272    connections: Vec<RankedConnection>,
3273    /// Whether the bounded sample omitted any validated relation through family or global overflow.
3274    #[serde(skip_serializing_if = "is_false")]
3275    connections_truncated: bool,
3276    /// Existing navigation capability recommended after this row.
3277    next_call: NavigationNextCall,
3278}
3279
3280/// Compact host-owned purpose-curator handoff for startup briefs.
3281#[derive(Debug, Serialize)]
3282#[allow(clippy::struct_excessive_bools)]
3283struct McpCompactBriefPurposeHandoff {
3284    /// Whether this report is intended for an agent harness.
3285    #[serde(skip_serializing_if = "is_true")]
3286    agent_harness_expected: bool,
3287    /// Recommended host-relative reliable subagent tier selection.
3288    recommended_subagent_reasoning: &'static str,
3289    /// Single host-neutral selection instruction retained from the expanded handoff.
3290    instructions: Vec<String>,
3291    /// Whether the current main agent may process the same bounded batch.
3292    #[serde(skip_serializing_if = "is_true")]
3293    main_agent_fallback: bool,
3294    /// Explicitly records that `ProjectAtlas` did not spawn a host agent.
3295    #[serde(skip_serializing_if = "is_false")]
3296    server_started_curator: bool,
3297    /// Successful maintenance should not add ordinary conversation output.
3298    #[serde(skip_serializing_if = "is_true")]
3299    silent_on_success: bool,
3300    /// Whether the queue call has more rows after this bounded batch.
3301    #[serde(skip_serializing_if = "is_false")]
3302    truncated: bool,
3303    /// Exact existing MCP call that returns conditional-review tokens and bounded row context.
3304    next_call: McpCompactBriefRecommendation,
3305}
3306
3307/// Bounded health blocker section.
3308#[derive(Clone, Debug, Serialize)]
3309struct McpBriefBlockers {
3310    /// Findings after filters are applied.
3311    total: usize,
3312    /// Findings returned in this brief.
3313    returned: usize,
3314    /// Whether more blockers exist.
3315    truncated: bool,
3316    /// Blocker rows.
3317    items: Vec<McpBriefBlocker>,
3318}
3319
3320/// Compact blocker count; the recommendation carries the exact bounded health call.
3321#[derive(Debug, Serialize)]
3322struct McpCompactBriefBlockers {
3323    /// Findings after filters are applied.
3324    total: usize,
3325}
3326
3327/// Return whether a serialized optional fact is false and can be omitted.
3328#[allow(clippy::trivially_copy_pass_by_ref)]
3329const fn is_false(value: &bool) -> bool {
3330    !*value
3331}
3332
3333/// Return whether a serialized invariant is true and can be omitted.
3334#[allow(clippy::trivially_copy_pass_by_ref)]
3335const fn is_true(value: &bool) -> bool {
3336    *value
3337}
3338
3339/// One health blocker row for startup briefs.
3340#[derive(Clone, Debug, Serialize)]
3341struct McpBriefBlocker {
3342    /// Stable finding id.
3343    id: String,
3344    /// Finding severity.
3345    severity: Severity,
3346    /// Finding category.
3347    category: String,
3348    /// Primary path.
3349    path: String,
3350    /// Related path when applicable.
3351    related_path: Option<String>,
3352    /// Health message.
3353    message: String,
3354    /// Recommended agent action.
3355    recommendation: String,
3356}
3357
3358/// One typed startup recommendation.
3359#[derive(Debug, Serialize)]
3360struct McpBriefRecommendation {
3361    /// Stable recommendation kind.
3362    kind: McpBriefRecommendationKind,
3363    /// MCP tool name or filesystem/tool family.
3364    target: String,
3365    /// Concise machine-readable reason.
3366    reason: String,
3367    /// Suggested arguments for the target.
3368    arguments: serde_json::Value,
3369}
3370
3371/// One typed recommendation in the compact startup projection.
3372#[derive(Debug, Serialize)]
3373struct McpCompactBriefRecommendation {
3374    /// Stable recommendation kind.
3375    kind: McpBriefRecommendationKind,
3376    /// MCP tool name or filesystem/tool family.
3377    target: String,
3378    /// Concise machine-readable reason.
3379    reason: String,
3380    /// Suggested arguments for the target.
3381    arguments: serde_json::Value,
3382}
3383
3384/// Startup recommendation kinds.
3385#[derive(Clone, Copy, Debug, Serialize)]
3386#[serde(rename_all = "snake_case")]
3387enum McpBriefRecommendationKind {
3388    /// Initialize the selected project-local atlas.
3389    Init,
3390    /// Inspect the already-ranked file summary.
3391    Summary,
3392    /// Search the index when ranking has no directly navigable file.
3393    Search,
3394    /// Inspect detailed relations for the already-ranked file.
3395    Relations,
3396    /// Inspect structural health.
3397    Health,
3398    /// Load one bounded task-scoped purpose-curation queue.
3399    PurposeQueue,
3400    /// Read exact source or non-indexed files with normal filesystem tools.
3401    FilesystemTools,
3402}
3403
3404/// Effective startup brief row limits.
3405#[derive(Debug, Serialize)]
3406struct McpBriefLimits {
3407    /// Effective folder row limit.
3408    folder_limit: usize,
3409    /// Effective file row limit.
3410    file_limit: usize,
3411    /// Effective blocker row limit.
3412    blocker_limit: usize,
3413    /// Effective actionable purpose row limit.
3414    purpose_limit: usize,
3415    /// Whether folder candidates were truncated.
3416    folders_truncated: bool,
3417    /// Whether file candidates were truncated.
3418    files_truncated: bool,
3419    /// Whether more actionable low-scope purpose rows exist.
3420    purposes_truncated: bool,
3421}
3422
3423/// Non-default limits and truncation state in the compact startup projection.
3424#[derive(Debug, Serialize)]
3425struct McpCompactBriefLimits {
3426    /// Effective folder row limit.
3427    #[serde(skip_serializing_if = "is_compact_brief_default_limit")]
3428    folder_limit: usize,
3429    /// Effective file row limit.
3430    #[serde(skip_serializing_if = "is_compact_brief_default_limit")]
3431    file_limit: usize,
3432    /// Effective blocker row limit.
3433    #[serde(skip_serializing_if = "is_compact_brief_default_limit")]
3434    blocker_limit: usize,
3435    /// Effective actionable purpose row limit.
3436    #[serde(skip_serializing_if = "is_compact_brief_default_limit")]
3437    purpose_limit: usize,
3438    /// Whether folder candidates were omitted or truncated.
3439    #[serde(skip_serializing_if = "is_false")]
3440    folders_truncated: bool,
3441    /// Whether file candidates were truncated.
3442    #[serde(skip_serializing_if = "is_false")]
3443    files_truncated: bool,
3444    /// Whether more actionable low-scope purpose rows exist.
3445    #[serde(skip_serializing_if = "is_false")]
3446    purposes_truncated: bool,
3447}
3448
3449/// Return whether a startup row limit is the compact projection default.
3450#[allow(clippy::trivially_copy_pass_by_ref)]
3451const fn is_compact_brief_default_limit(value: &usize) -> bool {
3452    *value == COMPACT_SESSION_BRIEF_DEFAULT_LIMIT
3453}
3454
3455/// Task status lookup response.
3456#[derive(Debug, Serialize)]
3457struct McpTaskStatusResponse {
3458    /// Requested task id.
3459    task_id: String,
3460    /// Lookup outcome.
3461    lookup: McpTaskLookupStatus,
3462    /// Supported task states in this contract.
3463    states: Vec<McpTaskState>,
3464    /// Supported operation families in this contract.
3465    operations: Vec<McpTaskOperation>,
3466    /// Registry capacity.
3467    registry_capacity: usize,
3468    /// Task record when found.
3469    task: Option<McpTaskRecord>,
3470}
3471
3472/// Task lookup outcome.
3473#[derive(Debug, Eq, PartialEq, Serialize)]
3474#[serde(rename_all = "snake_case")]
3475enum McpTaskLookupStatus {
3476    /// The task was found.
3477    Found,
3478    /// The task id is unknown to this MCP session.
3479    NotFound,
3480}
3481
3482/// Task cancellation response.
3483#[derive(Debug, Serialize)]
3484struct McpTaskCancelResponse {
3485    /// Requested task id.
3486    task_id: String,
3487    /// Cancellation outcome.
3488    result: McpTaskCancelResult,
3489    /// Registry capacity.
3490    registry_capacity: usize,
3491    /// Task record when found.
3492    task: Option<McpTaskRecord>,
3493}
3494
3495/// Immediate response for one accepted background indexing task.
3496#[derive(Debug, Serialize)]
3497struct McpTaskStartResponse {
3498    /// Opaque session-local task id.
3499    task_id: String,
3500    /// Accepted operation family.
3501    operation: McpTaskOperation,
3502    /// Initial task state.
3503    state: McpTaskState,
3504    /// Existing MCP tool used to poll the task.
3505    status_tool: &'static str,
3506    /// Existing MCP tool used to request cancellation.
3507    cancel_tool: &'static str,
3508}
3509
3510/// Task cancellation outcome.
3511#[derive(Debug, Eq, PartialEq, Serialize)]
3512#[serde(rename_all = "snake_case")]
3513enum McpTaskCancelResult {
3514    /// Cooperative cancellation was delivered to active work.
3515    CancellationRequested,
3516    /// The task id is unknown to this MCP session.
3517    NotFound,
3518    /// The task was already finished.
3519    AlreadyFinished,
3520    /// The task exists but cannot currently be canceled.
3521    NotCancelable,
3522}
3523
3524/// Fixed worker partition shared by background indexing tasks in one MCP server.
3525#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3526struct McpBackgroundResourceEnvelope {
3527    /// Maximum concurrently admitted background tasks.
3528    task_limit: usize,
3529    /// Maximum scan and parser workers available to each admitted task.
3530    workers_per_task: usize,
3531    /// Maximum aggregate workers owned by all admitted tasks.
3532    total_worker_limit: usize,
3533}
3534
3535impl McpBackgroundResourceEnvelope {
3536    /// Derive one fixed partition from supported host availability and process policy.
3537    fn for_host() -> Self {
3538        let available_workers = thread::available_parallelism().map_or(1, usize::from);
3539        Self::from_available_workers(available_workers)
3540    }
3541
3542    /// Derive a deterministic envelope from an observed host worker count.
3543    fn from_available_workers(available_workers: usize) -> Self {
3544        let total_worker_limit = available_workers.clamp(1, INDEX_WORKER_SAFE_CEILING);
3545        let task_limit = total_worker_limit.min(MCP_BACKGROUND_TASK_SAFE_CEILING);
3546        let workers_per_task = (total_worker_limit / task_limit).max(1);
3547        Self {
3548            task_limit,
3549            workers_per_task,
3550            total_worker_limit,
3551        }
3552    }
3553}
3554
3555/// Native `ProjectAtlas` MCP server backed by the same services as the CLI.
3556#[derive(Debug, Clone)]
3557pub(crate) struct ProjectAtlasMcpServer {
3558    /// Immutable registry/control authority selected when the MCP process started.
3559    control_state: McpProjectState,
3560    /// Active project state for calls that omit `project_path`.
3561    project_state: Arc<RwLock<McpProjectState>>,
3562    /// Caller-visible compatibility label applied to this MCP process's events.
3563    session: String,
3564    /// Bounded telemetry lifecycle shared by every server clone and routed project.
3565    usage_runtime: Arc<Mutex<McpUsageRuntime>>,
3566    /// Whether absolute path arguments may select the nearest indexed project by default.
3567    allow_nearest_project: bool,
3568    /// Bounded MCP task-progress records for this server session.
3569    task_registry: Arc<RwLock<McpTaskRegistry>>,
3570    /// Fixed aggregate worker partition for background indexing tasks.
3571    background_resources: McpBackgroundResourceEnvelope,
3572    /// Monotonic session-local task identifier source.
3573    next_task_sequence: Arc<AtomicU64>,
3574    /// Bounded per-project source observers and verified read epochs.
3575    source_observations: Arc<SourceObservationRegistry>,
3576    /// Official RMCP tool router.
3577    tool_router: ToolRouter<Self>,
3578}
3579
3580/// Bridge one RMCP request cancellation token into synchronous index work.
3581struct McpRequestCancellationBridge {
3582    /// Signal used to stop the request-local cancellation monitor.
3583    stop: Arc<std::sync::atomic::AtomicBool>,
3584    /// Join handle for the bounded request-local monitor thread.
3585    monitor: Option<thread::JoinHandle<()>>,
3586    /// Direct request probe retained for synchronous cancellation fences.
3587    probe: Arc<dyn Fn() -> bool + Send + Sync>,
3588}
3589
3590impl McpRequestCancellationBridge {
3591    /// Start a request-local monitor from one owned RMCP context.
3592    fn start(
3593        context: &RequestContext<RoleServer>,
3594        control: &IndexWorkControl,
3595    ) -> Result<Self, CliError> {
3596        let token = context.ct.clone();
3597        Self::start_with_probe(move || token.is_cancelled(), control)
3598    }
3599
3600    /// Start a cancellation monitor from a deterministic probe for tests.
3601    fn start_with_probe<P>(probe: P, control: &IndexWorkControl) -> Result<Self, CliError>
3602    where
3603        P: Fn() -> bool + Send + Sync + 'static,
3604    {
3605        let probe: Arc<dyn Fn() -> bool + Send + Sync> = Arc::new(probe);
3606        if probe() {
3607            control.cancel();
3608        }
3609        let observed_control = control.clone();
3610        let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
3611        let monitor_stop = Arc::clone(&stop);
3612        let monitor_probe = Arc::clone(&probe);
3613        let monitor = thread::Builder::new()
3614            .name(MCP_CANCELLATION_MONITOR_THREAD_NAME.to_string())
3615            .spawn(move || {
3616                while !monitor_stop.load(Ordering::Acquire) {
3617                    if monitor_probe() {
3618                        observed_control.cancel();
3619                        break;
3620                    }
3621                    thread::sleep(std::time::Duration::from_millis(5));
3622                }
3623            })
3624            .map_err(|source| {
3625                let mut message = MCP_CANCELLATION_MONITOR_START_ERROR_PREFIX.to_string();
3626                message.push_str(&source.to_string());
3627                CliError::InvalidInput(message)
3628            })?;
3629        Ok(Self {
3630            stop,
3631            monitor: Some(monitor),
3632            probe,
3633        })
3634    }
3635
3636    /// Copy the request token into synchronous index-work cancellation.
3637    fn synchronize(&self, control: &IndexWorkControl) {
3638        if (self.probe)() {
3639            control.cancel();
3640        }
3641    }
3642}
3643
3644impl Drop for McpRequestCancellationBridge {
3645    fn drop(&mut self) {
3646        self.stop.store(true, Ordering::Release);
3647        if let Some(monitor) = self.monitor.take() {
3648            drop(monitor.join());
3649        }
3650    }
3651}
3652
3653impl ProjectAtlasMcpServer {
3654    /// Create a `ProjectAtlas` MCP server instance.
3655    pub(crate) fn new(
3656        db_path: PathBuf,
3657        config_path: Option<PathBuf>,
3658        session: String,
3659        allow_nearest_project: bool,
3660    ) -> Self {
3661        let startup_state = Self::startup_project_state(db_path, config_path);
3662        Self {
3663            control_state: startup_state.clone(),
3664            project_state: Arc::new(RwLock::new(startup_state)),
3665            session,
3666            usage_runtime: Arc::new(Mutex::new(McpUsageRuntime::default())),
3667            allow_nearest_project,
3668            task_registry: Arc::new(RwLock::new(McpTaskRegistry::new())),
3669            background_resources: McpBackgroundResourceEnvelope::for_host(),
3670            next_task_sequence: Arc::new(AtomicU64::new(1)),
3671            source_observations: Arc::new(SourceObservationRegistry::default()),
3672            tool_router: Self::tool_router(),
3673        }
3674    }
3675
3676    /// Open the durable index through one root-bound read snapshot.
3677    fn open_read_store(state: &McpProjectState) -> Result<AtlasStore, CliError> {
3678        if !state.db_path.exists() {
3679            return Err(Self::with_target_error_context(
3680                index_init_required(&state.root, &state.db_path),
3681                state,
3682            ));
3683        }
3684        let store = open_atlas_store_read_only_for_project(&state.db_path, &state.root)
3685            .map_err(|error| Self::with_target_error_context(error, state))?;
3686        Self::require_captured_worktree_identity(state.worktree.as_ref(), &store)
3687            .map_err(|error| Self::with_target_error_context(error, state))?;
3688        Ok(store)
3689    }
3690
3691    /// Require registered worktree aliases to be initialized outside `atlas_init`.
3692    fn require_initialized_worktree_target(state: &McpProjectState) -> Result<(), CliError> {
3693        if state.worktree.is_some() && !state.db_path.is_file() {
3694            return Err(Self::with_target_error_context(
3695                index_init_required(&state.root, &state.db_path),
3696                state,
3697            ));
3698        }
3699        Ok(())
3700    }
3701
3702    /// Preserve a captured worktree alias on typed init/refresh recovery state.
3703    fn with_target_error_context(mut error: CliError, state: &McpProjectState) -> CliError {
3704        let Some(selection) = state.worktree.as_ref() else {
3705            return error;
3706        };
3707        match &mut error {
3708            CliError::InitRequired(report) => report.worktree = Some(selection.alias.clone()),
3709            CliError::RefreshRequired(report) => report.worktree = Some(selection.alias.clone()),
3710            CliError::VerificationIncomplete(report) => {
3711                report.worktree = Some(selection.alias.clone());
3712            }
3713            CliError::ProjectMismatch(report) => {
3714                report.worktree = Some(selection.alias.clone());
3715            }
3716            _ => {}
3717        }
3718        error
3719    }
3720
3721    /// Run one normal MCP query inside the verified source-epoch boundary.
3722    #[cfg(test)]
3723    fn with_fresh_store<T, F>(
3724        &self,
3725        state: &McpProjectState,
3726        query: F,
3727    ) -> Result<VerifiedReadOutcome<T>, CliError>
3728    where
3729        F: FnMut(&AtlasStore, VerifiedReadStamp) -> Result<T, CliError>,
3730    {
3731        self.with_fresh_store_for_request(state, None, query)
3732    }
3733
3734    /// Run one verified query with optional RMCP request cancellation bridging.
3735    fn with_fresh_store_for_request<T, F>(
3736        &self,
3737        state: &McpProjectState,
3738        context: Option<RequestContext<RoleServer>>,
3739        mut query: F,
3740    ) -> Result<VerifiedReadOutcome<T>, CliError>
3741    where
3742        F: FnMut(&AtlasStore, VerifiedReadStamp) -> Result<T, CliError>,
3743    {
3744        self.with_fresh_store_controlled_for_request(state, context, |store, stamp, _control| {
3745            query(store, stamp)
3746        })
3747    }
3748
3749    /// Run one verified query that consumes the request cancellation boundary.
3750    fn with_fresh_store_controlled_for_request<T, F>(
3751        &self,
3752        state: &McpProjectState,
3753        context: Option<RequestContext<RoleServer>>,
3754        mut query: F,
3755    ) -> Result<VerifiedReadOutcome<T>, CliError>
3756    where
3757        F: FnMut(&AtlasStore, VerifiedReadStamp, &IndexWorkControl) -> Result<T, CliError>,
3758    {
3759        if !state.db_path.is_file() {
3760            return Err(Self::with_target_error_context(
3761                index_init_required(&state.root, &state.db_path),
3762                state,
3763            ));
3764        }
3765        let control =
3766            index_work_control(&SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, None, None));
3767        let bridge = context
3768            .map(|context| McpRequestCancellationBridge::start(&context, &control))
3769            .transpose()?;
3770        let result = self
3771            .source_observations
3772            .with_verified_read(
3773                &state.db_path,
3774                &state.root,
3775                state.config_path.as_deref(),
3776                &control,
3777                |store, stamp| {
3778                    Self::require_captured_worktree_identity(state.worktree.as_ref(), store)?;
3779                    query(store, stamp, &control)
3780                },
3781            )
3782            .map_err(|error| Self::with_target_error_context(error, state));
3783        if let Some(bridge) = bridge.as_ref() {
3784            bridge.synchronize(&control);
3785        }
3786        drop(bridge);
3787        result
3788    }
3789
3790    /// Revalidate one alias selection against the exact opened read snapshot.
3791    fn require_captured_worktree_identity(
3792        selection: Option<&McpWorktreeSelection>,
3793        store: &AtlasStore,
3794    ) -> Result<(), CliError> {
3795        let Some(expected) = selection.and_then(|selection| selection.project_instance_id) else {
3796            return Ok(());
3797        };
3798        if store.project_instance_id()? != Some(expected) {
3799            return Err(CliError::InvalidInput(
3800                MCP_ERROR_WORKTREE_IDENTITY_CONFLICT.to_string(),
3801            ));
3802        }
3803        Ok(())
3804    }
3805
3806    /// Revalidate the control catalog that owns one captured alias registration.
3807    fn require_captured_control_identity(
3808        selection: Option<&McpWorktreeSelection>,
3809        control: &AtlasStore,
3810    ) -> Result<(), CliError> {
3811        let Some(expected) = selection.and_then(|selection| selection.control_project_instance_id)
3812        else {
3813            return Ok(());
3814        };
3815        if control.project_instance_id()? != Some(expected) {
3816            return Err(CliError::InvalidInput(
3817                MCP_ERROR_WORKTREE_CONTROL_IDENTITY_CONFLICT.to_string(),
3818            ));
3819        }
3820        Ok(())
3821    }
3822
3823    /// Admit only federation snapshots that retain their captured alias identities.
3824    fn require_federated_worktree_identities(
3825        stores: Vec<FederatedStore>,
3826        selections: &[McpWorktreeSelection],
3827    ) -> Result<Vec<FederatedStore>, CliError> {
3828        if stores.len() != selections.len() {
3829            for store in stores {
3830                drop(store.finish());
3831            }
3832            return Err(CliError::InvalidInput(
3833                MCP_ERROR_FEDERATED_ALIAS_MISSING.to_string(),
3834            ));
3835        }
3836        for (store, selection) in stores.iter().zip(selections) {
3837            if let Err(error) =
3838                Self::require_captured_worktree_identity(Some(selection), store.store())
3839            {
3840                let error = federated_worktree_error(error, &selection.alias);
3841                for store in stores {
3842                    drop(store.finish());
3843                }
3844                return Err(error);
3845            }
3846        }
3847        Ok(stores)
3848    }
3849
3850    /// Run one rendered MCP query with request cancellation bridged into index work.
3851    fn with_fresh_string_for_request<F>(
3852        &self,
3853        state: &McpProjectState,
3854        context: Option<RequestContext<RoleServer>>,
3855        mut query: F,
3856    ) -> Result<String, CliError>
3857    where
3858        F: FnMut(&AtlasStore, VerifiedReadStamp) -> Result<String, CliError>,
3859    {
3860        self.with_fresh_string_and_usage_for_request(state, context, |store, stamp| {
3861            Ok((query(store, stamp)?, None))
3862        })
3863    }
3864
3865    /// Record optional usage only after source and `SQLite` result acceptance succeeds.
3866    fn with_fresh_string_and_usage_for_request<F>(
3867        &self,
3868        state: &McpProjectState,
3869        context: Option<RequestContext<RoleServer>>,
3870        mut query: F,
3871    ) -> Result<String, CliError>
3872    where
3873        F: FnMut(
3874            &AtlasStore,
3875            VerifiedReadStamp,
3876        ) -> Result<(String, Option<McpUsageIntent>), CliError>,
3877    {
3878        self.with_fresh_string_and_usage_controlled_for_request(
3879            state,
3880            context,
3881            |store, stamp, _control| query(store, stamp),
3882        )
3883    }
3884
3885    /// Render one verified MCP query that consumes request cancellation.
3886    fn with_fresh_string_and_usage_controlled_for_request<F>(
3887        &self,
3888        state: &McpProjectState,
3889        context: Option<RequestContext<RoleServer>>,
3890        query: F,
3891    ) -> Result<String, CliError>
3892    where
3893        F: FnMut(
3894            &AtlasStore,
3895            VerifiedReadStamp,
3896            &IndexWorkControl,
3897        ) -> Result<(String, Option<McpUsageIntent>), CliError>,
3898    {
3899        let outcome = self.with_fresh_store_controlled_for_request(state, context, query)?;
3900        let stamp = outcome.stamp.clone();
3901        let (value, usage) = outcome.value;
3902        let output_bytes = value.len();
3903        let outcome = VerifiedReadOutcome {
3904            value,
3905            stamp,
3906            work: outcome.work,
3907        }
3908        .with_output_bytes(output_bytes);
3909        if let Some(usage) = usage {
3910            self.record_accepted_usage(state, &outcome.stamp, &usage, &outcome.value);
3911        }
3912        Ok(outcome.value)
3913    }
3914
3915    /// Open the durable index for mutation and bind any migrated alias identity first.
3916    fn open_mut_store(
3917        state: &McpProjectState,
3918        control_state: &McpProjectState,
3919    ) -> Result<AtlasStore, CliError> {
3920        let Some(selection) = state
3921            .worktree
3922            .as_ref()
3923            .filter(|selection| selection.registration_id.is_some())
3924        else {
3925            let store = open_atlas_store_for_project(&state.db_path, &state.root)?;
3926            Self::require_captured_worktree_identity(state.worktree.as_ref(), &store)?;
3927            return Ok(store);
3928        };
3929        Self::open_registered_worktree_mut_store(state, control_state, selection)
3930    }
3931
3932    /// Open and, when needed, bind one alias under exact control-writer exclusion.
3933    fn open_registered_worktree_mut_store(
3934        state: &McpProjectState,
3935        control_state: &McpProjectState,
3936        selection: &McpWorktreeSelection,
3937    ) -> Result<AtlasStore, CliError> {
3938        let alias = WorktreeAlias::parse(&selection.alias)?;
3939        let registration_id =
3940            selection
3941                .registration_id
3942                .ok_or_else(|| DbError::WorktreeRegistrationNotFound {
3943                    alias: selection.alias.clone(),
3944                })?;
3945        let control = open_atlas_store_for_project(&control_state.db_path, &control_state.root)?;
3946        Self::require_captured_control_identity(Some(selection), &control)?;
3947        control.with_active_worktree_registration(registration_id, &alias, |guard| {
3948            if let Err(error) =
3949                require_registered_worktree_lifecycle(guard.registration(), &state.root)
3950            {
3951                return Ok(Err(error));
3952            }
3953            if !state.db_path.is_file() {
3954                return Ok(Err(Self::with_target_error_context(
3955                    index_init_required(&state.root, &state.db_path),
3956                    state,
3957                )));
3958            }
3959            let target = match open_atlas_store_for_project(&state.db_path, &state.root) {
3960                Ok(store) => store,
3961                Err(error) => return Ok(Err(error)),
3962            };
3963            let project = match target.captured_project_binding() {
3964                Ok(binding) => binding.project_instance_id,
3965                Err(error) => return Ok(Err(error.into())),
3966            };
3967            if selection
3968                .project_instance_id
3969                .is_some_and(|expected| expected != project)
3970            {
3971                return Ok(Err(CliError::InvalidInput(
3972                    MCP_ERROR_WORKTREE_IDENTITY_CONFLICT.to_string(),
3973                )));
3974            }
3975            match guard.registration().project_instance_id {
3976                Some(bound) if bound == project => {}
3977                Some(_) => {
3978                    return Ok(Err(CliError::InvalidInput(
3979                        MCP_ERROR_WORKTREE_IDENTITY_CONFLICT.to_string(),
3980                    )));
3981                }
3982                None => {
3983                    let snapshot = match target.export_worktree_usage_snapshot() {
3984                        Ok(snapshot) => snapshot,
3985                        Err(error) => return Ok(Err(error.into())),
3986                    };
3987                    if let Err(error) =
3988                        require_registered_worktree_lifecycle(guard.registration(), &state.root)
3989                    {
3990                        return Ok(Err(error));
3991                    }
3992                    if let Err(error) = require_current_worktree_usage_snapshot(
3993                        &state.db_path,
3994                        &state.root,
3995                        &snapshot,
3996                    ) {
3997                        return Ok(Err(error));
3998                    }
3999                    guard.bind_project_with_usage_snapshot(&state.root, project, &snapshot)?;
4000                }
4001            }
4002            Ok(Ok(target))
4003        })?
4004    }
4005
4006    /// Open an existing selected-project index for purpose or health mutation.
4007    fn open_existing_mut_store(
4008        state: &McpProjectState,
4009        control_state: &McpProjectState,
4010    ) -> Result<AtlasStore, CliError> {
4011        if !state.db_path.is_file() {
4012            return Err(Self::with_target_error_context(
4013                index_init_required(&state.root, &state.db_path),
4014                state,
4015            ));
4016        }
4017        Self::open_mut_store(state, control_state)
4018    }
4019
4020    /// Apply one purpose mutation under a source witness retained through commit.
4021    fn with_admitted_purpose_mutation<T>(
4022        &self,
4023        state: &McpProjectState,
4024        context: Option<RequestContext<RoleServer>>,
4025        mutation: impl FnOnce(&AtlasStore) -> Result<T, CliError>,
4026    ) -> Result<T, CliError> {
4027        if !state.db_path.is_file() {
4028            return Err(Self::with_target_error_context(
4029                index_init_required(&state.root, &state.db_path),
4030                state,
4031            ));
4032        }
4033        let control =
4034            index_work_control(&SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, None, None));
4035        let bridge = context
4036            .map(|context| McpRequestCancellationBridge::start(&context, &control))
4037            .transpose()?;
4038        let result = self
4039            .with_admitted_purpose_mutation_controlled(state, &control, bridge.as_ref(), mutation)
4040            .map_err(|error| Self::with_target_error_context(error, state));
4041        drop(bridge);
4042        result
4043    }
4044
4045    /// Apply one admitted mutation with explicit cancellation and rollback boundaries.
4046    fn with_admitted_purpose_mutation_controlled<T>(
4047        &self,
4048        state: &McpProjectState,
4049        control: &IndexWorkControl,
4050        bridge: Option<&McpRequestCancellationBridge>,
4051        mutation: impl FnOnce(&AtlasStore) -> Result<T, CliError>,
4052    ) -> Result<T, CliError> {
4053        if let Some(bridge) = bridge {
4054            bridge.synchronize(control);
4055        }
4056        let admission = self.source_observations.admit_mutation(
4057            &state.db_path,
4058            &state.root,
4059            state.config_path.as_deref(),
4060            control,
4061        )?;
4062        let store = Self::open_existing_mut_store(state, &self.control_state)?;
4063        Self::require_captured_worktree_identity(state.worktree.as_ref(), &store)?;
4064        let transaction = store.begin_purpose_mutation()?;
4065        let operation = (|| {
4066            let value = mutation(&store)?;
4067            if let Some(bridge) = bridge {
4068                bridge.synchronize(control);
4069            }
4070            admission.verify()?;
4071            if let Some(bridge) = bridge {
4072                bridge.synchronize(control);
4073            }
4074            control.check(IndexWorkStage::Publication)?;
4075            Ok(value)
4076        })();
4077        match operation {
4078            Ok(value) => {
4079                transaction.commit()?;
4080                Ok(value)
4081            }
4082            Err(operation) => Err(crate::rollback_rejected_purpose_mutation(
4083                transaction,
4084                operation,
4085            )),
4086        }
4087    }
4088
4089    /// Return whether this MCP process can record optional telemetry.
4090    fn telemetry_enabled() -> bool {
4091        !telemetry_disabled()
4092    }
4093
4094    /// Record telemetry and rotate only this project's scope if baselines are full.
4095    fn record_usage_for_state<F>(&self, state: &McpProjectState, store: &AtlasStore, record: F)
4096    where
4097        F: FnMut(UsageRuntimeInstance) -> Result<(), CliError>,
4098    {
4099        self.record_usage_for_origin(state, store, None, record);
4100    }
4101
4102    /// Record telemetry under one exact authority and optional worktree origin.
4103    fn record_usage_for_origin<F>(
4104        &self,
4105        state: &McpProjectState,
4106        store: &AtlasStore,
4107        worktree_registration_id: Option<i64>,
4108        mut record: F,
4109    ) where
4110        F: FnMut(UsageRuntimeInstance) -> Result<(), CliError>,
4111    {
4112        if telemetry_disabled() {
4113            return;
4114        }
4115        let Ok(binding) =
4116            McpUsageProjectBinding::capture_with_origin(state, store, worktree_registration_id)
4117        else {
4118            return;
4119        };
4120        let Some(project_instance) = self
4121            .usage_runtime
4122            .lock()
4123            .ok()
4124            .and_then(|mut runtime| runtime.instance_for_binding(binding, store))
4125        else {
4126            return;
4127        };
4128        let Ok(mut usage_instance) = project_instance.lock() else {
4129            return;
4130        };
4131        if !matches!(
4132            record(*usage_instance),
4133            Err(CliError::Db(DbError::TelemetryBaselineCapacity))
4134        ) {
4135            return;
4136        }
4137
4138        let Some(next_instance) = UsageRuntimeInstance::new(UsageInstanceOwner::McpProcess) else {
4139            return;
4140        };
4141        // Creating a candidate has no lifecycle effect; install it only after
4142        // the old identity is durably sealed so failure cannot leak or replace it.
4143        if usage_instance.seal(store).is_err() {
4144            return;
4145        }
4146        *usage_instance = next_instance;
4147        drop(record(next_instance));
4148    }
4149
4150    /// Best-effort telemetry for one result whose source epoch has already been accepted.
4151    fn record_accepted_usage(
4152        &self,
4153        state: &McpProjectState,
4154        stamp: &VerifiedReadStamp,
4155        intent: &McpUsageIntent,
4156        output: &str,
4157    ) {
4158        if !Self::telemetry_enabled() {
4159            return;
4160        }
4161        let Ok(store) = Self::open_read_store(state) else {
4162            return;
4163        };
4164        let Ok(binding) = store.captured_project_binding() else {
4165            return;
4166        };
4167        if binding.project_instance_id != stamp.project_instance_id {
4168            return;
4169        }
4170        if let Some(selection) = state
4171            .worktree
4172            .as_ref()
4173            .filter(|selection| selection.registration_id.is_some())
4174        {
4175            let Some(registration_id) = selection.registration_id else {
4176                return;
4177            };
4178            let event = match &intent.baseline {
4179                McpUsageBaseline::Estimate(baseline_tokens) => usage_from_estimates_with_context(
4180                    &self.session,
4181                    intent.command,
4182                    intent.path.clone(),
4183                    intent.query.clone(),
4184                    *baseline_tokens,
4185                    projectatlas_core::outline::estimate_tokens(output),
4186                    TOKEN_BUCKET_NAVIGATION_AVOIDANCE,
4187                    TOKEN_BASELINE_SELECTED_CANDIDATES,
4188                    TOKEN_CONFIDENCE_INFERRED,
4189                ),
4190                McpUsageBaseline::DirectoryWalk(baseline_tokens) => {
4191                    usage_from_estimates_with_context(
4192                        &self.session,
4193                        intent.command,
4194                        intent.path.clone(),
4195                        intent.query.clone(),
4196                        *baseline_tokens,
4197                        projectatlas_core::outline::estimate_tokens(output),
4198                        TOKEN_BUCKET_NAVIGATION_AVOIDANCE,
4199                        TOKEN_BASELINE_DIRECTORY_WALK,
4200                        TOKEN_CONFIDENCE_POLICY_ESTIMATE,
4201                    )
4202                }
4203                McpUsageBaseline::Text(baseline_text) => usage_from_text(
4204                    &self.session,
4205                    intent.command,
4206                    intent.path.clone(),
4207                    intent.query.clone(),
4208                    baseline_text,
4209                    output,
4210                ),
4211            };
4212            let Ok(control) = Self::open_read_store(&self.control_state) else {
4213                return;
4214            };
4215            if Self::require_captured_control_identity(Some(selection), &control).is_err() {
4216                return;
4217            }
4218            if control.finish_index_read_snapshot().is_ok() {
4219                self.record_usage_for_origin(
4220                    &self.control_state,
4221                    &control,
4222                    Some(registration_id),
4223                    |usage_instance| {
4224                        usage_instance.record_for_worktree(&control, registration_id, &event)
4225                    },
4226                );
4227            }
4228            return;
4229        }
4230        self.record_usage_for_state(state, &store, |usage_instance| match &intent.baseline {
4231            McpUsageBaseline::Estimate(baseline_tokens) => record_usage_estimate(
4232                &store,
4233                Some(usage_instance),
4234                &self.session,
4235                intent.command,
4236                intent.path.clone(),
4237                intent.query.clone(),
4238                *baseline_tokens,
4239                output,
4240            ),
4241            McpUsageBaseline::DirectoryWalk(baseline_tokens) => {
4242                record_directory_walk_usage_estimate(
4243                    &store,
4244                    Some(usage_instance),
4245                    &self.session,
4246                    intent.command,
4247                    intent.path.clone(),
4248                    intent.query.clone(),
4249                    *baseline_tokens,
4250                    output,
4251                )
4252            }
4253            McpUsageBaseline::Text(baseline_text) => record_usage_text(
4254                &store,
4255                Some(usage_instance),
4256                &self.session,
4257                intent.command,
4258                intent.path.clone(),
4259                intent.query.clone(),
4260                baseline_text,
4261                output,
4262            ),
4263        });
4264    }
4265
4266    /// Reuse an optional broad-source telemetry baseline within one complete generation.
4267    fn estimated_source_tokens_cached(
4268        &self,
4269        state: &McpProjectState,
4270        store: &AtlasStore,
4271        stamp: &VerifiedReadStamp,
4272        folder: Option<&str>,
4273        file_pattern: Option<&str>,
4274    ) -> Result<usize, CliError> {
4275        let key = McpSourceTokenBaselineKey {
4276            binding: McpUsageProjectBinding {
4277                root: state.root.clone(),
4278                db_path: state.db_path.clone(),
4279                project_instance_id: stamp.project_instance_id,
4280                worktree_registration_id: None,
4281            },
4282            generation: stamp.generation,
4283            folder: folder.map(ToOwned::to_owned),
4284            file_pattern: file_pattern.map(ToOwned::to_owned),
4285        };
4286        if let Some(value) = self
4287            .usage_runtime
4288            .lock()
4289            .ok()
4290            .and_then(|runtime| runtime.source_token_baseline(&key))
4291        {
4292            return Ok(value);
4293        }
4294        let value = estimated_source_tokens_for_indexed_files(store, folder, file_pattern)?;
4295        if let Ok(mut runtime) = self.usage_runtime.lock() {
4296            runtime.insert_source_token_baseline(key, value);
4297        }
4298        Ok(value)
4299    }
4300
4301    /// Best-effort seal each selected project's current identity at shutdown.
4302    fn seal_usage_instances_for_projects(&self) {
4303        let Some(projects) = self
4304            .usage_runtime
4305            .lock()
4306            .ok()
4307            .map(|runtime| runtime.snapshot())
4308        else {
4309            return;
4310        };
4311        for project in projects {
4312            let Ok(instance) = project.instance.lock() else {
4313                continue;
4314            };
4315            if let Ok(store) =
4316                open_atlas_store_for_project(&project.binding.db_path, &project.binding.root)
4317            {
4318                drop(instance.seal(&store));
4319            }
4320        }
4321    }
4322
4323    /// Load effective atlas config for the selected state.
4324    fn load_config_for_state(state: &McpProjectState) -> Result<AtlasMapConfig, CliError> {
4325        state
4326            .config_path
4327            .as_deref()
4328            .map_or_else(
4329                || load_atlas_config_for_root(&state.root).map_err(CliError::from),
4330                |config_path| load_atlas_config(Some(config_path)).map_err(CliError::from),
4331            )
4332            .map(|config| config.with_database_path(&state.db_path))
4333    }
4334
4335    /// Return the selected project root used by initialization.
4336    fn init_project_root(
4337        &self,
4338        project_path: Option<String>,
4339        worktree: Option<String>,
4340    ) -> Result<McpProjectState, CliError> {
4341        self.state_for_target_with_config_validation(
4342            project_path,
4343            worktree,
4344            McpConfigValidation::Immediate,
4345        )
4346    }
4347
4348    /// Initialize one registered worktree, preferring a reconciled control-atlas baseline.
4349    fn run_registered_worktree_init(
4350        &self,
4351        state: &McpProjectState,
4352        config_path: &Path,
4353        options: &InitBootstrapOptions,
4354    ) -> Result<InitSetupReport, CliError> {
4355        let Some(selection) = state
4356            .worktree
4357            .as_ref()
4358            .filter(|selection| selection.registration_id.is_some())
4359        else {
4360            let report =
4361                run_init_bootstrap(&state.root, &state.db_path, Some(config_path), options)?;
4362            if report.ok {
4363                self.bind_initialized_registration_for_root(state)?;
4364            }
4365            return Ok(report);
4366        };
4367        if state.db_path.is_file() {
4368            let mut report =
4369                run_init_bootstrap(&state.root, &state.db_path, Some(config_path), options)?;
4370            report.hydration = Some(InitHydrationPhase {
4371                status: InitHydrationStatus::Existing,
4372                source_root: None,
4373                source_project_instance_id: None,
4374                target_project_instance_id: None,
4375                baseline_generation: None,
4376                reconciled_generation: None,
4377                fallback_reason: None,
4378            });
4379            if report.ok {
4380                self.bind_initialized_worktree(selection, state)?;
4381            }
4382            return Ok(report);
4383        }
4384
4385        preflight_existing_project_binding(&state.db_path, &state.root)?;
4386        let project_dir = state.root.join(PROJECTATLAS_DIR_NAME);
4387        let nonsource_file = project_dir.join(MCP_NONSOURCE_FILE_NAME);
4388        let project_dir_existed = project_dir.exists();
4389        let config_existed = config_path.exists();
4390        let nonsource_existed = nonsource_file.exists();
4391        init_project_with_config(&state.root, Some(config_path))?;
4392
4393        let hydration = if options.no_scan {
4394            McpWorktreeHydration::Fallback(MCP_HYDRATION_NO_SCAN_REASON.to_string())
4395        } else {
4396            self.attempt_worktree_hydration(state, config_path, options)?
4397        };
4398        let mut report = match hydration {
4399            McpWorktreeHydration::Activated { hydration, scan } => {
4400                let mut report = run_init_bootstrap(
4401                    &state.root,
4402                    &state.db_path,
4403                    Some(config_path),
4404                    &InitBootstrapOptions {
4405                        no_scan: true,
4406                        force_rescan: options.force_rescan,
4407                        text_index_max_bytes: options.text_index_max_bytes,
4408                    },
4409                )?;
4410                report.scan = InitScanPhase {
4411                    status: InitPhaseStatus::Verified,
4412                    requested: true,
4413                    force_rescan: options.force_rescan,
4414                    report: Some(*scan),
4415                    error: None,
4416                };
4417                report.next_steps =
4418                    init_next_steps(false, false, report.purpose_handoff.queue.total);
4419                report.hydration = Some(hydration);
4420                report
4421            }
4422            McpWorktreeHydration::Fallback(reason) => {
4423                let mut report =
4424                    run_init_bootstrap(&state.root, &state.db_path, Some(config_path), options)?;
4425                report.hydration = Some(InitHydrationPhase {
4426                    status: InitHydrationStatus::Fallback,
4427                    source_root: lossless_project_root_display(&self.control_state.root),
4428                    source_project_instance_id: None,
4429                    target_project_instance_id: None,
4430                    baseline_generation: None,
4431                    reconciled_generation: None,
4432                    fallback_reason: Some(reason),
4433                });
4434                report
4435            }
4436        };
4437        report.project_dir.status = if project_dir_existed {
4438            InitPhaseStatus::Exists
4439        } else {
4440            InitPhaseStatus::Created
4441        };
4442        report.config.status = if config_existed {
4443            InitPhaseStatus::Exists
4444        } else {
4445            InitPhaseStatus::Created
4446        };
4447        report.nonsource_files.status = if nonsource_existed {
4448            InitPhaseStatus::Exists
4449        } else {
4450            InitPhaseStatus::Created
4451        };
4452        report.db.status = InitPhaseStatus::Created;
4453        if report.ok {
4454            self.bind_initialized_worktree(selection, state)?;
4455        }
4456        Ok(report)
4457    }
4458
4459    /// Build, reconcile, and no-clobber activate one private hydration candidate.
4460    fn attempt_worktree_hydration(
4461        &self,
4462        state: &McpProjectState,
4463        config_path: &Path,
4464        options: &InitBootstrapOptions,
4465    ) -> Result<McpWorktreeHydration, CliError> {
4466        let selection = state
4467            .worktree
4468            .as_ref()
4469            .filter(|selection| selection.registration_id.is_some())
4470            .ok_or_else(|| CliError::InvalidInput(MCP_ERROR_FEDERATED_ALIAS_MISSING.to_string()))?;
4471        let source = match Self::open_read_store(&self.control_state) {
4472            Ok(source) => source,
4473            Err(error) if Self::hydration_can_fallback(&error) => {
4474                return Ok(McpWorktreeHydration::Fallback(error.to_string()));
4475            }
4476            Err(error) => return Err(error),
4477        };
4478        Self::require_captured_control_identity(state.worktree.as_ref(), &source)?;
4479        let symbol_options = SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, None, None);
4480        let control = index_work_control(&symbol_options);
4481        let mut candidate =
4482            match source.prepare_worktree_hydration(&state.root, &state.db_path, &control) {
4483                Ok(candidate) => candidate,
4484                Err(error) => {
4485                    let error = Self::hydration_db_error(error);
4486                    if Self::hydration_can_fallback(&error) {
4487                        return Ok(McpWorktreeHydration::Fallback(error.to_string()));
4488                    }
4489                    return Err(error);
4490                }
4491            };
4492        let candidate_path = candidate
4493            .path()
4494            .map_err(Self::hydration_db_error)?
4495            .to_path_buf();
4496        let mut target = open_atlas_store_for_project(&candidate_path, &state.root)?;
4497        let plan = ScanRuntimePlan::for_path_controlled(
4498            Some(config_path),
4499            &state.root,
4500            options.text_index_max_bytes,
4501            &control,
4502        )?;
4503        let (scan, source_unchanged) =
4504            reconcile_hydrated_index_controlled(&mut target, &plan, &symbol_options, &control)?;
4505        drop(target);
4506        if source_unchanged {
4507            candidate
4508                .accept_verified_source_state(&control)
4509                .map_err(Self::hydration_db_error)?;
4510        }
4511        drop(source);
4512        let candidate = candidate
4513            .prepare_activation(&control)
4514            .map_err(Self::hydration_db_error)?;
4515        let activation = match self
4516            .activate_registered_worktree_hydration(state, selection, candidate, &control)
4517        {
4518            Ok(activation) => activation,
4519            Err(CliError::Db(error @ DbError::WorktreeHydrationDestinationExists { .. })) => {
4520                return Ok(McpWorktreeHydration::Fallback(error.to_string()));
4521            }
4522            Err(error) => return Err(error),
4523        };
4524        Ok(McpWorktreeHydration::Activated {
4525            hydration: InitHydrationPhase {
4526                status: InitHydrationStatus::Hydrated,
4527                source_root: lossless_project_root_display(&self.control_state.root),
4528                source_project_instance_id: Some(activation.source_project_instance_id.to_string()),
4529                target_project_instance_id: Some(activation.target_project_instance_id.to_string()),
4530                baseline_generation: Some(activation.baseline_generation.get()),
4531                reconciled_generation: Some(activation.reconciled_generation.get()),
4532                fallback_reason: None,
4533            },
4534            scan: Box::new(scan),
4535        })
4536    }
4537
4538    /// Publish one prepared hydration candidate only for the still-exact registered lifecycle.
4539    fn activate_registered_worktree_hydration(
4540        &self,
4541        state: &McpProjectState,
4542        selection: &McpWorktreeSelection,
4543        candidate: PreparedWorktreeHydrationCandidate,
4544        work_control: &IndexWorkControl,
4545    ) -> Result<WorktreeHydrationActivation, CliError> {
4546        self.activate_registered_worktree_hydration_with_post_publication(
4547            state,
4548            selection,
4549            candidate,
4550            work_control,
4551            || Ok(()),
4552        )
4553    }
4554
4555    /// Publish and bind a verified candidate with one deterministic final-admission seam.
4556    fn activate_registered_worktree_hydration_with_post_publication<F>(
4557        &self,
4558        state: &McpProjectState,
4559        selection: &McpWorktreeSelection,
4560        candidate: PreparedWorktreeHydrationCandidate,
4561        work_control: &IndexWorkControl,
4562        post_publication: F,
4563    ) -> Result<WorktreeHydrationActivation, CliError>
4564    where
4565        F: FnOnce() -> Result<(), CliError>,
4566    {
4567        let alias = WorktreeAlias::parse(&selection.alias)?;
4568        let registration_id =
4569            selection
4570                .registration_id
4571                .ok_or_else(|| DbError::WorktreeRegistrationNotFound {
4572                    alias: selection.alias.clone(),
4573                })?;
4574        let control = Self::open_existing_mut_store(&self.control_state, &self.control_state)?;
4575        Self::require_captured_control_identity(Some(selection), &control)?;
4576        control.with_active_worktree_registration(registration_id, &alias, |guard| {
4577            if let Err(error) =
4578                require_registered_worktree_lifecycle(guard.registration(), &state.root)
4579            {
4580                return Ok(Err(error));
4581            }
4582            let activation = candidate.activate(work_control)?;
4583            if let Err(error) = post_publication() {
4584                return Ok(Err(error));
4585            }
4586            let target =
4587                match open_atlas_store_read_only_for_project(&activation.database, &state.root) {
4588                    Ok(target) => target,
4589                    Err(error) => return Ok(Err(error)),
4590                };
4591            let snapshot = match target.export_worktree_usage_snapshot() {
4592                Ok(snapshot) => snapshot,
4593                Err(error) => return Ok(Err(error.into())),
4594            };
4595            if let Err(error) =
4596                require_registered_worktree_lifecycle(guard.registration(), &state.root)
4597            {
4598                return Ok(Err(error));
4599            }
4600            if let Err(error) = require_current_worktree_usage_snapshot(
4601                &activation.database,
4602                &state.root,
4603                &snapshot,
4604            ) {
4605                return Ok(Err(error));
4606            }
4607            guard.bind_project_with_usage_snapshot(
4608                &state.root,
4609                activation.target_project_instance_id,
4610                &snapshot,
4611            )?;
4612            Ok(Ok(activation))
4613        })?
4614    }
4615
4616    /// Bind a successfully initialized alias to its exact local atlas identity.
4617    fn bind_initialized_worktree(
4618        &self,
4619        selection: &McpWorktreeSelection,
4620        state: &McpProjectState,
4621    ) -> Result<(), CliError> {
4622        drop(Self::open_registered_worktree_mut_store(
4623            state,
4624            &self.control_state,
4625            selection,
4626        )?);
4627        Ok(())
4628    }
4629
4630    /// Bind a legacy exact-path init when its root already has an active alias.
4631    fn bind_initialized_registration_for_root(
4632        &self,
4633        state: &McpProjectState,
4634    ) -> Result<(), CliError> {
4635        let Some(repository) = self.control_git_repository_if_present()? else {
4636            return Ok(());
4637        };
4638        let Some(entry) = repository
4639            .worktrees
4640            .iter()
4641            .find(|entry| Self::active_worktree_root(entry).is_some_and(|root| root == state.root))
4642        else {
4643            return Ok(());
4644        };
4645        let administrative_identity = git_administrative_identity(&entry.administrative_directory)?;
4646        let administrative_directory =
4647            CanonicalProjectRoot::from_path(&entry.administrative_directory)
4648                .map_err(|source| CliError::InvalidInput(source.to_string()))?;
4649        let control = Self::open_existing_mut_store(&self.control_state, &self.control_state)?;
4650        let control_project_instance_id = control.captured_project_binding()?.project_instance_id;
4651        let Some(registration) =
4652            control
4653                .worktree_registrations(false)?
4654                .into_iter()
4655                .find(|registration| {
4656                    registration.git_administrative_directory_identity == administrative_directory
4657                        && registration.git_administrative_identity == administrative_identity
4658                })
4659        else {
4660            return Ok(());
4661        };
4662        self.bind_initialized_worktree(
4663            &McpWorktreeSelection {
4664                alias: registration.alias.to_string(),
4665                registration_id: Some(registration.registration_id),
4666                project_instance_id: registration.project_instance_id,
4667                control_project_instance_id: Some(control_project_instance_id),
4668            },
4669            state,
4670        )
4671    }
4672
4673    /// Preserve cancellation/resource failures and fallback only for unusable source baselines.
4674    fn hydration_can_fallback(error: &CliError) -> bool {
4675        matches!(
4676            error,
4677            CliError::Db(
4678                DbError::WorktreeHydrationInvalid { .. }
4679                    | DbError::WorktreeHydrationDestinationExists { .. }
4680                    | DbError::WorktreeHydrationBackupBusy { .. }
4681                    | DbError::GraphPublicationUnavailable
4682                    | DbError::GraphProjectIdentityMismatch { .. }
4683                    | DbError::DerivedSnapshotInvalid { .. }
4684                    | DbError::DerivedSnapshotLimit { .. }
4685                    | DbError::SchemaVersion { .. }
4686                    | DbError::SchemaVersionMissing
4687                    | DbError::SchemaShape { .. }
4688                    | DbError::IntegrityCheck { .. }
4689                    | DbError::ProjectRootMissing
4690                    | DbError::ProjectInstanceIdentityMissing
4691            )
4692        )
4693    }
4694
4695    /// Lift database-owned cancellation into the shared CLI work-control error.
4696    fn hydration_db_error(error: DbError) -> CliError {
4697        match error {
4698            DbError::IndexWork(failure) => failure.into(),
4699            error => error.into(),
4700        }
4701    }
4702
4703    /// Return the initialized selected project root used by admin-style MCP calls.
4704    fn admin_project_root(
4705        &self,
4706        project_path: Option<String>,
4707        worktree: Option<String>,
4708    ) -> Result<McpProjectState, CliError> {
4709        self.state_for_target(project_path, worktree)
4710    }
4711
4712    /// Parse an MCP ignore kind parameter.
4713    fn parse_ignore_kind(
4714        kind: Option<&str>,
4715        required: bool,
4716    ) -> Result<Option<IgnoreEntryKind>, CliError> {
4717        let Some(kind) = kind.map(str::trim).filter(|value| !value.is_empty()) else {
4718            return if required {
4719                Err(CliError::InvalidInput(
4720                    MCP_ERROR_IGNORE_KIND_REQUIRED.to_string(),
4721                ))
4722            } else {
4723                Ok(None)
4724            };
4725        };
4726        match kind {
4727            MCP_IGNORE_KIND_DIR_NAME | MCP_IGNORE_KIND_DIR_NAME_ALIAS => {
4728                Ok(Some(IgnoreEntryKind::DirName))
4729            }
4730            MCP_IGNORE_KIND_PATH_PREFIX | MCP_IGNORE_KIND_PATH_PREFIX_ALIAS => {
4731                Ok(Some(IgnoreEntryKind::PathPrefix))
4732            }
4733            other => Err(CliError::InvalidInput(Self::invalid_parameter_message(
4734                MCP_ERROR_INVALID_IGNORE_KIND_PREFIX,
4735                other,
4736                MCP_ERROR_INVALID_IGNORE_KIND_SUFFIX,
4737            ))),
4738        }
4739    }
4740
4741    /// Parse an MCP purpose lint level parameter.
4742    fn parse_purpose_lint_level(value: Option<&str>) -> Result<PurposeLintLevel, CliError> {
4743        match value.map(str::trim).filter(|value| !value.is_empty()) {
4744            None | Some(MCP_PURPOSE_LEVEL_LOW) => Ok(PurposeLintLevel::Low),
4745            Some(MCP_PURPOSE_LEVEL_MEDIUM) => Ok(PurposeLintLevel::Medium),
4746            Some(MCP_PURPOSE_LEVEL_STRICT) => Ok(PurposeLintLevel::Strict),
4747            Some(other) => Err(CliError::InvalidInput(Self::invalid_parameter_message(
4748                MCP_ERROR_INVALID_PURPOSE_LEVEL_PREFIX,
4749                other,
4750                MCP_ERROR_INVALID_PURPOSE_LEVEL_SUFFIX,
4751            ))),
4752        }
4753    }
4754
4755    /// Parse an MCP harness config parameter.
4756    fn parse_harness_config(value: Option<&str>) -> Result<HarnessConfig, CliError> {
4757        match value.map(str::trim).filter(|value| !value.is_empty()) {
4758            None | Some(MCP_HARNESS_MCP_JSON | MCP_HARNESS_MCP_JSON_ALIAS) => {
4759                Ok(HarnessConfig::McpJson)
4760            }
4761            Some(MCP_HARNESS_CODEX) => Ok(HarnessConfig::Codex),
4762            Some(MCP_HARNESS_CLAUDE_CODE | MCP_HARNESS_CLAUDE_CODE_ALIAS) => {
4763                Ok(HarnessConfig::ClaudeCode)
4764            }
4765            Some(MCP_HARNESS_OPENCODE) => Ok(HarnessConfig::OpenCode),
4766            Some(other) => Err(CliError::InvalidInput(Self::invalid_parameter_message(
4767                MCP_ERROR_INVALID_HARNESS_PREFIX,
4768                other,
4769                MCP_ERROR_INVALID_HARNESS_SUFFIX,
4770            ))),
4771        }
4772    }
4773
4774    /// Parse the optional token chart theme parameter.
4775    fn parse_token_chart_theme(value: Option<&str>) -> Result<TokenDashboardTheme, CliError> {
4776        match value.map(str::trim).filter(|value| !value.is_empty()) {
4777            None => Ok(TokenDashboardTheme::Dark),
4778            Some(theme) => TokenDashboardTheme::parse(theme).ok_or_else(|| {
4779                CliError::InvalidInput(Self::invalid_parameter_message(
4780                    TOKEN_CHART_THEME_ERROR_PREFIX,
4781                    theme,
4782                    TOKEN_CHART_THEME_ERROR_SUFFIX,
4783                ))
4784            }),
4785        }
4786    }
4787
4788    /// Build an invalid-parameter diagnostic from centralized fragments.
4789    fn invalid_parameter_message(prefix: &str, value: &str, suffix: &str) -> String {
4790        let mut message = String::with_capacity(prefix.len() + value.len() + suffix.len());
4791        message.push_str(prefix);
4792        message.push_str(value);
4793        message.push_str(suffix);
4794        message
4795    }
4796
4797    /// Build a compatibility map report, writing the map unless CI skip policy applies.
4798    fn build_map_report(
4799        state: &McpProjectState,
4800        json: bool,
4801        force: bool,
4802    ) -> Result<McpMapReport, CliError> {
4803        let config = Self::load_config_for_state(state)?;
4804        let skipped_reason = if !force
4805            && (crate::truthy_env(MCP_ENV_CI) || crate::truthy_env(MCP_ENV_GITHUB_ACTIONS))
4806        {
4807            Some(MCP_MAP_SKIPPED_IN_CI_REASON.to_string())
4808        } else {
4809            write_map(&config, json)?;
4810            None
4811        };
4812        Ok(McpMapReport {
4813            root: lossless_project_root_display(&config.root),
4814            map_path: lossless_native_path_display(&config.map_path),
4815            written: skipped_reason.is_none(),
4816            json,
4817            skipped_reason,
4818        })
4819    }
4820
4821    /// Build typed MCP session capabilities from active server state.
4822    fn session_capabilities(
4823        &self,
4824        state: &McpProjectState,
4825        text_index_max_bytes: u64,
4826    ) -> McpSessionCapabilities {
4827        McpSessionCapabilities {
4828            runtime: build_runtime_info(),
4829            selected_project: Self::selected_project_capability(state),
4830            startup_policy: McpStartupPolicy {
4831                nearest_project: Self::policy_state(self.allow_nearest_project),
4832            },
4833            path_scope: self.path_scope(),
4834            scan_policy: McpScanPolicy {
4835                implicit_scan: McpPolicyState::Disabled,
4836                text_index_max_bytes,
4837            },
4838            classified_navigation: classified_navigation_capabilities(),
4839            telemetry: McpTelemetryPolicy {
4840                mode: Self::policy_state(!telemetry_disabled()),
4841            },
4842            privacy: McpPrivacyPolicy {
4843                environment_dump: false,
4844                secret_values: false,
4845                projectatlas_paths_only: true,
4846            },
4847        }
4848    }
4849
4850    /// Encode settings plus additive MCP-session capability fields.
4851    fn render_settings_with_capabilities(
4852        &self,
4853        state: &McpProjectState,
4854    ) -> Result<String, CliError> {
4855        let report = build_settings_report(
4856            &state.db_path,
4857            state.config_path.as_deref(),
4858            OutputFormat::Toon,
4859        )?;
4860        let capabilities = self.session_capabilities(state, report.text_index_max_bytes);
4861        let rendered = Self::encode_two_named_payloads(
4862            MCP_PAYLOAD_SETTINGS,
4863            &report,
4864            MCP_PAYLOAD_SESSION_CAPABILITIES,
4865            &capabilities,
4866        )?;
4867        if rendered.len() > MCP_SETTINGS_RESPONSE_MAX_BYTES {
4868            let mut message = MCP_SETTINGS_RESPONSE_LIMIT_PREFIX.to_string();
4869            message.push_str(&rendered.len().to_string());
4870            message.push_str(MCP_SETTINGS_RESPONSE_LIMIT_SEPARATOR);
4871            message.push_str(&MCP_SETTINGS_RESPONSE_MAX_BYTES.to_string());
4872            message.push_str(MCP_SETTINGS_RESPONSE_LIMIT_SUFFIX);
4873            return Err(CliError::InvalidInput(message));
4874        }
4875        Ok(rendered)
4876    }
4877
4878    /// Build the selected-project capability row.
4879    fn selected_project_capability(state: &McpProjectState) -> McpSelectedProjectCapability {
4880        McpSelectedProjectCapability {
4881            worktree: state
4882                .worktree
4883                .as_ref()
4884                .map(|selection| selection.alias.clone()),
4885            registration_id: state
4886                .worktree
4887                .as_ref()
4888                .and_then(|selection| selection.registration_id),
4889            root: lossless_project_root_display(&state.root),
4890            db: lossless_native_path_display(&state.db_path),
4891            config: state
4892                .config_path
4893                .as_ref()
4894                .and_then(|path| lossless_native_path_display(path)),
4895            index_status: if state.db_path.exists() {
4896                McpIndexStatus::Available
4897            } else {
4898                McpIndexStatus::Missing
4899            },
4900        }
4901    }
4902
4903    /// Return the path scope for this MCP server.
4904    fn path_scope(&self) -> McpPathScope {
4905        if self.allow_nearest_project {
4906            McpPathScope::NearestIndexedProject
4907        } else {
4908            McpPathScope::SelectedProject
4909        }
4910    }
4911
4912    /// Convert a bool into a serialized policy state.
4913    fn policy_state(enabled: bool) -> McpPolicyState {
4914        if enabled {
4915            McpPolicyState::Enabled
4916        } else {
4917            McpPolicyState::Disabled
4918        }
4919    }
4920
4921    /// Clamp a caller-provided brief limit to the supported range.
4922    fn brief_limit(value: Option<usize>) -> usize {
4923        value
4924            .unwrap_or(SESSION_BRIEF_DEFAULT_LIMIT)
4925            .clamp(1, SESSION_BRIEF_MAX_LIMIT)
4926    }
4927
4928    /// Build a read-only agent startup brief.
4929    fn build_session_brief(
4930        &self,
4931        params: AtlasSessionBriefParams,
4932        context: Option<RequestContext<RoleServer>>,
4933    ) -> Result<McpSessionBrief, CliError> {
4934        let selected_project_path = params.project_path.clone();
4935        let selected_worktree = params.worktree.clone();
4936        let state =
4937            self.state_for_target(selected_project_path.clone(), selected_worktree.clone())?;
4938        let query = Self::query_or_empty(params.query);
4939        let purpose_task = params
4940            .purpose_task
4941            .unwrap_or_else(|| MCP_PURPOSE_TASK_SESSION_STARTUP.to_string());
4942        let folder_limit = Self::brief_limit(params.folder_limit);
4943        let file_limit = Self::brief_limit(params.file_limit);
4944        let blocker_limit = Self::brief_limit(params.blocker_limit);
4945        let purpose_limit = Self::brief_limit(params.purpose_limit);
4946        let project = Self::selected_project_capability(&state);
4947        if !state.db_path.exists() {
4948            let init_project_path = selected_worktree
4949                .is_none()
4950                .then(|| lossless_project_root_display(&state.root))
4951                .flatten();
4952            return Ok(McpSessionBrief {
4953                project,
4954                policy: self.brief_policy(),
4955                overview: None,
4956                folders: Vec::new(),
4957                files: Vec::new(),
4958                blockers: McpBriefBlockers {
4959                    total: 0,
4960                    returned: 0,
4961                    truncated: false,
4962                    items: Vec::new(),
4963                },
4964                purpose_handoff: None,
4965                recommendations: Self::missing_index_recommendations(
4966                    init_project_path,
4967                    selected_worktree,
4968                ),
4969                limits: McpBriefLimits {
4970                    folder_limit,
4971                    file_limit,
4972                    blocker_limit,
4973                    purpose_limit,
4974                    folders_truncated: false,
4975                    files_truncated: false,
4976                    purposes_truncated: false,
4977                },
4978            });
4979        }
4980        let outcome = self.with_fresh_store_for_request(&state, context, |store, _stamp| {
4981            let overview = store.overview()?;
4982            let folder_rows =
4983                ranked_folder_nodes_with_reasons(store, &query, folder_limit.saturating_add(1))?;
4984            let file_rows = ranked_file_nodes_with_reasons(
4985                store,
4986                &query,
4987                None,
4988                None,
4989                file_limit.saturating_add(1),
4990                false,
4991            )?;
4992            let blockers = Self::brief_blockers(store, blocker_limit)?;
4993            let purpose_query = HealthQuery {
4994                start_index: 0,
4995                limit: purpose_limit,
4996                category: None,
4997                severity: None,
4998                path_prefix: None,
4999                summary_only: false,
5000                scope: HealthScope::purpose_default(),
5001            };
5002            let purpose_queue = purpose_curation_page(store, &purpose_query, &purpose_task)?;
5003            let purposes_truncated = purpose_queue.truncated;
5004            let folders_truncated = folder_rows.len() > folder_limit;
5005            let files_truncated = file_rows.len() > file_limit;
5006            let next_navigation_call = file_rows.first().map(|row| row.next_call.clone());
5007            Ok(McpSessionBrief {
5008                project: Self::selected_project_capability(&state),
5009                policy: self.brief_policy(),
5010                overview: Some(overview),
5011                folders: folder_rows
5012                    .into_iter()
5013                    .take(folder_limit)
5014                    .map(Self::brief_candidate)
5015                    .collect(),
5016                files: file_rows
5017                    .into_iter()
5018                    .take(file_limit)
5019                    .map(Self::brief_candidate)
5020                    .collect(),
5021                recommendations: Self::indexed_project_recommendations(
5022                    &query,
5023                    next_navigation_call,
5024                    blockers.total,
5025                    blocker_limit,
5026                    selected_project_path.clone(),
5027                    selected_worktree.clone(),
5028                ),
5029                blockers,
5030                purpose_handoff: purpose_queue
5031                    .actionable
5032                    .then(|| purpose_curator_handoff(purpose_queue)),
5033                limits: McpBriefLimits {
5034                    folder_limit,
5035                    file_limit,
5036                    blocker_limit,
5037                    purpose_limit,
5038                    folders_truncated,
5039                    files_truncated,
5040                    purposes_truncated,
5041                },
5042            })
5043        })?;
5044        Ok(outcome.value)
5045    }
5046
5047    /// Build brief policy fields.
5048    fn brief_policy(&self) -> McpBriefPolicy {
5049        McpBriefPolicy {
5050            nearest_project: Self::policy_state(self.allow_nearest_project),
5051            path_scope: self.path_scope(),
5052        }
5053    }
5054
5055    /// Build the additive compact projection without changing legacy defaults or query behavior.
5056    fn build_compact_session_brief(
5057        &self,
5058        mut params: AtlasSessionBriefParams,
5059        context: Option<RequestContext<RoleServer>>,
5060    ) -> Result<McpCompactSessionBrief, CliError> {
5061        let project_path = params.project_path.clone();
5062        let worktree = params.worktree.clone();
5063        params.compact = None;
5064        params
5065            .folder_limit
5066            .get_or_insert(COMPACT_SESSION_BRIEF_DEFAULT_LIMIT);
5067        params
5068            .file_limit
5069            .get_or_insert(COMPACT_SESSION_BRIEF_DEFAULT_LIMIT);
5070        params
5071            .blocker_limit
5072            .get_or_insert(COMPACT_SESSION_BRIEF_DEFAULT_LIMIT);
5073        params
5074            .purpose_limit
5075            .get_or_insert(COMPACT_SESSION_BRIEF_DEFAULT_LIMIT);
5076        let brief = self.build_session_brief(params, context)?;
5077        Ok(Self::compact_session_brief(
5078            &brief,
5079            project_path.as_deref(),
5080            worktree.as_deref(),
5081        ))
5082    }
5083
5084    /// Project the compatibility report into the explicit compact response shape.
5085    fn compact_session_brief(
5086        brief: &McpSessionBrief,
5087        project_path: Option<&str>,
5088        worktree: Option<&str>,
5089    ) -> McpCompactSessionBrief {
5090        let omit_folders = !brief.files.is_empty();
5091        let folders_truncated =
5092            brief.limits.folders_truncated || (omit_folders && !brief.folders.is_empty());
5093        let compact_limits = McpCompactBriefLimits {
5094            folder_limit: brief.limits.folder_limit,
5095            file_limit: brief.limits.file_limit,
5096            blocker_limit: brief.limits.blocker_limit,
5097            purpose_limit: brief.limits.purpose_limit,
5098            folders_truncated,
5099            files_truncated: brief.limits.files_truncated,
5100            purposes_truncated: brief.limits.purposes_truncated,
5101        };
5102        let limits_are_default = is_compact_brief_default_limit(&compact_limits.folder_limit)
5103            && is_compact_brief_default_limit(&compact_limits.file_limit)
5104            && is_compact_brief_default_limit(&compact_limits.blocker_limit)
5105            && is_compact_brief_default_limit(&compact_limits.purpose_limit)
5106            && !compact_limits.folders_truncated
5107            && !compact_limits.files_truncated
5108            && !compact_limits.purposes_truncated;
5109        McpCompactSessionBrief {
5110            project: McpCompactBriefProject {
5111                worktree: brief.project.worktree.clone(),
5112                registration_id: brief.project.registration_id,
5113                root: brief.project.root.clone(),
5114                index_status: brief.project.index_status,
5115            },
5116            policy: (!brief.policy.is_default()).then_some(brief.policy),
5117            overview: brief
5118                .overview
5119                .as_ref()
5120                .map(|overview| McpCompactBriefOverview {
5121                    files: overview.files,
5122                    folders: overview.folders,
5123                }),
5124            folders: if omit_folders {
5125                Vec::new()
5126            } else {
5127                brief
5128                    .folders
5129                    .iter()
5130                    .map(Self::compact_brief_candidate)
5131                    .collect()
5132            },
5133            files: brief
5134                .files
5135                .iter()
5136                .map(Self::compact_brief_candidate)
5137                .collect(),
5138            blockers: (brief.blockers.total > 0).then_some(McpCompactBriefBlockers {
5139                total: brief.blockers.total,
5140            }),
5141            purpose_handoff: brief.purpose_handoff.as_ref().map(|handoff| {
5142                Self::compact_brief_purpose_handoff(
5143                    handoff,
5144                    project_path.map(ToString::to_string),
5145                    worktree.map(ToString::to_string),
5146                )
5147            }),
5148            recommendations: brief
5149                .recommendations
5150                .iter()
5151                .map(Self::compact_brief_recommendation)
5152                .collect(),
5153            limits: (!limits_are_default).then_some(compact_limits),
5154        }
5155    }
5156
5157    /// Convert a ranked node into the compatibility-preserving startup candidate.
5158    fn brief_candidate(row: RankedNode) -> McpBriefCandidate {
5159        let purpose_agent_reviewed = row.node.purpose.agent_reviewed();
5160        McpBriefCandidate {
5161            path: row.node.node.path,
5162            kind: row.node.node.kind.to_string(),
5163            purpose_status: row.node.purpose.status,
5164            purpose_source: row.node.purpose.source,
5165            purpose_agent_reviewed,
5166            purpose: row.node.purpose.purpose,
5167            summary: row.node.summary,
5168            reasons: row.reasons,
5169            reason_codes: row.reason_codes,
5170            connection_counts: row.connection_counts,
5171            connections: row.connections,
5172            connections_truncated: row.connections_truncated,
5173            next_call: row.next_call,
5174        }
5175    }
5176
5177    /// Project one compatibility candidate into the bounded compact shape.
5178    fn compact_brief_candidate(row: &McpBriefCandidate) -> McpCompactBriefCandidate {
5179        let connections = row
5180            .connections
5181            .iter()
5182            .find(|connection| Self::brief_connection_is_crisp(connection))
5183            .cloned()
5184            .into_iter()
5185            .collect::<Vec<_>>();
5186        let next_call = if row.next_call.capability == NavigationNextCapability::Relations {
5187            NavigationNextCall {
5188                capability: NavigationNextCapability::Summary,
5189                path: row.path.clone(),
5190            }
5191        } else {
5192            row.next_call.clone()
5193        };
5194        McpCompactBriefCandidate {
5195            path: row.path.clone(),
5196            purpose_status: (!row.purpose_agent_reviewed).then_some(row.purpose_status),
5197            purpose_source: (!row.purpose_agent_reviewed).then_some(row.purpose_source),
5198            purpose_agent_reviewed: row.purpose_agent_reviewed,
5199            purpose: row.purpose.clone(),
5200            connections_truncated: row.connections_truncated
5201                || connections.len() < row.connections.len(),
5202            connections,
5203            next_call,
5204        }
5205    }
5206
5207    /// Prefer a resolved non-import edge as the single default startup connection sample.
5208    fn brief_connection_is_crisp(connection: &RankedConnection) -> bool {
5209        connection.kind != RankedConnectionKind::Import
5210            && !matches!(
5211                &connection.target,
5212                RankedConnectionTarget::Unresolved { .. }
5213            )
5214    }
5215
5216    /// Project an actionable handoff into one exact follow-up call instead of duplicating rows.
5217    fn compact_brief_purpose_handoff(
5218        handoff: &PurposeCuratorHandoff,
5219        project_path: Option<String>,
5220        worktree: Option<String>,
5221    ) -> McpCompactBriefPurposeHandoff {
5222        McpCompactBriefPurposeHandoff {
5223            agent_harness_expected: handoff.agent_harness_expected,
5224            recommended_subagent_reasoning: handoff.recommended_subagent_reasoning,
5225            instructions: handoff.instructions.first().cloned().into_iter().collect(),
5226            main_agent_fallback: handoff.main_agent_fallback,
5227            server_started_curator: handoff.server_started_curator,
5228            silent_on_success: handoff.silent_on_success,
5229            truncated: handoff.queue.truncated,
5230            next_call: McpCompactBriefRecommendation {
5231                kind: McpBriefRecommendationKind::PurposeQueue,
5232                target: MCP_TOOL_ATLAS_PURPOSE_QUEUE.to_string(),
5233                reason: MCP_BRIEF_REASON_PURPOSE_QUEUE.to_string(),
5234                arguments: Self::brief_call_arguments(
5235                    project_path,
5236                    worktree,
5237                    &[(MCP_BRIEF_ARG_TASK, &handoff.queue.task)],
5238                    Some((MCP_BRIEF_ARG_LIMIT, handoff.queue.limit)),
5239                ),
5240            },
5241        }
5242    }
5243
5244    /// Add compact opt-ins to one legacy recommendation without changing its source report.
5245    fn compact_brief_recommendation(
5246        recommendation: &McpBriefRecommendation,
5247    ) -> McpCompactBriefRecommendation {
5248        let relation_to_summary =
5249            matches!(recommendation.kind, McpBriefRecommendationKind::Relations);
5250        let mut arguments = recommendation.arguments.clone();
5251        if let Some(object) = arguments.as_object_mut() {
5252            if relation_to_summary {
5253                object.remove(MCP_BRIEF_ARG_VIEW);
5254            }
5255            if relation_to_summary
5256                || matches!(recommendation.kind, McpBriefRecommendationKind::Summary)
5257            {
5258                object.insert(
5259                    MCP_BRIEF_ARG_COMPACT.to_string(),
5260                    serde_json::Value::Bool(true),
5261                );
5262            }
5263        }
5264        McpCompactBriefRecommendation {
5265            kind: if relation_to_summary {
5266                McpBriefRecommendationKind::Summary
5267            } else {
5268                recommendation.kind
5269            },
5270            target: if relation_to_summary {
5271                MCP_TOOL_ATLAS_FILE_SUMMARY.to_string()
5272            } else {
5273                recommendation.target.clone()
5274            },
5275            reason: if relation_to_summary {
5276                MCP_BRIEF_REASON_RANKED_FILE_SUMMARY.to_string()
5277            } else {
5278                recommendation.reason.clone()
5279            },
5280            arguments,
5281        }
5282    }
5283
5284    /// Build bounded health blockers for a session brief.
5285    fn brief_blockers(
5286        store: &AtlasStore,
5287        blocker_limit: usize,
5288    ) -> Result<McpBriefBlockers, CliError> {
5289        let query = HealthQuery {
5290            start_index: 0,
5291            limit: blocker_limit,
5292            category: None,
5293            severity: None,
5294            path_prefix: None,
5295            summary_only: false,
5296            scope: HealthScope::all(),
5297        };
5298        let page = store.unresolved_health_findings_page_current(&query)?;
5299        let total = page.total;
5300        let returned = page.returned;
5301        Ok(McpBriefBlockers {
5302            total,
5303            returned,
5304            truncated: returned < total,
5305            items: page
5306                .findings
5307                .into_iter()
5308                .map(|finding| McpBriefBlocker {
5309                    id: finding.id,
5310                    severity: finding.severity,
5311                    category: finding.category,
5312                    path: finding.path,
5313                    related_path: finding.related_path,
5314                    message: finding.message,
5315                    recommendation: finding.recommendation,
5316                })
5317                .collect(),
5318        })
5319    }
5320
5321    /// Recommend next calls for a missing index.
5322    fn missing_index_recommendations(
5323        project_path: Option<String>,
5324        worktree: Option<String>,
5325    ) -> Vec<McpBriefRecommendation> {
5326        vec![
5327            McpBriefRecommendation {
5328                kind: McpBriefRecommendationKind::Init,
5329                target: MCP_TOOL_ATLAS_INIT.to_string(),
5330                reason: MCP_BRIEF_REASON_SELECTED_INDEX_MISSING.to_string(),
5331                arguments: Self::target_arguments(project_path.clone(), worktree.clone()),
5332            },
5333            McpBriefRecommendation {
5334                kind: McpBriefRecommendationKind::FilesystemTools,
5335                target: MCP_BRIEF_TARGET_FILESYSTEM_TOOLS.to_string(),
5336                reason: MCP_BRIEF_REASON_FILESYSTEM_UNTIL_INDEX.to_string(),
5337                arguments: Self::target_arguments(project_path, worktree),
5338            },
5339        ]
5340    }
5341
5342    /// Recommend next calls for an indexed project.
5343    fn indexed_project_recommendations(
5344        query: &str,
5345        next_navigation_call: Option<NavigationNextCall>,
5346        blocker_total: usize,
5347        blocker_limit: usize,
5348        project_path: Option<String>,
5349        worktree: Option<String>,
5350    ) -> Vec<McpBriefRecommendation> {
5351        let mut recommendations = match next_navigation_call {
5352            Some(next_call) if next_call.capability == NavigationNextCapability::Summary => {
5353                vec![McpBriefRecommendation {
5354                    kind: McpBriefRecommendationKind::Summary,
5355                    target: MCP_TOOL_ATLAS_FILE_SUMMARY.to_string(),
5356                    reason: MCP_BRIEF_REASON_RANKED_FILE_SUMMARY.to_string(),
5357                    arguments: Self::brief_call_arguments(
5358                        project_path.clone(),
5359                        worktree.clone(),
5360                        &[(MCP_BRIEF_ARG_FILE, &next_call.path)],
5361                        None,
5362                    ),
5363                }]
5364            }
5365            Some(next_call) if next_call.capability == NavigationNextCapability::Relations => {
5366                vec![McpBriefRecommendation {
5367                    kind: McpBriefRecommendationKind::Relations,
5368                    target: MCP_TOOL_ATLAS_SYMBOL_RELATIONS.to_string(),
5369                    reason: MCP_BRIEF_REASON_RANKED_FILE_RELATIONS.to_string(),
5370                    arguments: Self::brief_call_arguments(
5371                        project_path.clone(),
5372                        worktree.clone(),
5373                        &[
5374                            (MCP_BRIEF_ARG_FILE, &next_call.path),
5375                            (MCP_BRIEF_ARG_VIEW, MCP_SYMBOL_RELATION_VIEW_DETAILED),
5376                        ],
5377                        None,
5378                    ),
5379                }]
5380            }
5381            _ if !query.trim().is_empty() => vec![McpBriefRecommendation {
5382                kind: McpBriefRecommendationKind::Search,
5383                target: MCP_TOOL_ATLAS_SEARCH.to_string(),
5384                reason: MCP_BRIEF_REASON_SEARCH_FALLBACK.to_string(),
5385                arguments: Self::brief_call_arguments(
5386                    project_path.clone(),
5387                    worktree.clone(),
5388                    &[(MCP_BRIEF_ARG_PATTERN, query)],
5389                    None,
5390                ),
5391            }],
5392            _ => vec![McpBriefRecommendation {
5393                kind: McpBriefRecommendationKind::FilesystemTools,
5394                target: MCP_BRIEF_TARGET_FILESYSTEM_TOOLS.to_string(),
5395                reason: MCP_BRIEF_REASON_NO_FILE_CANDIDATE.to_string(),
5396                arguments: Self::target_arguments(project_path.clone(), worktree.clone()),
5397            }],
5398        };
5399        if blocker_total > 0 {
5400            recommendations.push(McpBriefRecommendation {
5401                kind: McpBriefRecommendationKind::Health,
5402                target: MCP_TOOL_ATLAS_HEALTH.to_string(),
5403                reason: MCP_BRIEF_REASON_HEALTH_BLOCKERS.to_string(),
5404                arguments: Self::brief_call_arguments(
5405                    project_path,
5406                    worktree,
5407                    &[],
5408                    Some((MCP_BRIEF_ARG_LIMIT, blocker_limit)),
5409                ),
5410            });
5411        }
5412        recommendations
5413    }
5414
5415    /// Build a `JSON` object containing one mutually exclusive root selector.
5416    fn target_arguments(
5417        project_path: Option<String>,
5418        worktree: Option<String>,
5419    ) -> serde_json::Value {
5420        let mut arguments = serde_json::Map::new();
5421        if let Some(path) = project_path {
5422            arguments.insert(
5423                MCP_BRIEF_ARG_PROJECT_PATH.to_string(),
5424                serde_json::Value::String(path),
5425            );
5426        } else if let Some(alias) = worktree {
5427            arguments.insert(
5428                MCP_BRIEF_ARG_WORKTREE.to_string(),
5429                serde_json::Value::String(alias),
5430            );
5431        }
5432        serde_json::Value::Object(arguments)
5433    }
5434
5435    /// Build recommendation call arguments with optional project path and one payload argument.
5436    fn brief_call_arguments(
5437        project_path: Option<String>,
5438        worktree: Option<String>,
5439        string_args: &[(&'static str, &str)],
5440        usize_arg: Option<(&'static str, usize)>,
5441    ) -> serde_json::Value {
5442        let mut arguments = serde_json::Map::new();
5443        if let Some(path) = project_path {
5444            arguments.insert(
5445                MCP_BRIEF_ARG_PROJECT_PATH.to_string(),
5446                serde_json::Value::String(path),
5447            );
5448        } else if let Some(alias) = worktree {
5449            arguments.insert(
5450                MCP_BRIEF_ARG_WORKTREE.to_string(),
5451                serde_json::Value::String(alias),
5452            );
5453        }
5454        for (key, value) in string_args {
5455            arguments.insert(
5456                (*key).to_string(),
5457                serde_json::Value::String((*value).to_string()),
5458            );
5459        }
5460        if let Some((key, value)) = usize_arg {
5461            arguments.insert(key.to_string(), serde_json::json!(value));
5462        }
5463        serde_json::Value::Object(arguments)
5464    }
5465
5466    /// Return all task model states for contract discovery.
5467    fn task_state_values() -> Vec<McpTaskState> {
5468        vec![
5469            McpTaskState::Pending,
5470            McpTaskState::Running,
5471            McpTaskState::Complete,
5472            McpTaskState::Failed,
5473            McpTaskState::Canceled,
5474        ]
5475    }
5476
5477    /// Return all task operation values for contract discovery.
5478    fn task_operation_values() -> Vec<McpTaskOperation> {
5479        vec![
5480            McpTaskOperation::Contract,
5481            McpTaskOperation::Scan,
5482            McpTaskOperation::WatchOnce,
5483            McpTaskOperation::SymbolsBuild,
5484            McpTaskOperation::Search,
5485        ]
5486    }
5487
5488    /// Start one bounded session-local indexing task.
5489    fn start_index_task<F>(
5490        &self,
5491        operation: McpTaskOperation,
5492        options: SymbolBuildOptions,
5493        result_ref: &'static str,
5494        work: F,
5495    ) -> Result<McpTaskStartResponse, CliError>
5496    where
5497        F: FnOnce(&IndexWorkControl, SymbolBuildOptions) -> Result<(), CliError> + Send + 'static,
5498    {
5499        let options = options.with_worker_ceiling(self.background_resources.workers_per_task);
5500        let control = index_work_control(&options);
5501        let mut task_id = MCP_INDEX_TASK_ID_PREFIX.to_string();
5502        task_id.push_str(
5503            &self
5504                .next_task_sequence
5505                .fetch_add(1, Ordering::Relaxed)
5506                .to_string(),
5507        );
5508        let now = mcp_unix_time_ms();
5509        {
5510            let mut registry = self
5511                .task_registry
5512                .write()
5513                .map_err(|_poisoned| CliError::Mcp(MCP_PROJECT_STATE_LOCK_POISONED.to_string()))?;
5514            let active = registry.active_count();
5515            if active >= self.background_resources.task_limit {
5516                let mut message = MCP_INDEX_TASK_LIMIT_PREFIX.to_string();
5517                message.push_str(&self.background_resources.task_limit.to_string());
5518                message.push_str(MCP_INDEX_TASK_LIMIT_SUFFIX);
5519                return Err(CliError::Mcp(message));
5520            }
5521            registry.insert(McpTaskRecord {
5522                task_id: task_id.clone(),
5523                operation: operation.clone(),
5524                state: McpTaskState::Pending,
5525                created_at_ms: now,
5526                updated_at_ms: now,
5527                progress: Some(McpTaskProgress {
5528                    current: None,
5529                    total: None,
5530                    message: Some(MCP_TASK_PROGRESS_ACCEPTED.to_string()),
5531                }),
5532                error: None,
5533                result_ref: None,
5534                cancelable: true,
5535                control: Some(control.clone()),
5536            });
5537        }
5538
5539        let registry = Arc::clone(&self.task_registry);
5540        let worker_task_id = task_id.clone();
5541        let mut worker_name = MCP_INDEX_WORKER_NAME_PREFIX.to_string();
5542        worker_name.push_str(&task_id);
5543        let spawn_result = thread::Builder::new().name(worker_name).spawn(move || {
5544            if let Ok(mut registry) = registry.write() {
5545                registry.update(&worker_task_id, |record| {
5546                    record.state = McpTaskState::Running;
5547                    record.updated_at_ms = mcp_unix_time_ms();
5548                    record.progress = Some(McpTaskProgress {
5549                        current: None,
5550                        total: None,
5551                        message: Some(MCP_TASK_PROGRESS_RUNNING.to_string()),
5552                    });
5553                });
5554            }
5555            let outcome =
5556                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| work(&control, options)));
5557            let (state, progress, error, completed_result_ref) = match outcome {
5558                Ok(Ok(())) => (
5559                    McpTaskState::Complete,
5560                    MCP_TASK_PROGRESS_COMPLETE,
5561                    None,
5562                    Some(result_ref.to_string()),
5563                ),
5564                Ok(Err(error)) => {
5565                    let state = if task_error_is_canceled(&error) {
5566                        McpTaskState::Canceled
5567                    } else {
5568                        McpTaskState::Failed
5569                    };
5570                    (
5571                        state,
5572                        if state == McpTaskState::Canceled {
5573                            MCP_TASK_PROGRESS_CANCELED
5574                        } else {
5575                            MCP_TASK_PROGRESS_FAILED
5576                        },
5577                        Some(bounded_task_error(&error)),
5578                        None,
5579                    )
5580                }
5581                Err(_panic) => (
5582                    McpTaskState::Failed,
5583                    MCP_TASK_PROGRESS_FAILED,
5584                    Some(MCP_INDEX_WORKER_PANIC_ERROR.to_string()),
5585                    None,
5586                ),
5587            };
5588            if let Ok(mut registry) = registry.write() {
5589                registry.update(&worker_task_id, |record| {
5590                    record.state = state;
5591                    record.updated_at_ms = mcp_unix_time_ms();
5592                    record.progress = Some(McpTaskProgress {
5593                        current: None,
5594                        total: None,
5595                        message: Some(progress.to_string()),
5596                    });
5597                    record.error = error;
5598                    record.result_ref = completed_result_ref;
5599                    record.cancelable = false;
5600                    record.control = None;
5601                });
5602            }
5603        });
5604        if let Err(source) = spawn_result {
5605            let mut spawn_error = MCP_INDEX_WORKER_SPAWN_ERROR_PREFIX.to_string();
5606            spawn_error.push_str(&source.to_string());
5607            if let Ok(mut registry) = self.task_registry.write() {
5608                registry.update(&task_id, |record| {
5609                    record.state = McpTaskState::Failed;
5610                    record.updated_at_ms = mcp_unix_time_ms();
5611                    record.progress = Some(McpTaskProgress {
5612                        current: None,
5613                        total: None,
5614                        message: Some(MCP_TASK_PROGRESS_FAILED.to_string()),
5615                    });
5616                    record.error = Some(spawn_error.clone());
5617                    record.cancelable = false;
5618                    record.control = None;
5619                });
5620            }
5621            return Err(CliError::Mcp(spawn_error));
5622        }
5623
5624        Ok(McpTaskStartResponse {
5625            task_id,
5626            operation,
5627            state: McpTaskState::Pending,
5628            status_tool: MCP_TOOL_ATLAS_TASK_STATUS,
5629            cancel_tool: MCP_TOOL_ATLAS_TASK_CANCEL,
5630        })
5631    }
5632
5633    /// Look up one MCP task status.
5634    fn task_status(&self, task_id: String) -> Result<McpTaskStatusResponse, CliError> {
5635        let registry = self
5636            .task_registry
5637            .read()
5638            .map_err(|_poisoned| CliError::Mcp(MCP_TASK_REGISTRY_LOCK_POISONED.to_string()))?;
5639        let task = registry.get(&task_id);
5640        Ok(McpTaskStatusResponse {
5641            task_id,
5642            lookup: if task.is_some() {
5643                McpTaskLookupStatus::Found
5644            } else {
5645                McpTaskLookupStatus::NotFound
5646            },
5647            states: Self::task_state_values(),
5648            operations: Self::task_operation_values(),
5649            registry_capacity: MCP_TASK_REGISTRY_CAPACITY,
5650            task,
5651        })
5652    }
5653
5654    /// Cancel one MCP task when cancellation is supported.
5655    fn task_cancel(&self, task_id: String) -> Result<McpTaskCancelResponse, CliError> {
5656        let mut registry = self
5657            .task_registry
5658            .write()
5659            .map_err(|_poisoned| CliError::Mcp(MCP_TASK_REGISTRY_LOCK_POISONED.to_string()))?;
5660        let Some(record) = registry.get(&task_id) else {
5661            return Ok(McpTaskCancelResponse {
5662                task_id,
5663                result: McpTaskCancelResult::NotFound,
5664                registry_capacity: MCP_TASK_REGISTRY_CAPACITY,
5665                task: None,
5666            });
5667        };
5668        if matches!(
5669            record.state,
5670            McpTaskState::Complete | McpTaskState::Failed | McpTaskState::Canceled
5671        ) {
5672            return Ok(McpTaskCancelResponse {
5673                task_id,
5674                result: McpTaskCancelResult::AlreadyFinished,
5675                registry_capacity: MCP_TASK_REGISTRY_CAPACITY,
5676                task: Some(record),
5677            });
5678        }
5679        if !record.cancelable {
5680            return Ok(McpTaskCancelResponse {
5681                task_id,
5682                result: McpTaskCancelResult::NotCancelable,
5683                registry_capacity: MCP_TASK_REGISTRY_CAPACITY,
5684                task: Some(record),
5685            });
5686        }
5687        let task = registry.update(&task_id, |record| {
5688            if let Some(control) = &record.control {
5689                control.cancel();
5690            }
5691            record.updated_at_ms = mcp_unix_time_ms();
5692            record.progress = Some(McpTaskProgress {
5693                current: None,
5694                total: None,
5695                message: Some(MCP_TASK_PROGRESS_CANCELLATION_REQUESTED.to_string()),
5696            });
5697            record.cancelable = false;
5698        });
5699        Ok(McpTaskCancelResponse {
5700            task_id,
5701            result: McpTaskCancelResult::CancellationRequested,
5702            registry_capacity: MCP_TASK_REGISTRY_CAPACITY,
5703            task,
5704        })
5705    }
5706
5707    /// Build a CLI-compatible lint report for MCP callers.
5708    fn lint_report_for_state(
5709        state: &McpProjectState,
5710        params: &AtlasLintParams,
5711    ) -> Result<crate::runtime::LintReport, CliError> {
5712        let config = Self::load_config_for_state(state)?;
5713        let purpose_level = Self::parse_purpose_lint_level(params.purpose_level.as_deref())?;
5714        lint_project(
5715            &config,
5716            &state.db_path,
5717            state.config_path.as_deref(),
5718            LintOptions {
5719                strict_folders: params.strict_folders.unwrap_or(false),
5720                report_untracked: params.report_untracked.unwrap_or(false),
5721                strict_untracked: params.strict_untracked.unwrap_or(false),
5722            },
5723            purpose_level,
5724        )
5725    }
5726
5727    /// Build startup state from CLI-supplied DB/config paths.
5728    fn startup_project_state(db_path: PathBuf, config_path: Option<PathBuf>) -> McpProjectState {
5729        let root = Self::startup_project_root(&db_path, config_path.as_deref());
5730        let config_path = config_path.filter(|path| Self::config_matches_project_root(&root, path));
5731        McpProjectState {
5732            root,
5733            db_path,
5734            config_path,
5735            worktree: None,
5736        }
5737    }
5738
5739    /// Resolve startup root best-effort so server construction stays infallible.
5740    fn startup_project_root(db_path: &Path, config_path: Option<&Path>) -> PathBuf {
5741        if let Ok(root) = default_mcp_project_root(db_path, config_path) {
5742            return root;
5743        }
5744        if let Some(root) = Self::project_root_from_default_db_path(db_path) {
5745            return root;
5746        }
5747        std::env::current_dir()
5748            .ok()
5749            .and_then(|root| canonical_project_root(&root).ok())
5750            .unwrap_or_else(|| PathBuf::from(CURRENT_DIR_ALIAS))
5751    }
5752
5753    /// Infer a root from a conventional `root/.projectatlas/projectatlas.db` path.
5754    fn project_root_from_default_db_path(db_path: &Path) -> Option<PathBuf> {
5755        let atlas_dir = db_path.parent()?;
5756        if atlas_dir.file_name()? != PROJECTATLAS_DIR_NAME {
5757            return None;
5758        }
5759        let root = atlas_dir.parent()?;
5760        canonical_project_root(root)
5761            .ok()
5762            .or_else(|| Some(root.to_path_buf()))
5763    }
5764
5765    /// Read the active MCP project state.
5766    fn active_project_state(&self) -> Result<McpProjectState, CliError> {
5767        let state = self
5768            .project_state
5769            .read()
5770            .map(|state| state.clone())
5771            .map_err(|_poisoned| CliError::Mcp(MCP_PROJECT_STATE_LOCK_POISONED.to_string()))?;
5772        canonical_source_project_root(&state.root)?;
5773        Ok(state)
5774    }
5775
5776    /// Replace the active MCP project state.
5777    fn set_active_project_state(&self, state: McpProjectState) -> Result<(), CliError> {
5778        *self
5779            .project_state
5780            .write()
5781            .map_err(|_poisoned| CliError::Mcp(MCP_PROJECT_STATE_LOCK_POISONED.to_string()))? =
5782            state;
5783        Ok(())
5784    }
5785
5786    /// Return the bounded reciprocal Git inventory for the immutable control checkout.
5787    fn control_git_repository(&self) -> Result<GitRepositoryStructure, CliError> {
5788        self.control_git_repository_if_present()?.ok_or_else(|| {
5789            CliError::InvalidInput(MCP_ERROR_WORKTREE_CONTROL_REPOSITORY_REQUIRED.to_string())
5790        })
5791    }
5792
5793    /// Return the control Git inventory, or none for a true non-Git control root.
5794    fn control_git_repository_if_present(
5795        &self,
5796    ) -> Result<Option<GitRepositoryStructure>, CliError> {
5797        let repository = match discover_repository_structure(&self.control_state.root)? {
5798            RepositoryStructure::Git(repository) => repository,
5799            RepositoryStructure::NonGit { .. } => return Ok(None),
5800            RepositoryStructure::InvalidGit { issue, .. } => {
5801                return Err(CliError::InvalidInput(format!(
5802                    "invalid control-repository Git evidence at '{}': {:?}",
5803                    normalize_native_path_display(issue.path),
5804                    issue.kind
5805                )));
5806            }
5807        };
5808        let control_present = repository.worktrees.iter().any(|entry| {
5809            matches!(
5810                &entry.state,
5811                GitWorktreeState::Active { root, .. } if root == &self.control_state.root
5812            )
5813        });
5814        if !control_present {
5815            return Err(CliError::InvalidInput(format!(
5816                "selected control root '{}' is not one active worktree in its reciprocal Git inventory",
5817                normalize_native_path_display(&self.control_state.root)
5818            )));
5819        }
5820        Ok(Some(repository))
5821    }
5822
5823    /// Derive one stable, short candidate selector from Git administrative identity.
5824    fn worktree_candidate_selector(entry: &GitWorktreeEntry) -> String {
5825        Self::worktree_candidate_selector_from_path(&entry.administrative_directory)
5826    }
5827
5828    /// Derive one stable selector from a native administrative-directory path.
5829    fn worktree_candidate_selector_from_path(identity: &Path) -> String {
5830        if let Some(identity) = identity.to_str() {
5831            return Self::worktree_candidate_selector_from_identity(
5832                &normalize_native_path_display_str(identity),
5833            );
5834        }
5835        Self::worktree_candidate_selector_from_native_identity(
5836            identity.as_os_str().as_encoded_bytes(),
5837        )
5838    }
5839
5840    /// Derive one stable, short candidate selector from normalized Git identity text.
5841    fn worktree_candidate_selector_from_identity(identity: &str) -> String {
5842        Self::worktree_candidate_selector_from_native_identity(identity.as_bytes())
5843    }
5844
5845    /// Derive a selector from exact native administrative-directory bytes.
5846    fn worktree_candidate_selector_from_native_identity(identity: &[u8]) -> String {
5847        let digest = blake3::hash(identity).to_hex();
5848        let mut selector = String::with_capacity(
5849            MCP_WORKTREE_SELECTOR_PREFIX.len() + MCP_WORKTREE_SELECTOR_DIGEST_CHARS,
5850        );
5851        selector.push_str(MCP_WORKTREE_SELECTOR_PREFIX);
5852        selector.push_str(&digest.as_str()[..MCP_WORKTREE_SELECTOR_DIGEST_CHARS]);
5853        selector
5854    }
5855
5856    /// Derive the same selector from a persisted native identity that a live
5857    /// Git entry would expose, retaining the exact bytes when UTF-8 display is
5858    /// unavailable.
5859    fn worktree_candidate_selector_from_canonical_identity(
5860        identity: &CanonicalProjectRoot,
5861    ) -> String {
5862        Self::worktree_candidate_selector_from_path(identity.as_path())
5863    }
5864
5865    /// Borrow one active source root from structural worktree evidence.
5866    fn active_worktree_root(entry: &GitWorktreeEntry) -> Option<&Path> {
5867        match &entry.state {
5868            GitWorktreeState::Active { root, .. } => Some(root),
5869            GitWorktreeState::Missing { .. } | GitWorktreeState::Invalid { .. } => None,
5870        }
5871    }
5872
5873    /// Return whether one short selector identifies this structural candidate.
5874    fn worktree_candidate_matches(entry: &GitWorktreeEntry, selector: &str) -> bool {
5875        if Self::worktree_candidate_selector(entry) == selector {
5876            return true;
5877        }
5878        let root_name = Self::active_worktree_root(entry)
5879            .and_then(Path::file_name)
5880            .and_then(std::ffi::OsStr::to_str);
5881        let administrative_name = entry
5882            .administrative_directory
5883            .file_name()
5884            .and_then(std::ffi::OsStr::to_str);
5885        root_name.is_some_and(|name| name.eq_ignore_ascii_case(selector))
5886            || administrative_name.is_some_and(|name| name.eq_ignore_ascii_case(selector))
5887    }
5888
5889    /// Return bounded active non-control candidates for one stable or human selector.
5890    fn matching_worktree_candidates<'a>(
5891        &'a self,
5892        repository: &'a GitRepositoryStructure,
5893        selector: &str,
5894    ) -> Vec<&'a GitWorktreeEntry> {
5895        repository
5896            .worktrees
5897            .iter()
5898            .filter(|entry| {
5899                Self::active_worktree_root(entry)
5900                    .is_some_and(|root| root != self.control_state.root)
5901            })
5902            .filter(|entry| Self::worktree_candidate_matches(entry, selector))
5903            .collect()
5904    }
5905
5906    /// Revalidate one selected candidate and its filesystem lifecycle before registration.
5907    fn revalidate_worktree_candidate(
5908        &self,
5909        expected_repository: &GitRepositoryStructure,
5910        expected_entry: &GitWorktreeEntry,
5911        expected_identity: &str,
5912    ) -> Result<(GitRepositoryStructure, GitWorktreeEntry), CliError> {
5913        let expected_root = Self::active_worktree_root(expected_entry).ok_or_else(|| {
5914            CliError::InvalidInput(MCP_ERROR_WORKTREE_NO_LONGER_ACTIVE.to_string())
5915        })?;
5916        let repository = self.control_git_repository()?;
5917        let entry = repository
5918            .worktrees
5919            .iter()
5920            .find(|entry| entry.administrative_directory == expected_entry.administrative_directory)
5921            .cloned()
5922            .ok_or_else(|| {
5923                CliError::InvalidInput(MCP_ERROR_WORKTREE_LIFECYCLE_CHANGED.to_string())
5924            })?;
5925        let root = Self::active_worktree_root(&entry).ok_or_else(|| {
5926            CliError::InvalidInput(MCP_ERROR_WORKTREE_LIFECYCLE_CHANGED.to_string())
5927        })?;
5928        let identity = git_administrative_identity(&entry.administrative_directory)?;
5929        if repository.common_directory != expected_repository.common_directory
5930            || entry.role != expected_entry.role
5931            || root != expected_root
5932            || identity != expected_identity
5933        {
5934            return Err(CliError::InvalidInput(
5935                MCP_ERROR_WORKTREE_LIFECYCLE_CHANGED.to_string(),
5936            ));
5937        }
5938        Ok((repository, entry))
5939    }
5940
5941    /// Build one concise add-candidate row from active structural evidence.
5942    fn worktree_candidate(
5943        common_directory: &Path,
5944        entry: &GitWorktreeEntry,
5945    ) -> Option<McpWorktreeCandidate> {
5946        let root = Self::active_worktree_root(entry)?;
5947        Some(McpWorktreeCandidate {
5948            selector: Self::worktree_candidate_selector(entry),
5949            path_display: if common_directory.to_str().is_some()
5950                && entry.administrative_directory.to_str().is_some()
5951                && root.to_str().is_some()
5952            {
5953                McpWorktreePathDisplayState::Available
5954            } else {
5955                McpWorktreePathDisplayState::Unavailable
5956            },
5957            root: lossless_project_root_display(root),
5958            role: entry.role.into(),
5959        })
5960    }
5961
5962    /// Derive a valid default alias from one selected active worktree root.
5963    fn default_worktree_alias(root: &Path) -> Result<WorktreeAlias, CliError> {
5964        let name = root
5965            .file_name()
5966            .and_then(std::ffi::OsStr::to_str)
5967            .ok_or_else(|| CliError::InvalidInput(MCP_ERROR_WORKTREE_PATH_NON_UTF8.to_string()))?;
5968        WorktreeAlias::parse(&name.to_ascii_lowercase()).map_err(|source| {
5969            CliError::InvalidInput(format!(
5970                "selected worktree directory cannot be used as an alias; provide alias explicitly: {source}"
5971            ))
5972        })
5973    }
5974
5975    /// Open one exact local worktree atlas without creating or migrating it.
5976    fn open_local_worktree_atlas(root: &Path) -> Result<Option<AtlasStore>, CliError> {
5977        let db_path = Self::projectatlas_db_path(root);
5978        if !db_path.exists() {
5979            return Ok(None);
5980        }
5981        Ok(Some(open_atlas_store_read_only_for_project(
5982            &db_path, root,
5983        )?))
5984    }
5985
5986    /// Require the exact project identity from one already-open local atlas.
5987    fn local_worktree_project_instance_id(
5988        store: &AtlasStore,
5989        db_path: &Path,
5990    ) -> Result<ProjectInstanceId, CliError> {
5991        let project_instance_id = store.project_instance_id()?.ok_or_else(|| {
5992            CliError::InvalidInput(format!(
5993                "worktree atlas '{}' has no exact project identity",
5994                normalize_native_path_display(db_path)
5995            ))
5996        })?;
5997        Ok(project_instance_id)
5998    }
5999
6000    /// Export one local usage snapshot bound to an independently read project identity.
6001    fn local_worktree_usage_snapshot(
6002        store: &AtlasStore,
6003        db_path: &Path,
6004        project_instance_id: ProjectInstanceId,
6005    ) -> Result<WorktreeUsageSnapshot, CliError> {
6006        let snapshot = store.export_worktree_usage_snapshot()?;
6007        if snapshot.project_instance_id() != project_instance_id {
6008            return Err(CliError::InvalidInput(format!(
6009                "worktree atlas '{}' telemetry identity does not match its project identity",
6010                normalize_native_path_display(db_path)
6011            )));
6012        }
6013        Ok(snapshot)
6014    }
6015
6016    /// Read one exact local worktree atlas and its bounded telemetry snapshot without migration.
6017    fn local_worktree_atlas(root: &Path) -> Result<Option<LocalWorktreeAtlas>, CliError> {
6018        let db_path = Self::projectatlas_db_path(root);
6019        let Some(store) = Self::open_local_worktree_atlas(root)? else {
6020            return Ok(None);
6021        };
6022        let project_instance_id = Self::local_worktree_project_instance_id(&store, &db_path)?;
6023        let snapshot = Self::local_worktree_usage_snapshot(&store, &db_path, project_instance_id)?;
6024        Ok(Some(LocalWorktreeAtlas {
6025            project_instance_id,
6026            snapshot,
6027        }))
6028    }
6029
6030    /// Reopen an identity-bound local atlas before its registration is committed.
6031    fn revalidate_local_worktree_atlas_identity(
6032        root: &Path,
6033        expected: Option<ProjectInstanceId>,
6034    ) -> Result<(), CliError> {
6035        let Some(expected) = expected else {
6036            return Ok(());
6037        };
6038        let db_path = Self::projectatlas_db_path(root);
6039        let Some(store) = Self::open_local_worktree_atlas(root)? else {
6040            return Err(CliError::InvalidInput(
6041                MCP_ERROR_WORKTREE_IDENTITY_CONFLICT.to_string(),
6042            ));
6043        };
6044        if Self::local_worktree_project_instance_id(&store, &db_path)? != expected {
6045            return Err(CliError::InvalidInput(
6046                MCP_ERROR_WORKTREE_IDENTITY_CONFLICT.to_string(),
6047            ));
6048        }
6049        Ok(())
6050    }
6051
6052    /// Finalize one retirement under control-first writer exclusion.
6053    fn retire_registered_worktree(
6054        control: &AtlasStore,
6055        registration: &WorktreeRegistration,
6056        root: Option<&Path>,
6057        retired_at_epoch: u64,
6058        initial_blocker: Option<String>,
6059    ) -> Result<
6060        (
6061            WorktreeRegistration,
6062            Option<WorktreeUsageSyncState>,
6063            Option<String>,
6064        ),
6065        CliError,
6066    > {
6067        Self::retire_registered_worktree_with_pre_open(
6068            control,
6069            registration,
6070            root,
6071            retired_at_epoch,
6072            initial_blocker,
6073            || Ok(()),
6074        )
6075    }
6076
6077    /// Finalize retirement with one deterministic seam before the local atlas is opened.
6078    fn retire_registered_worktree_with_pre_open<F>(
6079        control: &AtlasStore,
6080        registration: &WorktreeRegistration,
6081        root: Option<&Path>,
6082        retired_at_epoch: u64,
6083        initial_blocker: Option<String>,
6084        pre_open: F,
6085    ) -> Result<
6086        (
6087            WorktreeRegistration,
6088            Option<WorktreeUsageSyncState>,
6089            Option<String>,
6090        ),
6091        CliError,
6092    >
6093    where
6094        F: FnOnce() -> Result<(), CliError>,
6095    {
6096        control.with_active_worktree_registration(
6097            registration.registration_id,
6098            &registration.alias,
6099            |guard| {
6100                let Some(root) = root else {
6101                    let retired = guard.retire(retired_at_epoch)?;
6102                    return Ok(Ok((retired, None, initial_blocker)));
6103                };
6104                if require_registered_worktree_lifecycle(guard.registration(), root).is_err() {
6105                    return Self::retire_changed_worktree_lifecycle(guard, retired_at_epoch)
6106                        .map(Ok);
6107                }
6108                if let Err(error) = pre_open() {
6109                    return Ok(Err(error));
6110                }
6111                let local = match Self::open_local_worktree_atlas(root) {
6112                    Ok(local) => local,
6113                    Err(error) => {
6114                        return Self::classify_retirement_failure(
6115                            guard,
6116                            root,
6117                            retired_at_epoch,
6118                            error,
6119                        );
6120                    }
6121                };
6122                let Some(local) = local else {
6123                    if require_registered_worktree_lifecycle(guard.registration(), root).is_err() {
6124                        return Self::retire_changed_worktree_lifecycle(guard, retired_at_epoch)
6125                            .map(Ok);
6126                    }
6127                    if guard.registration().project_instance_id.is_some() {
6128                        return Ok(Err(CliError::InvalidInput(
6129                            MCP_ERROR_BOUND_WORKTREE_ATLAS_MISSING.to_string(),
6130                        )));
6131                    }
6132                    let retired = guard.retire(retired_at_epoch)?;
6133                    return Ok(Ok((retired, None, initial_blocker)));
6134                };
6135                let db_path = Self::projectatlas_db_path(root);
6136                let project_instance_id =
6137                    match Self::local_worktree_project_instance_id(&local, &db_path) {
6138                        Ok(project_instance_id) => project_instance_id,
6139                        Err(error) => {
6140                            return Self::classify_retirement_failure(
6141                                guard,
6142                                root,
6143                                retired_at_epoch,
6144                                error,
6145                            );
6146                        }
6147                    };
6148                let finalization = match local.with_exclusive_worktree_usage_snapshot(|snapshot| {
6149                    if require_registered_worktree_lifecycle(guard.registration(), root).is_err() {
6150                        return Ok(None);
6151                    }
6152                    guard
6153                        .retire_with_usage_snapshot(
6154                            root,
6155                            project_instance_id,
6156                            snapshot,
6157                            retired_at_epoch,
6158                        )
6159                        .map(Some)
6160                }) {
6161                    Ok(finalization) => finalization,
6162                    Err(error) => {
6163                        return Self::classify_retirement_failure(
6164                            guard,
6165                            root,
6166                            retired_at_epoch,
6167                            error.into(),
6168                        );
6169                    }
6170                };
6171                if let Some((retired, synchronized)) = finalization {
6172                    return Ok(Ok((retired, Some(synchronized), initial_blocker)));
6173                }
6174                Self::retire_changed_worktree_lifecycle(guard, retired_at_epoch).map(Ok)
6175            },
6176        )?
6177    }
6178
6179    /// Preserve an unchanged database error or retire a replaced Git lifecycle.
6180    fn classify_retirement_failure(
6181        guard: &mut ActiveWorktreeRegistrationGuard<'_>,
6182        root: &Path,
6183        retired_at_epoch: u64,
6184        error: CliError,
6185    ) -> Result<
6186        Result<
6187            (
6188                WorktreeRegistration,
6189                Option<WorktreeUsageSyncState>,
6190                Option<String>,
6191            ),
6192            CliError,
6193        >,
6194        DbError,
6195    > {
6196        if require_registered_worktree_lifecycle(guard.registration(), root).is_err() {
6197            Self::retire_changed_worktree_lifecycle(guard, retired_at_epoch).map(Ok)
6198        } else {
6199            Ok(Err(error))
6200        }
6201    }
6202
6203    /// Retire a stale registration without binding, importing, or modifying replacement state.
6204    fn retire_changed_worktree_lifecycle(
6205        guard: &mut ActiveWorktreeRegistrationGuard<'_>,
6206        retired_at_epoch: u64,
6207    ) -> Result<
6208        (
6209            WorktreeRegistration,
6210            Option<WorktreeUsageSyncState>,
6211            Option<String>,
6212        ),
6213        DbError,
6214    > {
6215        let retired = guard.retire(retired_at_epoch)?;
6216        Ok((
6217            retired,
6218            None,
6219            Some(MCP_ERROR_WORKTREE_LIFECYCLE_CHANGED.to_string()),
6220        ))
6221    }
6222
6223    /// Reset one unbound alias while excluding concurrent bind or retirement publication.
6224    fn reset_registered_worktree_index(
6225        &self,
6226        state: &McpProjectState,
6227        selection: &McpWorktreeSelection,
6228        include_mcp_config: bool,
6229    ) -> Result<ResetIndexReport, CliError> {
6230        self.reset_registered_worktree_index_with_post_validation(
6231            state,
6232            selection,
6233            include_mcp_config,
6234            || Ok(()),
6235        )
6236    }
6237
6238    /// Reset one unbound alias with a deterministic post-validation seam.
6239    fn reset_registered_worktree_index_with_post_validation<F>(
6240        &self,
6241        state: &McpProjectState,
6242        selection: &McpWorktreeSelection,
6243        include_mcp_config: bool,
6244        post_validation: F,
6245    ) -> Result<ResetIndexReport, CliError>
6246    where
6247        F: FnOnce() -> Result<(), CliError>,
6248    {
6249        let alias = WorktreeAlias::parse(&selection.alias)?;
6250        let registration_id =
6251            selection
6252                .registration_id
6253                .ok_or_else(|| DbError::WorktreeRegistrationNotFound {
6254                    alias: selection.alias.clone(),
6255                })?;
6256        let control = Self::open_existing_mut_store(&self.control_state, &self.control_state)?;
6257        Self::require_captured_control_identity(Some(selection), &control)?;
6258        match control.with_unbound_worktree_registration(registration_id, &alias, |registration| {
6259            require_registered_worktree_lifecycle(registration, &state.root)?;
6260            post_validation()?;
6261            reset_index_files_with_revalidation(&state.db_path, include_mcp_config, || {
6262                require_registered_worktree_lifecycle(registration, &state.root)
6263            })
6264        }) {
6265            Ok(result) => result,
6266            Err(DbError::WorktreeRegistrationConflict { .. }) => Err(CliError::InvalidInput(
6267                MCP_ERROR_BOUND_WORKTREE_RESET_UNSUPPORTED.to_string(),
6268            )),
6269            Err(error) => Err(error.into()),
6270        }
6271    }
6272
6273    /// Match invalid evidence by its retained path without following a replacement target.
6274    fn worktree_administrative_path_identity(
6275        entry: &GitWorktreeEntry,
6276    ) -> Option<CanonicalProjectRoot> {
6277        match &entry.state {
6278            GitWorktreeState::Invalid { .. } => {
6279                CanonicalProjectRoot::from_persisted_path(entry.administrative_directory.clone())
6280                    .ok()
6281            }
6282            _ => CanonicalProjectRoot::from_path(&entry.administrative_directory).ok(),
6283        }
6284    }
6285
6286    /// Join one structural worktree entry to registry, atlas, and telemetry state.
6287    fn worktree_list_row(
6288        &self,
6289        common_directory: &Path,
6290        entry: &GitWorktreeEntry,
6291        registrations: &[WorktreeRegistration],
6292    ) -> McpWorktreeRow {
6293        let administrative_directory =
6294            lossless_native_path_display(&entry.administrative_directory);
6295        let common_identity = CanonicalProjectRoot::from_path(common_directory).ok();
6296        let administrative_identity = Self::worktree_administrative_path_identity(entry);
6297        let registration = registrations.iter().find(|registration| {
6298            registration.state == WorktreeRegistrationState::Active
6299                && common_identity
6300                    .as_ref()
6301                    .is_some_and(|identity| *identity == registration.git_common_directory_identity)
6302                && administrative_identity.as_ref().is_some_and(|identity| {
6303                    *identity == registration.git_administrative_directory_identity
6304                })
6305        });
6306        let administrative_identity = git_administrative_identity(&entry.administrative_directory);
6307        let lifecycle_matches = registration.is_none_or(|registration| {
6308            administrative_identity
6309                .as_ref()
6310                .is_ok_and(|identity| identity == &registration.git_administrative_identity)
6311        });
6312        let root = Self::active_worktree_root(entry);
6313        let display_root = match &entry.state {
6314            GitWorktreeState::Missing { .. } => {
6315                registration.map(|registration| registration.last_root_identity.as_path())
6316            }
6317            _ => root,
6318        };
6319        let path_display = if common_directory.to_str().is_some()
6320            && entry.administrative_directory.to_str().is_some()
6321            && display_root.is_none_or(|root| root.to_str().is_some())
6322        {
6323            McpWorktreePathDisplayState::Available
6324        } else {
6325            McpWorktreePathDisplayState::Unavailable
6326        };
6327        let control = root.is_some_and(|root| root == self.control_state.root);
6328        let alias = if control {
6329            Some(MCP_MAIN_WORKTREE_ALIAS.to_string())
6330        } else {
6331            registration.map(|registration| registration.alias.to_string())
6332        };
6333        let registration_state = if control {
6334            McpWorktreeRegistrationState::Control
6335        } else if registration.is_some() {
6336            McpWorktreeRegistrationState::Registered
6337        } else {
6338            McpWorktreeRegistrationState::Unregistered
6339        };
6340        let mut atlas_state = McpWorktreeAtlasState::Unavailable;
6341        let mut telemetry_state = McpWorktreeTelemetryState::Unavailable;
6342        let mut local_telemetry_revision = None;
6343        let mut project_instance_id = None;
6344        let mut blocker = (!lifecycle_matches)
6345            .then(|| MCP_ERROR_WORKTREE_LIFECYCLE_CHANGED.to_string())
6346            .or_else(|| administrative_identity.err().map(|error| error.to_string()));
6347        if matches!(path_display, McpWorktreePathDisplayState::Unavailable) && blocker.is_none() {
6348            blocker = Some(MCP_ERROR_WORKTREE_PATH_NON_UTF8.to_string());
6349        }
6350        let git_state = match &entry.state {
6351            GitWorktreeState::Active { root, .. } => {
6352                if lifecycle_matches {
6353                    match Self::local_worktree_atlas(root) {
6354                        Ok(Some(local)) => {
6355                            let identity_matches = registration
6356                                .and_then(|registration| registration.project_instance_id)
6357                                .is_none_or(|expected| expected == local.project_instance_id);
6358                            if identity_matches {
6359                                atlas_state = McpWorktreeAtlasState::Initialized;
6360                                local_telemetry_revision = Some(local.snapshot.revision());
6361                                project_instance_id = Some(local.project_instance_id.to_string());
6362                                telemetry_state = if control {
6363                                    McpWorktreeTelemetryState::Control
6364                                } else if let Some(registration) = registration {
6365                                    if local.snapshot.revision()
6366                                        > registration.accepted_telemetry_revision
6367                                    {
6368                                        McpWorktreeTelemetryState::Pending
6369                                    } else {
6370                                        McpWorktreeTelemetryState::Current
6371                                    }
6372                                } else {
6373                                    McpWorktreeTelemetryState::Unregistered
6374                                };
6375                            } else {
6376                                atlas_state = McpWorktreeAtlasState::Invalid;
6377                                blocker = Some(MCP_ERROR_WORKTREE_IDENTITY_CONFLICT.to_string());
6378                            }
6379                        }
6380                        Ok(None) => {
6381                            atlas_state = McpWorktreeAtlasState::Missing;
6382                            telemetry_state = if registration.is_some() {
6383                                McpWorktreeTelemetryState::MissingAtlas
6384                            } else {
6385                                McpWorktreeTelemetryState::Unregistered
6386                            };
6387                        }
6388                        Err(error) => {
6389                            atlas_state = McpWorktreeAtlasState::Invalid;
6390                            blocker = Some(error.to_string());
6391                        }
6392                    }
6393                } else {
6394                    atlas_state = McpWorktreeAtlasState::Invalid;
6395                    telemetry_state = McpWorktreeTelemetryState::Unavailable;
6396                }
6397                McpGitWorktreeState::Active
6398            }
6399            GitWorktreeState::Missing { git_control_path } => {
6400                blocker = Some(format!(
6401                    "Git registration target is missing at '{}'",
6402                    normalize_native_path_display(git_control_path)
6403                ));
6404                McpGitWorktreeState::Missing
6405            }
6406            GitWorktreeState::Invalid { issue } => {
6407                blocker = Some(format!(
6408                    "invalid Git evidence at '{}': {:?}",
6409                    normalize_native_path_display(&issue.path),
6410                    issue.kind
6411                ));
6412                McpGitWorktreeState::Invalid
6413            }
6414        };
6415        McpWorktreeRow {
6416            selector: Some(Self::worktree_candidate_selector(entry)),
6417            alias,
6418            role: entry.role.into(),
6419            path_display,
6420            git_state,
6421            registration: registration_state,
6422            administrative_directory,
6423            root: display_root.and_then(lossless_project_root_display),
6424            atlas_state,
6425            telemetry_state,
6426            accepted_telemetry_revision: registration
6427                .map(|registration| registration.accepted_telemetry_revision),
6428            local_telemetry_revision,
6429            project_instance_id,
6430            blocker,
6431        }
6432    }
6433
6434    /// Preserve one active alias when Git no longer reports its worktree registration.
6435    fn missing_registered_worktree_row(registration: &WorktreeRegistration) -> McpWorktreeRow {
6436        let path_display = if registration
6437            .git_common_directory_identity
6438            .display_string()
6439            .is_ok()
6440            && registration.last_root_identity.display_string().is_ok()
6441            && registration
6442                .git_administrative_directory_identity
6443                .display_string()
6444                .is_ok()
6445        {
6446            McpWorktreePathDisplayState::Available
6447        } else {
6448            McpWorktreePathDisplayState::Unavailable
6449        };
6450        McpWorktreeRow {
6451            selector: Some(Self::worktree_candidate_selector_from_canonical_identity(
6452                &registration.git_administrative_directory_identity,
6453            )),
6454            alias: Some(registration.alias.to_string()),
6455            role: McpGitWorktreeRole::Linked,
6456            path_display,
6457            git_state: McpGitWorktreeState::Missing,
6458            registration: McpWorktreeRegistrationState::Registered,
6459            administrative_directory: registration
6460                .git_administrative_directory_identity
6461                .display_string()
6462                .ok(),
6463            root: registration.last_root_identity.display_string().ok(),
6464            atlas_state: McpWorktreeAtlasState::Unavailable,
6465            telemetry_state: McpWorktreeTelemetryState::Unavailable,
6466            accepted_telemetry_revision: Some(registration.accepted_telemetry_revision),
6467            local_telemetry_revision: None,
6468            project_instance_id: registration
6469                .project_instance_id
6470                .map(|identity| identity.to_string()),
6471            blocker: Some(MCP_WORKTREE_MISSING_RETENTION_REASON.to_string()),
6472        }
6473    }
6474
6475    /// Return the current Unix epoch seconds within the persisted `SQLite` domain.
6476    fn current_epoch_seconds() -> Result<u64, CliError> {
6477        u64::try_from(mcp_unix_time_ms().saturating_div(1_000)).map_err(|source| {
6478            CliError::InvalidInput(format!(
6479                "current Unix epoch exceeds the supported worktree registry range: {source}"
6480            ))
6481        })
6482    }
6483
6484    /// Return active/legacy state or one captured registered alias target.
6485    fn state_for_target(
6486        &self,
6487        project_path: Option<String>,
6488        worktree: Option<String>,
6489    ) -> Result<McpProjectState, CliError> {
6490        let state = self.state_for_target_with_config_validation(
6491            project_path,
6492            worktree,
6493            McpConfigValidation::Immediate,
6494        )?;
6495        Self::require_initialized_worktree_target(&state)?;
6496        Ok(state)
6497    }
6498
6499    /// Resolve an ordered alias federation into exact initialized roots and identities.
6500    fn federated_worktree_roots(
6501        &self,
6502        worktrees: &[String],
6503    ) -> Result<(Vec<PathBuf>, Vec<McpWorktreeSelection>), CliError> {
6504        validate_federated_root_count(worktrees.len()).map_err(CliError::Service)?;
6505        let mut roots = Vec::with_capacity(worktrees.len());
6506        let mut selections = Vec::with_capacity(worktrees.len());
6507        for worktree in worktrees {
6508            let state = self.state_for_target(None, Some(worktree.clone()))?;
6509            let selection = state.worktree.ok_or_else(|| {
6510                CliError::InvalidInput(MCP_ERROR_FEDERATED_ALIAS_MISSING.to_string())
6511            })?;
6512            if selections
6513                .iter()
6514                .any(|captured: &McpWorktreeSelection| captured.alias == selection.alias)
6515                || roots.contains(&state.root)
6516            {
6517                return Err(CliError::Service(ServiceError::InvalidInput(
6518                    MCP_ERROR_FEDERATED_TARGET_DUPLICATE.to_string(),
6519                )));
6520            }
6521            selections.push(selection);
6522            roots.push(state.root);
6523        }
6524        Ok((roots, selections))
6525    }
6526
6527    /// Resolve one mutually exclusive target under the selected config-validation timing.
6528    fn state_for_target_with_config_validation(
6529        &self,
6530        project_path: Option<String>,
6531        worktree: Option<String>,
6532        validation: McpConfigValidation,
6533    ) -> Result<McpProjectState, CliError> {
6534        let project_path = Self::normalized_optional_path(project_path);
6535        let worktree = worktree.map(|alias| alias.trim().to_string());
6536        if project_path.is_some() && worktree.is_some() {
6537            return Err(CliError::InvalidInput(
6538                MCP_WORKTREE_PROJECT_PATH_CONFLICT.to_string(),
6539            ));
6540        }
6541        let Some(alias) = worktree else {
6542            return self.state_for_project_path_with_config_validation(project_path, validation);
6543        };
6544        if alias.is_empty() {
6545            return Err(CliError::InvalidInput(
6546                MCP_ERROR_WORKTREE_SELECTOR_EMPTY.to_string(),
6547            ));
6548        }
6549        if alias == MCP_MAIN_WORKTREE_ALIAS {
6550            let mut state = self.control_state.clone();
6551            let project_instance_id = if state.db_path.is_file() {
6552                Self::open_read_store(&state)?.project_instance_id()?
6553            } else {
6554                None
6555            };
6556            state.worktree = Some(McpWorktreeSelection {
6557                alias,
6558                registration_id: None,
6559                project_instance_id,
6560                control_project_instance_id: project_instance_id,
6561            });
6562            return Ok(state);
6563        }
6564        let alias = WorktreeAlias::parse(&alias)?;
6565        self.resolve_registered_worktree(&alias, validation)
6566    }
6567
6568    /// Resolve one active registration through current reciprocal Git structure.
6569    fn resolve_registered_worktree(
6570        &self,
6571        alias: &WorktreeAlias,
6572        validation: McpConfigValidation,
6573    ) -> Result<McpProjectState, CliError> {
6574        let control = Self::open_existing_mut_store(&self.control_state, &self.control_state)?;
6575        let control_project_instance_id = control.captured_project_binding()?.project_instance_id;
6576        let registration = control.worktree_registration(alias)?;
6577        let repository = self.control_git_repository()?;
6578        let common_identity = CanonicalProjectRoot::from_path(&repository.common_directory)
6579            .map_err(|source| CliError::InvalidInput(source.to_string()))?;
6580        if common_identity != registration.git_common_directory_identity {
6581            return Err(CliError::InvalidInput(format!(
6582                "registered worktree '{}' belongs to a different Git common directory",
6583                alias.as_str()
6584            )));
6585        }
6586        let entry = repository
6587            .worktrees
6588            .iter()
6589            .find(|entry| {
6590                Self::worktree_administrative_path_identity(entry).is_some_and(|identity| {
6591                    identity == registration.git_administrative_directory_identity
6592                })
6593            })
6594            .ok_or_else(|| {
6595                CliError::InvalidInput(format!(
6596                    "registered worktree '{}' is no longer present in the bounded Git inventory",
6597                    alias.as_str()
6598                ))
6599            })?;
6600        let administrative_identity = git_administrative_identity(&entry.administrative_directory)?;
6601        if administrative_identity != registration.git_administrative_identity {
6602            return Err(CliError::InvalidInput(
6603                MCP_ERROR_WORKTREE_LIFECYCLE_CHANGED.to_string(),
6604            ));
6605        }
6606        let root = match &entry.state {
6607            GitWorktreeState::Active { root, .. } => root.clone(),
6608            GitWorktreeState::Missing { .. } => {
6609                return Err(CliError::InvalidInput(format!(
6610                    "registered worktree '{}' is missing; restore it through Git or unregister it",
6611                    alias.as_str()
6612                )));
6613            }
6614            GitWorktreeState::Invalid { issue } => {
6615                return Err(CliError::InvalidInput(format!(
6616                    "registered worktree '{}' has invalid Git evidence at '{}': {:?}",
6617                    alias.as_str(),
6618                    normalize_native_path_display(&issue.path),
6619                    issue.kind
6620                )));
6621            }
6622        };
6623        if root == self.control_state.root {
6624            return Err(CliError::InvalidInput(
6625                MCP_ERROR_CONTROL_ALIAS_REQUIRED.to_string(),
6626            ));
6627        }
6628        CanonicalProjectRoot::from_path(&root).map_err(|source| {
6629            let mut message = MCP_ERROR_REGISTERED_WORKTREE_ROOT_INVALID_PREFIX.to_string();
6630            message.push_str(&source.to_string());
6631            CliError::InvalidInput(message)
6632        })?;
6633        let current_registry_root = CanonicalProjectRoot::from_path(&root)
6634            .map_err(|source| CliError::InvalidInput(source.to_string()))?;
6635        // Validate a bound local atlas before any control-catalog refresh. A Git
6636        // move alone is not enough evidence to rewrite `last_root`; the local
6637        // atlas must first be opened through its native admission boundary and
6638        // retain the captured project identity.
6639        if let Some(expected) = registration.project_instance_id {
6640            let store = Self::open_local_worktree_atlas(&root)?.ok_or_else(|| {
6641                CliError::InvalidInput(MCP_ERROR_BOUND_WORKTREE_ATLAS_MISSING.to_string())
6642            })?;
6643            let db_path = Self::projectatlas_db_path(&root);
6644            let observed = Self::local_worktree_project_instance_id(&store, &db_path)?;
6645            if observed != expected {
6646                return Err(CliError::InvalidInput(
6647                    MCP_ERROR_WORKTREE_IDENTITY_CONFLICT.to_string(),
6648                ));
6649            }
6650        }
6651        let registration_root_matches = current_registry_root == registration.last_root_identity;
6652        let registration = if registration_root_matches {
6653            registration
6654        } else {
6655            control.refresh_worktree_root(&registration, &root)?
6656        };
6657        let mut state = Self::project_state_from_root_with_config_validation(&root, validation)?;
6658        state.worktree = Some(McpWorktreeSelection {
6659            alias: alias.to_string(),
6660            registration_id: Some(registration.registration_id),
6661            project_instance_id: registration.project_instance_id,
6662            control_project_instance_id: Some(control_project_instance_id),
6663        });
6664        Ok(state)
6665    }
6666
6667    /// Return project state under the requested configuration-validation timing.
6668    fn state_for_project_path_with_config_validation(
6669        &self,
6670        project_path: Option<String>,
6671        validation: McpConfigValidation,
6672    ) -> Result<McpProjectState, CliError> {
6673        let project_path = Self::normalized_optional_path(project_path);
6674        project_path.map_or_else(
6675            || self.active_project_state(),
6676            |path| {
6677                Self::project_state_from_root_with_config_validation(Path::new(&path), validation)
6678            },
6679        )
6680    }
6681
6682    /// Return the nearest-project policy for one call, honoring explicit overrides.
6683    fn nearest_project_enabled(&self, override_value: Option<bool>) -> bool {
6684        override_value.unwrap_or(self.allow_nearest_project)
6685    }
6686
6687    /// Return selected state and validate an optional root assertion.
6688    fn state_and_root_path(
6689        &self,
6690        project_path: Option<String>,
6691        worktree: Option<String>,
6692        path: Option<String>,
6693        nearest_project: bool,
6694    ) -> Result<(McpProjectState, PathBuf), CliError> {
6695        self.state_and_root_path_with_config_validation(
6696            project_path,
6697            worktree,
6698            path,
6699            nearest_project,
6700            McpConfigValidation::Immediate,
6701        )
6702    }
6703
6704    /// Select a background project without reading configuration before task admission.
6705    fn background_state_and_root_path(
6706        &self,
6707        project_path: Option<String>,
6708        worktree: Option<String>,
6709        path: Option<String>,
6710        nearest_project: bool,
6711    ) -> Result<(McpProjectState, PathBuf), CliError> {
6712        self.state_and_root_path_with_config_validation(
6713            project_path,
6714            worktree,
6715            path,
6716            nearest_project,
6717            McpConfigValidation::Deferred,
6718        )
6719    }
6720
6721    /// Select project state and validate a root assertion under one config timing policy.
6722    fn state_and_root_path_with_config_validation(
6723        &self,
6724        project_path: Option<String>,
6725        worktree: Option<String>,
6726        path: Option<String>,
6727        nearest_project: bool,
6728        validation: McpConfigValidation,
6729    ) -> Result<(McpProjectState, PathBuf), CliError> {
6730        let explicit_target = project_path.is_some() || worktree.is_some();
6731        let state = self.state_for_target_with_config_validation(
6732            project_path.clone(),
6733            worktree,
6734            validation,
6735        )?;
6736        Self::require_initialized_worktree_target(&state)?;
6737        let root = match (
6738            Self::normalized_optional_path(project_path),
6739            Self::normalized_optional_path(path),
6740        ) {
6741            (None, Some(path)) if !explicit_target => {
6742                match Self::path_or_project_root(&state, Some(path.clone())) {
6743                    Ok(root) => root,
6744                    Err(active_error) => {
6745                        if !nearest_project {
6746                            return Err(active_error);
6747                        }
6748                        if Self::absolute_path_inside_selected_root(&state, &path)? {
6749                            return Err(active_error);
6750                        }
6751                        let Some(indexed_state) =
6752                            Self::nearest_root_state_for_root_argument_with_config_validation(
6753                                Path::new(&path),
6754                                validation,
6755                            )?
6756                        else {
6757                            return Err(active_error);
6758                        };
6759                        let root = indexed_state.root.clone();
6760                        return Ok((indexed_state, root));
6761                    }
6762                }
6763            }
6764            (_, path) => Self::path_or_project_root(&state, path)?,
6765        };
6766        Ok((state, root))
6767    }
6768
6769    /// Return whether a root assertion path is inside the selected root but not root-equivalent.
6770    fn absolute_path_inside_selected_root(
6771        state: &McpProjectState,
6772        path: &str,
6773    ) -> Result<bool, CliError> {
6774        let candidate = PathBuf::from(path);
6775        if !candidate.is_absolute() {
6776            return Ok(false);
6777        }
6778        let resolved = canonical_project_root(&candidate)?;
6779        Ok(resolved != state.root && resolved.starts_with(&state.root))
6780    }
6781
6782    /// Return nearest indexed root under one configuration-validation timing policy.
6783    fn nearest_root_state_for_root_argument_with_config_validation(
6784        path: &Path,
6785        validation: McpConfigValidation,
6786    ) -> Result<Option<McpProjectState>, CliError> {
6787        let Ok(addressed_root) = canonical_project_root(path) else {
6788            return Ok(None);
6789        };
6790        let Some(indexed_state) =
6791            Self::project_state_from_nearest_indexed_path_with_config_validation(path, validation)?
6792        else {
6793            return Ok(None);
6794        };
6795        if addressed_root == indexed_state.root {
6796            Ok(Some(indexed_state))
6797        } else {
6798            Ok(None)
6799        }
6800    }
6801
6802    /// Return state and a repository-relative file key for an MCP file argument.
6803    fn state_and_file_key(
6804        &self,
6805        project_path: Option<&str>,
6806        worktree: Option<&str>,
6807        file: &str,
6808        nearest_project: bool,
6809    ) -> Result<McpResolvedRepoPath, CliError> {
6810        let state = self.state_for_target(
6811            project_path.map(ToString::to_string),
6812            worktree.map(ToString::to_string),
6813        )?;
6814        let file_path = PathBuf::from(&file);
6815        if !file_path.is_absolute() {
6816            let file_key = validated_repo_file_key(&file_path)
6817                .map_err(|source| CliError::InvalidInput(source.to_string()))?;
6818            return Ok(McpResolvedRepoPath {
6819                state,
6820                key: file_key,
6821                routed_project: false,
6822            });
6823        }
6824        if nearest_project && project_path.is_none() && worktree.is_none() {
6825            let resolved = Self::nearest_state_and_repo_key(&state, file)?.ok_or_else(|| {
6826                Self::selected_project_path_error(PATH_NOT_INSIDE_INDEXED_PROJECT_ERROR)
6827            })?;
6828            let file_key = validated_repo_file_key(Path::new(&resolved.key))
6829                .map_err(|source| CliError::InvalidInput(source.to_string()))?;
6830            return Ok(McpResolvedRepoPath {
6831                key: file_key,
6832                ..resolved
6833            });
6834        }
6835        if let Some(file_key) = Self::absolute_path_key_in_selected_project(&state, &file_path)? {
6836            let file_key = validated_repo_file_key(Path::new(&file_key))
6837                .map_err(|source| CliError::InvalidInput(source.to_string()))?;
6838            return Ok(McpResolvedRepoPath {
6839                state,
6840                key: file_key,
6841                routed_project: false,
6842            });
6843        }
6844        if project_path.is_some() || worktree.is_some() {
6845            return Err(Self::selected_project_path_error(
6846                PATH_NOT_INSIDE_INDEXED_PROJECT_ERROR,
6847            ));
6848        }
6849        if !nearest_project {
6850            return Err(Self::selected_project_path_error(
6851                PATH_NOT_INSIDE_INDEXED_PROJECT_ERROR,
6852            ));
6853        }
6854        Err(Self::selected_project_path_error(
6855            PATH_NOT_INSIDE_INDEXED_PROJECT_ERROR,
6856        ))
6857    }
6858
6859    /// Return state and an optional repository-relative file key for MCP symbol arguments.
6860    fn state_and_optional_file_key(
6861        &self,
6862        project_path: Option<&str>,
6863        worktree: Option<&str>,
6864        file: Option<&str>,
6865        nearest_project: bool,
6866    ) -> Result<(McpProjectState, Option<String>, bool), CliError> {
6867        let Some(file) = file else {
6868            return self
6869                .state_for_target(
6870                    project_path.map(ToString::to_string),
6871                    worktree.map(ToString::to_string),
6872                )
6873                .map(|state| (state, None, false));
6874        };
6875        let resolved = self.state_and_file_key(project_path, worktree, file, nearest_project)?;
6876        Ok((resolved.state, Some(resolved.key), resolved.routed_project))
6877    }
6878
6879    /// Return state and an optional folder filter for MCP file-ranking arguments.
6880    fn state_and_optional_folder_filter(
6881        &self,
6882        project_path: Option<&str>,
6883        worktree: Option<&str>,
6884        folder: Option<&str>,
6885        nearest_project: bool,
6886    ) -> Result<(McpProjectState, Option<String>, bool), CliError> {
6887        let state = self.state_for_target(
6888            project_path.map(ToString::to_string),
6889            worktree.map(ToString::to_string),
6890        )?;
6891        let Some(folder) = folder.map(str::trim).filter(|folder| !folder.is_empty()) else {
6892            return Ok((state, None, false));
6893        };
6894        let folder_path = PathBuf::from(&folder);
6895        if !folder_path.is_absolute() {
6896            let folder_filter = normalized_folder_filter(folder)?;
6897            return Ok((state, Some(folder_filter), false));
6898        }
6899        if nearest_project && project_path.is_none() && worktree.is_none() {
6900            let resolved = Self::nearest_state_and_repo_key(&state, folder)?.ok_or_else(|| {
6901                Self::selected_project_path_error(FOLDER_NOT_INSIDE_INDEXED_PROJECT_ERROR)
6902            })?;
6903            let folder_filter = normalized_folder_filter(&resolved.key)?;
6904            return Ok((resolved.state, Some(folder_filter), resolved.routed_project));
6905        }
6906        if let Some(folder_filter) =
6907            Self::absolute_path_key_in_selected_project(&state, &folder_path)?
6908        {
6909            let folder_filter = normalized_folder_filter(&folder_filter)?;
6910            return Ok((state, Some(folder_filter), false));
6911        }
6912        if project_path.is_some() || worktree.is_some() {
6913            return Err(Self::selected_project_path_error(
6914                FOLDER_NOT_INSIDE_INDEXED_PROJECT_ERROR,
6915            ));
6916        }
6917        if !nearest_project {
6918            return Err(Self::selected_project_path_error(
6919                FOLDER_NOT_INSIDE_INDEXED_PROJECT_ERROR,
6920            ));
6921        }
6922        Err(Self::selected_project_path_error(
6923            FOLDER_NOT_INSIDE_INDEXED_PROJECT_ERROR,
6924        ))
6925    }
6926
6927    /// Resolve an absolute addressed path to the nearest indexed project and repo key.
6928    fn nearest_state_and_repo_key(
6929        active_state: &McpProjectState,
6930        path: &str,
6931    ) -> Result<Option<McpResolvedRepoPath>, CliError> {
6932        let path = Path::new(path);
6933        let absolute_path = McpAbsolutePath::canonicalize(path)?;
6934        let lexical_state = Self::project_state_from_nearest_lexical_indexed_path(path)?;
6935        let canonical_state = Self::project_state_from_nearest_indexed_path(path)?;
6936        Self::reject_ambiguous_nearest_project_path(
6937            path,
6938            lexical_state.as_ref(),
6939            canonical_state.as_ref(),
6940            &absolute_path,
6941        )?;
6942        let Some(state) = canonical_state else {
6943            return Ok(None);
6944        };
6945        if state.root == active_state.root {
6946            let key = McpSelectedRoot::from_state(active_state)
6947                .repo_key_for(&absolute_path)?
6948                .ok_or_else(|| {
6949                    Self::selected_project_path_error(PATH_NOT_INSIDE_INDEXED_PROJECT_ERROR)
6950                })?;
6951            return Ok(Some(McpResolvedRepoPath {
6952                state: active_state.clone(),
6953                key: key.into_string(),
6954                routed_project: false,
6955            }));
6956        }
6957        let key = McpSelectedRoot::from_state(&state)
6958            .repo_key_for(&absolute_path)?
6959            .ok_or_else(|| {
6960                Self::selected_project_path_error(PATH_NOT_INSIDE_INDEXED_PROJECT_ERROR)
6961            })?;
6962        Ok(Some(McpResolvedRepoPath {
6963            state,
6964            key: key.into_string(),
6965            routed_project: true,
6966        }))
6967    }
6968
6969    /// Return a selected-project repository key for an absolute path inside the active root.
6970    fn absolute_path_key_in_selected_project(
6971        state: &McpProjectState,
6972        path: &Path,
6973    ) -> Result<Option<String>, CliError> {
6974        let absolute_path = McpAbsolutePath::canonicalize(path)?;
6975        McpSelectedRoot::from_state(state)
6976            .repo_key_for(&absolute_path)
6977            .map(|key| key.map(McpRepoKey::into_string))
6978    }
6979
6980    /// Normalize optional project/root path text from MCP payloads.
6981    fn normalized_optional_path(path: Option<String>) -> Option<String> {
6982        path.map(|path| path.trim().to_string())
6983            .filter(|path| !path.is_empty())
6984    }
6985
6986    /// Build `ProjectAtlas` state for one project root.
6987    fn project_state_from_root(root: &Path) -> Result<McpProjectState, CliError> {
6988        Self::project_state_from_root_with_config_validation(root, McpConfigValidation::Immediate)
6989    }
6990
6991    /// Build project state while controlling when configuration content is validated.
6992    fn project_state_from_root_with_config_validation(
6993        root: &Path,
6994        validation: McpConfigValidation,
6995    ) -> Result<McpProjectState, CliError> {
6996        let root = canonical_source_project_root(root)?;
6997        if !root.is_dir() {
6998            return Err(CliError::InvalidInput(format!(
6999                "project path '{}' is not a directory",
7000                root.display()
7001            )));
7002        }
7003        let db_path = Self::projectatlas_db_path(&root);
7004        let config_path = Self::config_path_for_project_root(&root, validation)?;
7005        Ok(McpProjectState {
7006            root,
7007            db_path,
7008            config_path,
7009            worktree: None,
7010        })
7011    }
7012
7013    /// Build project state from the nearest indexed ancestor of an addressed path.
7014    fn project_state_from_nearest_indexed_path(
7015        path: &Path,
7016    ) -> Result<Option<McpProjectState>, CliError> {
7017        Self::project_state_from_nearest_indexed_path_with_config_validation(
7018            path,
7019            McpConfigValidation::Immediate,
7020        )
7021    }
7022
7023    /// Build nearest canonical project state under one config-validation policy.
7024    fn project_state_from_nearest_indexed_path_with_config_validation(
7025        path: &Path,
7026        validation: McpConfigValidation,
7027    ) -> Result<Option<McpProjectState>, CliError> {
7028        let Ok(absolute_path) = McpAbsolutePath::canonicalize(path) else {
7029            return Ok(None);
7030        };
7031        let mut candidate = absolute_path.nearest_search_start();
7032        loop {
7033            if let Some(indexed_root) = Self::indexed_root_from_candidate(candidate) {
7034                let config_path =
7035                    Self::config_path_for_project_root(&indexed_root.root, validation)?;
7036                return Ok(Some(McpProjectState {
7037                    root: indexed_root.root,
7038                    db_path: indexed_root.db_path,
7039                    config_path,
7040                    worktree: None,
7041                }));
7042            }
7043            let Some(parent) = candidate.parent() else {
7044                return Ok(None);
7045            };
7046            candidate = parent;
7047        }
7048    }
7049
7050    /// Build project state from the nearest lexical indexed ancestor of an addressed path.
7051    fn project_state_from_nearest_lexical_indexed_path(
7052        path: &Path,
7053    ) -> Result<Option<McpProjectState>, CliError> {
7054        Self::project_state_from_nearest_lexical_indexed_path_with_config_validation(
7055            path,
7056            McpConfigValidation::Immediate,
7057        )
7058    }
7059
7060    /// Build nearest lexical project state under one config-validation policy.
7061    fn project_state_from_nearest_lexical_indexed_path_with_config_validation(
7062        path: &Path,
7063        validation: McpConfigValidation,
7064    ) -> Result<Option<McpProjectState>, CliError> {
7065        if !path.is_absolute() {
7066            return Ok(None);
7067        }
7068        let lexical_path = Self::lexically_normalized_absolute_path(path);
7069        let mut candidate = if lexical_path.is_dir() {
7070            lexical_path
7071        } else {
7072            lexical_path
7073                .parent()
7074                .unwrap_or(lexical_path.as_path())
7075                .to_path_buf()
7076        };
7077        loop {
7078            if let Some(indexed_root) = Self::indexed_root_from_lexical_candidate(&candidate) {
7079                let config_path =
7080                    Self::config_path_for_project_root(&indexed_root.root, validation)?;
7081                return Ok(Some(McpProjectState {
7082                    root: indexed_root.root,
7083                    db_path: indexed_root.db_path,
7084                    config_path,
7085                    worktree: None,
7086                }));
7087            }
7088            let Some(parent) = candidate.parent() else {
7089                return Ok(None);
7090            };
7091            candidate = parent.to_path_buf();
7092        }
7093    }
7094
7095    /// Return a role-typed indexed root when a candidate folder has a matching DB.
7096    fn indexed_root_from_candidate(candidate: &Path) -> Option<McpIndexedRoot> {
7097        let Ok(root) = canonical_project_root(candidate) else {
7098            return None;
7099        };
7100        let Ok(root_identity) = CanonicalProjectRoot::from_path(&root) else {
7101            return None;
7102        };
7103        let db_path = Self::projectatlas_db_path(&root);
7104        if !db_path.is_file() || !Self::nearest_indexed_db_matches_root(&db_path, &root_identity) {
7105            return None;
7106        }
7107        Some(McpIndexedRoot { root, db_path })
7108    }
7109
7110    /// Return an indexed lexical root without treating symlinked descendants as the lexical owner.
7111    fn indexed_root_from_lexical_candidate(candidate: &Path) -> Option<McpIndexedRoot> {
7112        if Self::path_has_symlink_component(candidate) {
7113            return None;
7114        }
7115        let Ok(root) = canonical_project_root(candidate) else {
7116            return None;
7117        };
7118        let Ok(candidate_identity) = CanonicalProjectRoot::from_path(candidate) else {
7119            return None;
7120        };
7121        let Ok(root_identity) = CanonicalProjectRoot::from_path(&root) else {
7122            return None;
7123        };
7124        if candidate_identity != root_identity {
7125            return None;
7126        }
7127        let db_path = Self::projectatlas_db_path(&root);
7128        if !db_path.is_file() || !Self::nearest_indexed_db_matches_root(&db_path, &root_identity) {
7129            return None;
7130        }
7131        Some(McpIndexedRoot { root, db_path })
7132    }
7133
7134    /// Return a normalized absolute path without resolving symlinks or junctions.
7135    fn lexically_normalized_absolute_path(path: &Path) -> PathBuf {
7136        let mut normalized = PathBuf::new();
7137        for component in path.components() {
7138            match component {
7139                Component::Prefix(prefix) => normalized.push(prefix.as_os_str()),
7140                Component::RootDir => normalized.push(component.as_os_str()),
7141                Component::CurDir => {}
7142                Component::ParentDir => {
7143                    normalized.pop();
7144                }
7145                Component::Normal(segment) => normalized.push(segment),
7146            }
7147        }
7148        normalized
7149    }
7150
7151    /// Return whether a path contains a symlink component in its lexical ancestry.
7152    fn path_has_symlink_component(path: &Path) -> bool {
7153        let mut current = PathBuf::new();
7154        for component in path.components() {
7155            match component {
7156                Component::Prefix(prefix) => current.push(prefix.as_os_str()),
7157                Component::RootDir => current.push(component.as_os_str()),
7158                Component::CurDir => {}
7159                Component::ParentDir => {
7160                    current.pop();
7161                }
7162                Component::Normal(segment) => {
7163                    current.push(segment);
7164                    if fs::symlink_metadata(&current)
7165                        .is_ok_and(|metadata| Self::metadata_is_symlink_or_reparse_point(&metadata))
7166                    {
7167                        return true;
7168                    }
7169                }
7170            }
7171        }
7172        false
7173    }
7174
7175    /// Return whether metadata represents a symlink or Windows reparse point.
7176    fn metadata_is_symlink_or_reparse_point(metadata: &fs::Metadata) -> bool {
7177        if metadata.file_type().is_symlink() {
7178            return true;
7179        }
7180        #[cfg(windows)]
7181        {
7182            use std::os::windows::fs::MetadataExt;
7183            const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x400;
7184            metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0
7185        }
7186        #[cfg(not(windows))]
7187        {
7188            false
7189        }
7190    }
7191
7192    /// Reject nearest-project routing when lexical and canonical roots disagree.
7193    fn reject_ambiguous_nearest_project_path(
7194        path: &Path,
7195        lexical_state: Option<&McpProjectState>,
7196        canonical_state: Option<&McpProjectState>,
7197        canonical_path: &McpAbsolutePath,
7198    ) -> Result<(), CliError> {
7199        if let (Some(lexical_state), Some(canonical_state)) = (lexical_state, canonical_state) {
7200            if lexical_state.root != canonical_state.root {
7201                return Err(Self::ambiguous_nearest_project_path_error(
7202                    path,
7203                    Some(lexical_state),
7204                    Some(canonical_state),
7205                ));
7206            }
7207            return Ok(());
7208        }
7209        let lexical_path = Self::lexically_normalized_absolute_path(path);
7210        if Self::path_has_symlink_component(&lexical_path)
7211            || lexical_state.is_some_and(|state| !canonical_path.as_path().starts_with(&state.root))
7212        {
7213            return Err(Self::ambiguous_nearest_project_path_error(
7214                path,
7215                lexical_state,
7216                canonical_state,
7217            ));
7218        }
7219        Ok(())
7220    }
7221
7222    /// Build a clear MCP error for ambiguous symlink or junction routing.
7223    fn ambiguous_nearest_project_path_error(
7224        path: &Path,
7225        lexical_state: Option<&McpProjectState>,
7226        canonical_state: Option<&McpProjectState>,
7227    ) -> CliError {
7228        let lexical_root = lexical_state.map_or_else(
7229            || MCP_NO_ROOT_PLACEHOLDER.to_string(),
7230            |state| normalize_native_path_display(&state.root),
7231        );
7232        let resolved_root = canonical_state.map_or_else(
7233            || MCP_NO_ROOT_PLACEHOLDER.to_string(),
7234            |state| normalize_native_path_display(&state.root),
7235        );
7236        let path_display = normalize_native_path_display(path);
7237        let mut message = String::new();
7238        message.push_str(AMBIGUOUS_NEAREST_PROJECT_PATH_ERROR);
7239        message.push_str(MCP_ERROR_FOR_PATH_FRAGMENT);
7240        message.push_str(&path_display);
7241        message.push_str(MCP_ERROR_LEXICAL_ROOT_FRAGMENT);
7242        message.push_str(&lexical_root);
7243        message.push_str(MCP_ERROR_RESOLVED_ROOT_FRAGMENT);
7244        message.push_str(&resolved_root);
7245        message.push_str(MCP_ERROR_GUIDANCE_FRAGMENT);
7246        message.push_str(OUTSIDE_SELECTED_PROJECT_GUIDANCE);
7247        CliError::InvalidInput(message)
7248    }
7249
7250    /// Return whether nearest routing can safely associate one DB with a live root.
7251    fn nearest_indexed_db_matches_root(db_path: &Path, root: &CanonicalProjectRoot) -> bool {
7252        match read_project_root_identity_read_only(db_path) {
7253            Ok(Some(stored_root)) => Self::nearest_existing_roots_match(&stored_root, root),
7254            Ok(None) => {
7255                let Ok(Some(legacy_root)) = read_legacy_project_root_candidate_read_only(db_path)
7256                else {
7257                    return false;
7258                };
7259                let Ok(legacy_root) = CanonicalProjectRoot::from_path(Path::new(&legacy_root))
7260                else {
7261                    return false;
7262                };
7263                Self::nearest_existing_roots_match(&legacy_root, root)
7264            }
7265            Err(_) => false,
7266        }
7267    }
7268
7269    /// Re-resolve both existing roots before nearest-routing comparison.
7270    fn nearest_existing_roots_match(
7271        persisted_root: &CanonicalProjectRoot,
7272        selected_root: &CanonicalProjectRoot,
7273    ) -> bool {
7274        let Ok(persisted_root) = CanonicalProjectRoot::from_path(persisted_root.as_path()) else {
7275            return false;
7276        };
7277        let Ok(selected_root) = CanonicalProjectRoot::from_path(selected_root.as_path()) else {
7278            return false;
7279        };
7280        persisted_root == selected_root
7281    }
7282
7283    /// Return the standard `ProjectAtlas` DB path for one project root.
7284    fn projectatlas_db_path(root: &Path) -> PathBuf {
7285        root.join(PROJECTATLAS_DIR_NAME)
7286            .join(PROJECTATLAS_DB_FILE_NAME)
7287    }
7288
7289    /// Return the standard nested `ProjectAtlas` config path for one project root.
7290    fn projectatlas_nested_config_path(root: &Path) -> PathBuf {
7291        root.join(PROJECTATLAS_DIR_NAME)
7292            .join(PROJECTATLAS_CONFIG_FILE_NAME)
7293    }
7294
7295    /// Return the flat `ProjectAtlas` config path for one project root.
7296    fn projectatlas_flat_config_path(root: &Path) -> PathBuf {
7297        root.join(PROJECTATLAS_FLAT_CONFIG_FILE_NAME)
7298    }
7299
7300    /// Find a project-local config and optionally reject one pointing at another root.
7301    fn config_path_for_project_root(
7302        root: &Path,
7303        validation: McpConfigValidation,
7304    ) -> Result<Option<PathBuf>, CliError> {
7305        for config_path in [
7306            Self::projectatlas_nested_config_path(root),
7307            Self::projectatlas_flat_config_path(root),
7308        ] {
7309            if config_path.exists() {
7310                if validation == McpConfigValidation::Immediate {
7311                    Self::validate_project_config_root(root, &config_path)?;
7312                }
7313                return Ok(Some(config_path));
7314            }
7315        }
7316        Ok(None)
7317    }
7318
7319    /// Ensure a selected project's config cannot redirect the MCP root.
7320    fn validate_project_config_root(root: &Path, config_path: &Path) -> Result<(), CliError> {
7321        let config = load_atlas_config(Some(config_path))?;
7322        let config_root = canonical_project_root(&config.root)?;
7323        if config_root != root {
7324            return Err(config_root_mismatch_error(config_path, &config_root, root));
7325        }
7326        Ok(())
7327    }
7328
7329    /// Return whether a startup config belongs to the selected root.
7330    fn config_matches_project_root(root: &Path, config_path: &Path) -> bool {
7331        Self::validate_project_config_root(root, config_path).is_ok()
7332    }
7333
7334    /// Render active project state for agents.
7335    fn render_project_state(state: &McpProjectState) -> Result<String, CliError> {
7336        let payload = McpProjectStateResponse {
7337            project: Self::project_state_payload(state),
7338        };
7339        Self::encode_serialized_payload(payload)
7340    }
7341
7342    /// Build selected-project payload fields.
7343    fn project_state_payload(state: &McpProjectState) -> McpProjectStatePayload {
7344        McpProjectStatePayload {
7345            worktree: state
7346                .worktree
7347                .as_ref()
7348                .map(|selection| selection.alias.clone()),
7349            registration_id: state
7350                .worktree
7351                .as_ref()
7352                .and_then(|selection| selection.registration_id),
7353            root: lossless_project_root_display(&state.root),
7354            db: lossless_native_path_display(&state.db_path),
7355            config: state
7356                .config_path
7357                .as_ref()
7358                .and_then(|path| lossless_native_path_display(path)),
7359            status: McpProjectStatus::Active,
7360        }
7361    }
7362
7363    /// Prefix routed cross-project read payloads with selected root/DB metadata.
7364    fn with_selected_project_audit(
7365        state: &McpProjectState,
7366        routed_project: bool,
7367        toon: String,
7368    ) -> Result<String, CliError> {
7369        if !routed_project {
7370            return Ok(toon);
7371        }
7372        let prefix = Self::encode_named_payload(
7373            MCP_PAYLOAD_SELECTED_PROJECT,
7374            &Self::project_state_payload(state),
7375        )?;
7376        let mut audited = String::with_capacity(prefix.len() + 1 + toon.len());
7377        audited.push_str(&prefix);
7378        audited.push('\n');
7379        audited.push_str(&toon);
7380        Ok(audited)
7381    }
7382
7383    /// Prefix one controlled analysis payload with selected root/DB metadata.
7384    fn with_selected_project_audit_controlled(
7385        state: &McpProjectState,
7386        routed_project: bool,
7387        toon: String,
7388        control: &IndexWorkControl,
7389    ) -> Result<String, CliError> {
7390        control.check(projectatlas_core::IndexWorkStage::RepositoryTraversal)?;
7391        if !routed_project {
7392            return Ok(toon);
7393        }
7394        let prefix = controlled_named_output(
7395            OutputFormat::Toon,
7396            MCP_PAYLOAD_SELECTED_PROJECT,
7397            &Self::project_state_payload(state),
7398            control,
7399        )?;
7400        let mut audited = String::with_capacity(prefix.len() + 1 + toon.len());
7401        audited.push_str(&prefix);
7402        audited.push('\n');
7403        audited.push_str(&toon);
7404        control.check(projectatlas_core::IndexWorkStage::RepositoryTraversal)?;
7405        Ok(audited)
7406    }
7407
7408    /// Return a path parameter or the selected project root.
7409    fn path_or_project_root(
7410        state: &McpProjectState,
7411        path: Option<String>,
7412    ) -> Result<PathBuf, CliError> {
7413        let Some(value) = path else {
7414            return Ok(state.root.clone());
7415        };
7416        if value.is_empty() {
7417            return Ok(state.root.clone());
7418        }
7419        let original = value.clone();
7420        let candidate = PathBuf::from(value);
7421        let resolved = if candidate.is_absolute() {
7422            candidate
7423        } else {
7424            state.root.join(candidate)
7425        };
7426        let resolved = canonical_project_root(&resolved)?;
7427        if resolved == state.root {
7428            Ok(resolved)
7429        } else if resolved.starts_with(&state.root) {
7430            let resolved_display = resolved.display();
7431            let project_root_display = state.root.display();
7432            Err(CliError::InvalidInput(format!(
7433                "MCP path '{original}' resolves to '{resolved_display}', not the selected project root '{project_root_display}'; {SELECTED_ROOT_ASSERTION_GUIDANCE}"
7434            )))
7435        } else {
7436            let resolved_display = resolved.display();
7437            let project_root_display = state.root.display();
7438            Err(CliError::InvalidInput(format!(
7439                "MCP path '{original}' resolves to '{resolved_display}', outside the selected project root '{project_root_display}'; {OUTSIDE_SELECTED_PROJECT_GUIDANCE}"
7440            )))
7441        }
7442    }
7443
7444    /// Reject invalid purpose targets before admission can supersede another source witness.
7445    fn preflight_purpose_path(state: &McpProjectState, path: &str) -> Result<String, CliError> {
7446        let node_key = validated_repo_node_key(std::path::Path::new(path))
7447            .map_err(Self::selected_project_path_error)?;
7448        let store = Self::open_read_store(state)?;
7449        let indexed = store.load_node_by_path(&node_key)?.is_some();
7450        store.finish_index_read_snapshot()?;
7451        let source = state.root.join(&node_key);
7452        if !indexed
7453            && !source.try_exists().map_err(|source_error| CliError::Io {
7454                path: source,
7455                source: source_error,
7456            })?
7457        {
7458            return Err(CliError::InvalidInput(format!(
7459                "path {node_key:?} is not indexed in the MCP-bound project"
7460            )));
7461        }
7462        Ok(node_key)
7463    }
7464
7465    /// Require a purpose target in the captured index, including inside its write transaction.
7466    fn require_indexed_purpose_path(store: &AtlasStore, node_key: &str) -> Result<(), CliError> {
7467        if store.load_node_by_path(node_key)?.is_none() {
7468            return Err(CliError::InvalidInput(format!(
7469                "path {node_key:?} is not indexed in the MCP-bound project"
7470            )));
7471        }
7472        Ok(())
7473    }
7474
7475    /// Add selected-project guidance to repository-relative path errors.
7476    fn selected_project_path_error(message: impl std::fmt::Display) -> CliError {
7477        CliError::InvalidInput(format!("{message}; {OUTSIDE_SELECTED_PROJECT_GUIDANCE}"))
7478    }
7479
7480    /// Return a query parameter with a stable default.
7481    fn query_or_empty(query: Option<String>) -> String {
7482        query.unwrap_or_default()
7483    }
7484
7485    /// Encode one serializable payload as agent-readable TOON text.
7486    fn encode_serialized_payload<T>(payload: T) -> Result<String, CliError>
7487    where
7488        T: Serialize,
7489    {
7490        Ok(encode_agent_payload(&serde_json::to_value(payload)?))
7491    }
7492
7493    /// Encode a dynamic top-level payload key without relying on `json!` key syntax.
7494    fn encode_named_payload<T>(key: &str, payload: &T) -> Result<String, CliError>
7495    where
7496        T: Serialize,
7497    {
7498        let mut object = serde_json::Map::new();
7499        object.insert(key.to_string(), serde_json::to_value(payload)?);
7500        Ok(encode_agent_payload(&serde_json::Value::Object(object)))
7501    }
7502
7503    /// Encode two dynamic top-level payload keys without relying on `json!` key syntax.
7504    fn encode_two_named_payloads<T, U>(
7505        first_key: &str,
7506        first_payload: &T,
7507        second_key: &str,
7508        second_payload: &U,
7509    ) -> Result<String, CliError>
7510    where
7511        T: Serialize,
7512        U: Serialize,
7513    {
7514        let mut object = serde_json::Map::new();
7515        object.insert(first_key.to_string(), serde_json::to_value(first_payload)?);
7516        object.insert(
7517            second_key.to_string(),
7518            serde_json::to_value(second_payload)?,
7519        );
7520        Ok(encode_agent_payload(&serde_json::Value::Object(object)))
7521    }
7522
7523    /// Encode an MCP error as a structured agent-readable payload.
7524    fn encode_error_payload(error: &CliError) -> String {
7525        let schema_version_mismatch = schema_version_mismatch_payload(error);
7526        let schema_migration_required = schema_migration_required_payload(error);
7527        let message = schema_migration_required.as_ref().map_or_else(
7528            || error.to_string(),
7529            SchemaMigrationRequiredPayload::message,
7530        );
7531        let (
7532            kind,
7533            refresh_required,
7534            init_required,
7535            worktree_required,
7536            verification_incomplete,
7537            project_mismatch,
7538            database_filesystem,
7539            search_capability,
7540            next,
7541        ) = match error {
7542            CliError::InitRequired(report) => (
7543                AgentErrorKind::InitRequired,
7544                None,
7545                Some(report.as_ref().clone()),
7546                None,
7547                None,
7548                None,
7549                None,
7550                None,
7551                Some(McpNextCall {
7552                    tool: MCP_TOOL_ATLAS_INIT,
7553                    project_path: report
7554                        .worktree
7555                        .is_none()
7556                        .then(|| report.project_root.clone())
7557                        .flatten(),
7558                    worktree: report.worktree.clone(),
7559                }),
7560            ),
7561            CliError::WorktreeRequired(report) => (
7562                AgentErrorKind::WorktreeRequired,
7563                None,
7564                None,
7565                Some(report.as_ref().clone()),
7566                None,
7567                None,
7568                None,
7569                None,
7570                None,
7571            ),
7572            CliError::RefreshRequired(report) => (
7573                AgentErrorKind::RefreshRequired,
7574                Some(report.as_ref().clone()),
7575                None,
7576                None,
7577                None,
7578                None,
7579                None,
7580                None,
7581                Some(McpNextCall {
7582                    tool: MCP_TOOL_ATLAS_WATCH_ONCE,
7583                    project_path: report
7584                        .worktree
7585                        .is_none()
7586                        .then(|| report.project_root.clone())
7587                        .flatten(),
7588                    worktree: report.worktree.clone(),
7589                }),
7590            ),
7591            CliError::VerificationIncomplete(report) => (
7592                AgentErrorKind::VerificationIncomplete,
7593                None,
7594                None,
7595                None,
7596                Some(report.as_ref().clone()),
7597                None,
7598                None,
7599                None,
7600                None,
7601            ),
7602            CliError::ProjectMismatch(report) => (
7603                AgentErrorKind::ProjectMismatch,
7604                None,
7605                None,
7606                None,
7607                None,
7608                Some(report.as_ref().clone()),
7609                None,
7610                None,
7611                None,
7612            ),
7613            CliError::Service(ServiceError::SearchCapabilityUnavailable {
7614                requested_mode,
7615                state,
7616                guidance,
7617            }) => (
7618                AgentErrorKind::SearchCapabilityUnavailable,
7619                None,
7620                None,
7621                None,
7622                None,
7623                None,
7624                None,
7625                Some(crate::SearchCapabilityErrorPayload {
7626                    requested_mode: *requested_mode,
7627                    state,
7628                    recovery: guidance,
7629                }),
7630                None,
7631            ),
7632            _ if schema_version_mismatch.is_some() => (
7633                AgentErrorKind::SchemaVersionMismatch,
7634                None,
7635                None,
7636                None,
7637                None,
7638                None,
7639                None,
7640                None,
7641                None,
7642            ),
7643            _ if schema_migration_required.is_some() => (
7644                AgentErrorKind::SchemaMigrationRequired,
7645                None,
7646                None,
7647                None,
7648                None,
7649                None,
7650                None,
7651                None,
7652                None,
7653            ),
7654            _ => database_filesystem_error_payload(error).map_or(
7655                (
7656                    AgentErrorKind::Error,
7657                    None,
7658                    None,
7659                    None,
7660                    None,
7661                    None,
7662                    None,
7663                    None,
7664                    None,
7665                ),
7666                |(kind, database_filesystem)| {
7667                    (
7668                        kind,
7669                        None,
7670                        None,
7671                        None,
7672                        None,
7673                        None,
7674                        Some(database_filesystem),
7675                        None,
7676                        None,
7677                    )
7678                },
7679            ),
7680        };
7681        let payload = McpErrorResponse {
7682            error: McpErrorPayload {
7683                kind,
7684                message,
7685                refresh_required,
7686                init_required,
7687                worktree_required,
7688                verification_incomplete,
7689                project_mismatch,
7690                database_filesystem,
7691                schema_version_mismatch,
7692                schema_migration_required,
7693                search_capability,
7694                next,
7695            },
7696        };
7697        serde_json::to_value(payload).map_or_else(
7698            |source| {
7699                let mut message = MCP_ERROR_SERIALIZATION_FALLBACK_PREFIX.to_string();
7700                message.push_str(&source.to_string());
7701                message
7702            },
7703            |value| encode_agent_payload(&value),
7704        )
7705    }
7706
7707    /// Convert a command result into an agent-readable TOON MCP text payload.
7708    fn as_mcp_text(result: Result<String, CliError>) -> McpToolTextResult {
7709        match result {
7710            Ok(text) => McpToolTextResult(Ok(text)),
7711            Err(error) => {
7712                let payload = Self::encode_error_payload(&error);
7713                if schema_version_mismatch_payload(&error).is_some() {
7714                    McpToolTextResult(Err(payload))
7715                } else {
7716                    McpToolTextResult(Ok(payload))
7717                }
7718            }
7719        }
7720    }
7721}
7722
7723/// Convert MCP health parameters into a DB health query.
7724fn health_query_from_params(
7725    params: &AtlasHealthParams,
7726    scope: HealthScope,
7727) -> Result<HealthQuery, CliError> {
7728    Ok(HealthQuery {
7729        start_index: params.start_index.unwrap_or(0),
7730        limit: params
7731            .limit
7732            .filter(|value| *value > 0)
7733            .unwrap_or(DEFAULT_HEALTH_LIMIT)
7734            .min(MAX_HEALTH_LIMIT),
7735        category: trimmed_filter(params.category.as_deref()),
7736        severity: trimmed_filter(params.severity.as_deref())
7737            .as_deref()
7738            .map(parse_health_severity)
7739            .transpose()?,
7740        path_prefix: trimmed_filter(params.path_prefix.as_deref())
7741            .map(|value| normalize_repo_path_prefix(&value)),
7742        summary_only: params.summary_only.unwrap_or(false),
7743        scope,
7744    })
7745}
7746
7747/// Return whether MCP parameters contain coverage-only filters.
7748fn has_coverage_filters(params: &AtlasHealthParams) -> bool {
7749    params.parser.is_some()
7750        || params.provider.is_some()
7751        || params.relation.is_some()
7752        || params.coverage_state.is_some()
7753        || params.reason.is_some()
7754}
7755
7756/// Convert explicit MCP coverage parameters into one typed bounded DB query.
7757fn coverage_query_from_params(
7758    params: &AtlasHealthParams,
7759) -> Result<RepositoryCoverageQuery, CliError> {
7760    let limit = params
7761        .limit
7762        .filter(|value| *value > 0)
7763        .unwrap_or(DEFAULT_HEALTH_LIMIT)
7764        .min(COVERAGE_PAGE_MAX_LIMIT as usize);
7765    Ok(RepositoryCoverageQuery {
7766        start_index: u32::try_from(params.start_index.unwrap_or(0)).map_err(|error| {
7767            let mut message = String::from(MCP_ERROR_COVERAGE_START_INDEX_TOO_LARGE_PREFIX);
7768            message.push_str(&error.to_string());
7769            CliError::InvalidInput(message)
7770        })?,
7771        limit: u32::try_from(limit).map_err(|error| {
7772            let mut message = String::from(MCP_ERROR_COVERAGE_LIMIT_TOO_LARGE_PREFIX);
7773            message.push_str(&error.to_string());
7774            CliError::InvalidInput(message)
7775        })?,
7776        path_prefix: trimmed_filter(params.path_prefix.as_deref())
7777            .map(|value| normalize_repo_path_prefix(&value)),
7778        parser: trimmed_filter(params.parser.as_deref())
7779            .as_deref()
7780            .map(parse_coverage_parser)
7781            .transpose()?,
7782        provider: trimmed_filter(params.provider.as_deref())
7783            .as_deref()
7784            .map(parse_coverage_parser)
7785            .transpose()?,
7786        relation: trimmed_filter(params.relation.as_deref())
7787            .as_deref()
7788            .map(parse_coverage_relation)
7789            .transpose()?,
7790        state: trimmed_filter(params.coverage_state.as_deref())
7791            .as_deref()
7792            .map(parse_coverage_state)
7793            .transpose()?,
7794        reason: trimmed_filter(params.reason.as_deref()),
7795    })
7796}
7797
7798/// Return the DB scope for MCP purpose queue parameters.
7799fn purpose_queue_scope(params: &AtlasHealthParams) -> HealthScope {
7800    match (
7801        params.include_assets.unwrap_or(false),
7802        params.include_low_priority_files.unwrap_or(false),
7803    ) {
7804        (false, false) => HealthScope::purpose_default(),
7805        (true, false) => HealthScope::purpose_with_assets(),
7806        (false, true) => HealthScope::purpose_with_source_files(),
7807        (true, true) => HealthScope::all(),
7808    }
7809}
7810
7811/// Return a trimmed non-empty string parameter.
7812fn trimmed_filter(value: Option<&str>) -> Option<String> {
7813    value
7814        .map(str::trim)
7815        .filter(|value| !value.is_empty())
7816        .map(ToString::to_string)
7817}
7818
7819/// Parse an MCP health severity filter.
7820fn parse_health_severity(value: &str) -> Result<Severity, CliError> {
7821    let trimmed = value.trim();
7822    trimmed.parse::<Severity>().map_err(|_source| {
7823        let expected = expected_health_severity_names();
7824        CliError::InvalidInput(format!(
7825            "invalid health severity '{trimmed}'; expected {expected}"
7826        ))
7827    })
7828}
7829
7830/// Render accepted health severity names for diagnostics.
7831fn expected_health_severity_names() -> String {
7832    let mut expected = Severity::Info.as_str().to_string();
7833    expected.push_str(SEVERITY_EXPECTED_SEPARATOR);
7834    expected.push_str(Severity::Warning.as_str());
7835    expected.push_str(SEVERITY_EXPECTED_FINAL_SEPARATOR);
7836    expected.push_str(Severity::Error.as_str());
7837    expected
7838}
7839
7840#[tool_router(router = tool_router)]
7841impl ProjectAtlasMcpServer {
7842    /// Select the active project root for subsequent MCP calls.
7843    #[tool(
7844        name = "atlas_set_project_path",
7845        description = "Select the active ProjectAtlas project root for later MCP calls that omit project_path."
7846    )]
7847    fn atlas_set_project_path(
7848        &self,
7849        Parameters(params): Parameters<AtlasSetProjectPathParams>,
7850    ) -> McpToolTextResult {
7851        Self::as_mcp_text((|| {
7852            let state = Self::project_state_from_root(Path::new(&params.project_path))?;
7853            self.set_active_project_state(state.clone())?;
7854            Self::render_project_state(&state)
7855        })())
7856    }
7857
7858    /// Return bounded structural and `ProjectAtlas` worktree state without mutation.
7859    #[tool(
7860        name = "atlas_worktree_list",
7861        description = "List structurally discovered Git worktrees, short ProjectAtlas aliases, atlas availability, and telemetry synchronization state without changing Git or files."
7862    )]
7863    fn atlas_worktree_list(
7864        &self,
7865        Parameters(params): Parameters<AtlasWorktreeListParams>,
7866    ) -> McpToolTextResult {
7867        Self::as_mcp_text((|| {
7868            let repository = self.control_git_repository()?;
7869            let store = open_atlas_store_read_only_for_project(
7870                &self.control_state.db_path,
7871                &self.control_state.root,
7872            )?;
7873            let include_retired = params.include_retired.unwrap_or(false);
7874            let registrations = store.worktree_registrations(include_retired)?;
7875            let structural_identities = repository
7876                .worktrees
7877                .iter()
7878                .filter_map(Self::worktree_administrative_path_identity)
7879                .collect::<HashSet<_>>();
7880            let (mut worktrees, unregistered): (Vec<_>, Vec<_>) = repository
7881                .worktrees
7882                .iter()
7883                .map(|entry| {
7884                    self.worktree_list_row(&repository.common_directory, entry, &registrations)
7885                })
7886                .partition(|row| {
7887                    !matches!(row.registration, McpWorktreeRegistrationState::Unregistered)
7888                });
7889            worktrees.extend(
7890                registrations
7891                    .iter()
7892                    .filter(|registration| {
7893                        registration.state == WorktreeRegistrationState::Active
7894                            && !structural_identities
7895                                .contains(&registration.git_administrative_directory_identity)
7896                    })
7897                    .map(Self::missing_registered_worktree_row),
7898            );
7899            let total_worktrees = worktrees.len() + unregistered.len();
7900            worktrees.extend(
7901                unregistered
7902                    .into_iter()
7903                    .take(MCP_WORKTREE_LIST_MAX_ROWS.saturating_sub(worktrees.len())),
7904            );
7905            let truncated = total_worktrees > worktrees.len();
7906            let retired = registrations
7907                .iter()
7908                .filter(|registration| registration.state == WorktreeRegistrationState::Retired)
7909                .map(|registration| McpRetiredWorktreeRow {
7910                    alias: registration.alias.to_string(),
7911                    path_display: if registration.last_root_identity.display_string().is_ok() {
7912                        McpWorktreePathDisplayState::Available
7913                    } else {
7914                        McpWorktreePathDisplayState::Unavailable
7915                    },
7916                    last_root: registration.last_root_identity.display_string().ok(),
7917                    project_instance_id: registration
7918                        .project_instance_id
7919                        .map(|identity| identity.to_string()),
7920                    accepted_telemetry_revision: registration.accepted_telemetry_revision,
7921                })
7922                .collect();
7923            Self::encode_named_payload(
7924                MCP_PAYLOAD_WORKTREES,
7925                &McpWorktreeListReport {
7926                    control_alias: MCP_MAIN_WORKTREE_ALIAS,
7927                    control_root: lossless_project_root_display(&self.control_state.root),
7928                    common_directory: lossless_native_path_display(&repository.common_directory),
7929                    worktrees,
7930                    retired,
7931                    total_worktrees,
7932                    truncated,
7933                },
7934            )
7935        })())
7936    }
7937
7938    /// Register one active structural worktree in the immutable control atlas.
7939    #[tool(
7940        name = "atlas_worktree_add",
7941        description = "Register one structurally discovered Git worktree under a short ProjectAtlas alias without creating, moving, or switching Git worktrees."
7942    )]
7943    fn atlas_worktree_add(
7944        &self,
7945        Parameters(params): Parameters<AtlasWorktreeAddParams>,
7946    ) -> McpToolTextResult {
7947        Self::as_mcp_text((|| {
7948            let requested = params.worktree.trim();
7949            if requested.is_empty() {
7950                return Err(CliError::InvalidInput(
7951                    MCP_ERROR_WORKTREE_SELECTOR_EMPTY.to_string(),
7952                ));
7953            }
7954            let repository = self.control_git_repository()?;
7955            let candidates = self.matching_worktree_candidates(&repository, requested);
7956            if candidates.len() != 1 {
7957                let ambiguous = !candidates.is_empty();
7958                let candidate_rows: Vec<McpWorktreeCandidate> = if candidates.is_empty() {
7959                    repository
7960                        .worktrees
7961                        .iter()
7962                        .filter(|entry| {
7963                            Self::active_worktree_root(entry)
7964                                .is_some_and(|root| root != self.control_state.root)
7965                        })
7966                        .filter_map(|entry| {
7967                            Self::worktree_candidate(&repository.common_directory, entry)
7968                        })
7969                        .take(MCP_WORKTREE_LIST_MAX_ROWS)
7970                        .collect()
7971                } else {
7972                    candidates
7973                        .into_iter()
7974                        .filter_map(|entry| {
7975                            Self::worktree_candidate(&repository.common_directory, entry)
7976                        })
7977                        .take(MCP_WORKTREE_LIST_MAX_ROWS)
7978                        .collect()
7979                };
7980                return Self::encode_named_payload(
7981                    MCP_PAYLOAD_WORKTREE,
7982                    &McpWorktreeMutationReport {
7983                        operation: McpWorktreeMutationOperation::Add,
7984                        status: if ambiguous {
7985                            McpWorktreeMutationStatus::Ambiguous
7986                        } else {
7987                            McpWorktreeMutationStatus::NotFound
7988                        },
7989                        selector: None,
7990                        alias: None,
7991                        root: None,
7992                        path_display: None,
7993                        registration_id: None,
7994                        telemetry_sync: None,
7995                        candidates: candidate_rows,
7996                        blocker: Some(format!(
7997                            "selector {requested:?} did not identify exactly one active non-control worktree"
7998                        )),
7999                        git_unchanged: true,
8000                        files_unchanged: true,
8001                    },
8002                );
8003            }
8004            let entry = candidates[0];
8005            let root = Self::active_worktree_root(entry).ok_or_else(|| {
8006                CliError::InvalidInput(MCP_ERROR_WORKTREE_NO_LONGER_ACTIVE.to_string())
8007            })?;
8008            let administrative_identity =
8009                git_administrative_identity(&entry.administrative_directory)?;
8010            let alias = match params.alias.as_deref() {
8011                Some(alias) => WorktreeAlias::parse(alias.trim())?,
8012                None => Self::default_worktree_alias(root)?,
8013            };
8014            let db_path = Self::projectatlas_db_path(root);
8015            let mut project_instance_id = None;
8016            let local = (|| {
8017                let Some(store) = Self::open_local_worktree_atlas(root)? else {
8018                    return Ok(None);
8019                };
8020                let identity = Self::local_worktree_project_instance_id(&store, &db_path)?;
8021                project_instance_id = Some(identity);
8022                let snapshot = Self::local_worktree_usage_snapshot(&store, &db_path, identity)?;
8023                Ok::<_, CliError>(Some(LocalWorktreeAtlas {
8024                    project_instance_id: identity,
8025                    snapshot,
8026                }))
8027            })();
8028            let (local, blocker) = match local {
8029                Ok(local) => (local, None),
8030                Err(error) if project_instance_id.is_some() => return Err(error),
8031                Err(error) => (
8032                    None,
8033                    Some(format!(
8034                        "registration committed without local telemetry import: {error}"
8035                    )),
8036                ),
8037            };
8038            let (repository, entry) =
8039                self.revalidate_worktree_candidate(&repository, entry, &administrative_identity)?;
8040            let root = Self::active_worktree_root(&entry).ok_or_else(|| {
8041                CliError::InvalidInput(MCP_ERROR_WORKTREE_NO_LONGER_ACTIVE.to_string())
8042            })?;
8043            Self::revalidate_local_worktree_atlas_identity(root, project_instance_id)?;
8044            if let Some(local) = local.as_ref() {
8045                require_current_worktree_usage_snapshot(&db_path, root, &local.snapshot)?;
8046            }
8047            let control = Self::open_existing_mut_store(&self.control_state, &self.control_state)?;
8048            let created_at_epoch = Self::current_epoch_seconds()?;
8049            let (registration, telemetry_sync) = if let Some(local) = local.as_ref() {
8050                match control.register_worktree_with_usage_snapshot(
8051                    &alias,
8052                    &repository.common_directory,
8053                    &entry.administrative_directory,
8054                    &administrative_identity,
8055                    root,
8056                    local.project_instance_id,
8057                    &local.snapshot,
8058                    created_at_epoch,
8059                ) {
8060                    Ok((registration, state)) => (registration, Some(state)),
8061                    Err(error) => return Err(error.into()),
8062                }
8063            } else {
8064                (
8065                    control.register_worktree(
8066                        &alias,
8067                        &repository.common_directory,
8068                        &entry.administrative_directory,
8069                        &administrative_identity,
8070                        root,
8071                        project_instance_id,
8072                        created_at_epoch,
8073                    )?,
8074                    None,
8075                )
8076            };
8077            Self::encode_named_payload(
8078                MCP_PAYLOAD_WORKTREE,
8079                &McpWorktreeMutationReport {
8080                    operation: McpWorktreeMutationOperation::Add,
8081                    status: McpWorktreeMutationStatus::Registered,
8082                    selector: Some(Self::worktree_candidate_selector(&entry)),
8083                    alias: Some(alias.to_string()),
8084                    root: lossless_project_root_display(root),
8085                    path_display: Some(
8086                        if root.to_str().is_some()
8087                            && repository.common_directory.to_str().is_some()
8088                            && entry.administrative_directory.to_str().is_some()
8089                        {
8090                            McpWorktreePathDisplayState::Available
8091                        } else {
8092                            McpWorktreePathDisplayState::Unavailable
8093                        },
8094                    ),
8095                    registration_id: Some(registration.registration_id),
8096                    telemetry_sync,
8097                    candidates: Vec::new(),
8098                    blocker,
8099                    git_unchanged: true,
8100                    files_unchanged: true,
8101                },
8102            )
8103        })())
8104    }
8105
8106    /// Final-sync and retire one `ProjectAtlas` alias without touching Git or files.
8107    #[tool(
8108        name = "atlas_worktree_remove",
8109        description = "Final-sync and retire one ProjectAtlas worktree alias while preserving retained token totals and leaving Git, source files, .projectatlas, and SQLite files untouched."
8110    )]
8111    fn atlas_worktree_remove(
8112        &self,
8113        Parameters(params): Parameters<AtlasWorktreeRemoveParams>,
8114    ) -> McpToolTextResult {
8115        Self::as_mcp_text((|| {
8116            let alias = WorktreeAlias::parse(params.worktree.trim())?;
8117            let repository = self.control_git_repository()?;
8118            let control = Self::open_existing_mut_store(&self.control_state, &self.control_state)?;
8119            let registration = control.worktree_registration(&alias)?;
8120            let mut blocker = None;
8121            let entry = repository.worktrees.iter().find(|entry| {
8122                Self::worktree_administrative_path_identity(entry).is_some_and(|identity| {
8123                    identity == registration.git_administrative_directory_identity
8124                })
8125            });
8126            let entry = match entry {
8127                Some(entry) if matches!(entry.state, GitWorktreeState::Invalid { .. }) => {
8128                    Some(entry)
8129                }
8130                Some(entry) => match git_administrative_identity(&entry.administrative_directory) {
8131                    Ok(identity) if identity == registration.git_administrative_identity => {
8132                        Some(entry)
8133                    }
8134                    Ok(_) => {
8135                        blocker = Some(MCP_ERROR_WORKTREE_LIFECYCLE_CHANGED.to_string());
8136                        None
8137                    }
8138                    Err(error) => {
8139                        blocker = Some(error.to_string());
8140                        None
8141                    }
8142                },
8143                None => None,
8144            };
8145            let retired_at_epoch = Self::current_epoch_seconds()?;
8146            let retained_path_display =
8147                Self::missing_registered_worktree_row(&registration).path_display;
8148            let (root_display, active_root) = match entry.map(|entry| &entry.state) {
8149                Some(GitWorktreeState::Active { root, .. }) => {
8150                    (lossless_project_root_display(root), Some(root.as_path()))
8151                }
8152                Some(GitWorktreeState::Invalid { issue }) => {
8153                    return Err(CliError::InvalidInput(format!(
8154                        "cannot retire worktree '{}' while its Git evidence is invalid at '{}': {:?}",
8155                        alias,
8156                        normalize_native_path_display(&issue.path),
8157                        issue.kind
8158                    )));
8159                }
8160                Some(GitWorktreeState::Missing { .. }) | None => {
8161                    blocker
8162                        .get_or_insert_with(|| MCP_WORKTREE_MISSING_RETENTION_REASON.to_string());
8163                    (registration.last_root_identity.display_string().ok(), None)
8164                }
8165            };
8166            let (retired, telemetry_sync, final_blocker) = Self::retire_registered_worktree(
8167                &control,
8168                &registration,
8169                active_root,
8170                retired_at_epoch,
8171                blocker,
8172            )?;
8173            Self::encode_named_payload(
8174                MCP_PAYLOAD_WORKTREE,
8175                &McpWorktreeMutationReport {
8176                    operation: McpWorktreeMutationOperation::Remove,
8177                    status: McpWorktreeMutationStatus::Retired,
8178                    selector: entry.map(Self::worktree_candidate_selector),
8179                    alias: Some(alias.to_string()),
8180                    root: root_display,
8181                    path_display: Some(entry.map_or_else(
8182                        || retained_path_display,
8183                        |entry| match &entry.state {
8184                            GitWorktreeState::Active { root, .. }
8185                                if root.to_str().is_some()
8186                                    && repository.common_directory.to_str().is_some()
8187                                    && entry.administrative_directory.to_str().is_some() =>
8188                            {
8189                                McpWorktreePathDisplayState::Available
8190                            }
8191                            GitWorktreeState::Missing { .. } => retained_path_display,
8192                            GitWorktreeState::Active { .. } | GitWorktreeState::Invalid { .. } => {
8193                                McpWorktreePathDisplayState::Unavailable
8194                            }
8195                        },
8196                    )),
8197                    registration_id: Some(retired.registration_id),
8198                    telemetry_sync,
8199                    candidates: Vec::new(),
8200                    blocker: final_blocker,
8201                    git_unchanged: true,
8202                    files_unchanged: true,
8203                },
8204            )
8205        })())
8206    }
8207
8208    /// Initialize a `ProjectAtlas` project-local config surface.
8209    #[tool(
8210        name = "atlas_init",
8211        description = "Initialize ProjectAtlas project-local config, database, host MCP configs, scan/index, and purpose handoff."
8212    )]
8213    fn atlas_init(&self, Parameters(params): Parameters<AtlasInitParams>) -> McpToolTextResult {
8214        Self::as_mcp_text((|| {
8215            let state = self.init_project_root(params.project_path, params.worktree)?;
8216            let config_path = init_config_path(&state.root, state.config_path.as_deref());
8217            let mut report = self.run_registered_worktree_init(
8218                &state,
8219                &config_path,
8220                &InitBootstrapOptions {
8221                    no_scan: params.no_scan.unwrap_or(false),
8222                    force_rescan: params.force_rescan.unwrap_or(false),
8223                    text_index_max_bytes: params.text_index_max_bytes,
8224                },
8225            )?;
8226            crate::write_init_mcp_config_files(
8227                &mut report,
8228                &state.root.join(PROJECTATLAS_DIR_NAME),
8229                &state.db_path,
8230                &config_path,
8231                false,
8232            );
8233            Self::encode_named_payload(MCP_PAYLOAD_INIT, &report)
8234        })())
8235    }
8236
8237    /// Write an explicit compatibility map export.
8238    #[tool(
8239        name = "atlas_map",
8240        description = "Write the explicit compatibility ProjectAtlas map export for older workflows."
8241    )]
8242    fn atlas_map(&self, Parameters(params): Parameters<AtlasMapParams>) -> McpToolTextResult {
8243        Self::as_mcp_text((|| {
8244            let state = self.admin_project_root(params.project_path, params.worktree)?;
8245            let report = Self::build_map_report(
8246                &state,
8247                params.json.unwrap_or(false),
8248                params.force.unwrap_or(false),
8249            )?;
8250            Self::encode_named_payload(MCP_PAYLOAD_MAP, &report)
8251        })())
8252    }
8253
8254    /// Return root/DB/config diagnostics.
8255    #[tool(
8256        name = "atlas_root",
8257        description = "Show or verify ProjectAtlas root, DB, config, and runtime identity."
8258    )]
8259    fn atlas_root(&self, Parameters(params): Parameters<AtlasRootParams>) -> McpToolTextResult {
8260        Self::as_mcp_text((|| {
8261            if let Some(control_root) = params.control_root.as_deref() {
8262                if params.project_path.is_some()
8263                    || params.worktree.is_some()
8264                    || params.verify.unwrap_or(false)
8265                {
8266                    return Err(CliError::InvalidInput(
8267                        MCP_ERROR_ROOT_CONTROL_CONFLICT.to_owned(),
8268                    ));
8269                }
8270                let report = build_repository_control_report(Path::new(control_root))?;
8271                return Ok(render_repository_control_report(&report));
8272            }
8273            let state = self.admin_project_root(params.project_path, params.worktree)?;
8274            let report = build_root_report(&state.db_path, state.config_path.as_deref())?;
8275            if params.verify.unwrap_or(false) && report.verified {
8276                verify_project_database(&state.db_path, &state.root)?;
8277            }
8278            Self::with_selected_project_audit(
8279                &state,
8280                state.worktree.is_some(),
8281                render_root_report(&report),
8282            )
8283        })())
8284    }
8285
8286    /// Bind, move, or detach a project root and then make it active.
8287    #[tool(
8288        name = "atlas_root_set",
8289        description = "Bind a repository root, generate project-local MCP configs, and make it active for later MCP calls."
8290    )]
8291    fn atlas_root_set(
8292        &self,
8293        Parameters(params): Parameters<AtlasRootSetParams>,
8294    ) -> McpToolTextResult {
8295        Self::as_mcp_text((|| {
8296            let root = canonical_project_root(Path::new(&params.root))?;
8297            let report = crate::bind_project_root(
8298                &root,
8299                params.transition.unwrap_or(RootTransition::Bind),
8300                params.nearest_project.unwrap_or(false),
8301            )?;
8302            let state = Self::project_state_from_root(&root)?;
8303            self.set_active_project_state(state)?;
8304            Ok(render_root_report(&report))
8305        })())
8306    }
8307
8308    /// Return the effective `ProjectAtlas` config.
8309    #[tool(
8310        name = "atlas_config",
8311        description = "Return the effective ProjectAtlas scan, purpose, and output configuration."
8312    )]
8313    fn atlas_config(
8314        &self,
8315        Parameters(params): Parameters<AtlasProjectParams>,
8316    ) -> McpToolTextResult {
8317        Self::as_mcp_text((|| {
8318            let state = self.admin_project_root(params.project_path, params.worktree)?;
8319            let report = effective_config_report(&Self::load_config_for_state(&state)?);
8320            Self::encode_named_payload(MCP_PAYLOAD_CONFIG, &report)
8321        })())
8322    }
8323
8324    /// Return the effective `ProjectAtlas` ignore policy.
8325    #[tool(
8326        name = "atlas_ignore_list",
8327        description = "List effective ProjectAtlas manual ignore policy and inherited .gitignore status."
8328    )]
8329    fn atlas_ignore_list(
8330        &self,
8331        Parameters(params): Parameters<AtlasProjectParams>,
8332    ) -> McpToolTextResult {
8333        Self::as_mcp_text((|| {
8334            let state = self.admin_project_root(params.project_path, params.worktree)?;
8335            let report = list_ignore_entries(state.config_path.as_deref(), &state.root)?;
8336            Self::encode_named_payload(MCP_PAYLOAD_IGNORE, &report)
8337        })())
8338    }
8339
8340    /// Create a project-root `.gitignore` when it is absent.
8341    #[tool(
8342        name = "atlas_ignore_init_gitignore",
8343        description = "Create a project-root .gitignore when it is missing."
8344    )]
8345    fn atlas_ignore_init_gitignore(
8346        &self,
8347        Parameters(params): Parameters<AtlasProjectParams>,
8348    ) -> McpToolTextResult {
8349        Self::as_mcp_text((|| {
8350            let state = self.admin_project_root(params.project_path, params.worktree)?;
8351            let report = init_gitignore(state.config_path.as_deref(), &state.root)?;
8352            Self::encode_named_payload(MCP_PAYLOAD_GITIGNORE, &report)
8353        })())
8354    }
8355
8356    /// Add a manual `ProjectAtlas` ignore entry.
8357    #[tool(
8358        name = "atlas_ignore_add",
8359        description = "Add one manual ProjectAtlas ignore entry to the selected project's config."
8360    )]
8361    fn atlas_ignore_add(
8362        &self,
8363        Parameters(params): Parameters<AtlasIgnoreMutationParams>,
8364    ) -> McpToolTextResult {
8365        Self::as_mcp_text((|| {
8366            let state = self.admin_project_root(params.project_path, params.worktree)?;
8367            let kind = Self::parse_ignore_kind(params.kind.as_deref(), true)?.ok_or_else(|| {
8368                CliError::InvalidInput(MCP_ERROR_IGNORE_KIND_REQUIRED_FOR_ADD.to_owned())
8369            })?;
8370            let report = add_ignore_entry(
8371                state.config_path.as_deref(),
8372                &state.root,
8373                kind,
8374                &params.value,
8375            )?;
8376            Self::encode_named_payload(MCP_PAYLOAD_IGNORE, &report)
8377        })())
8378    }
8379
8380    /// Remove a manual `ProjectAtlas` ignore entry.
8381    #[tool(
8382        name = "atlas_ignore_remove",
8383        description = "Remove one manual ProjectAtlas ignore entry from the selected project's config."
8384    )]
8385    fn atlas_ignore_remove(
8386        &self,
8387        Parameters(params): Parameters<AtlasIgnoreMutationParams>,
8388    ) -> McpToolTextResult {
8389        Self::as_mcp_text((|| {
8390            let state = self.admin_project_root(params.project_path, params.worktree)?;
8391            let kind = Self::parse_ignore_kind(params.kind.as_deref(), false)?;
8392            let report = remove_ignore_entry(
8393                state.config_path.as_deref(),
8394                &state.root,
8395                kind,
8396                &params.value,
8397            )?;
8398            Self::encode_named_payload(MCP_PAYLOAD_IGNORE, &report)
8399        })())
8400    }
8401
8402    /// Scan a repository, import purpose metadata, rebuild symbols, and return an overview.
8403    #[tool(
8404        name = "atlas_scan",
8405        description = "Scan repository structure, import ProjectAtlas purpose metadata, rebuild symbols, and return a TOON overview."
8406    )]
8407    fn atlas_scan(&self, Parameters(params): Parameters<AtlasScanParams>) -> McpToolTextResult {
8408        Self::as_mcp_text((|| {
8409            let nearest_project = self.nearest_project_enabled(params.nearest_project);
8410            let background = params.background.unwrap_or(false);
8411            let (state, path) = if background {
8412                self.background_state_and_root_path(
8413                    params.project_path,
8414                    params.worktree,
8415                    params.path,
8416                    nearest_project,
8417                )?
8418            } else {
8419                self.state_and_root_path(
8420                    params.project_path,
8421                    params.worktree,
8422                    params.path,
8423                    nearest_project,
8424                )?
8425            };
8426            let symbol_options = SymbolBuildOptions::new(
8427                params.max_bytes.unwrap_or(MAX_SYMBOL_FILE_BYTES),
8428                params.max_workers,
8429                params.timeout_seconds,
8430            );
8431            let text_index_max_bytes = params.text_index_max_bytes;
8432            if background {
8433                let control_state = self.control_state.clone();
8434                let task = self.start_index_task(
8435                    McpTaskOperation::Scan,
8436                    symbol_options,
8437                    MCP_TOOL_ATLAS_OVERVIEW,
8438                    move |control, symbol_options| {
8439                        let plan = ScanRuntimePlan::for_path_controlled(
8440                            state.config_path.as_deref(),
8441                            &path,
8442                            text_index_max_bytes,
8443                            control,
8444                        )?;
8445                        let mut store = Self::open_mut_store(&state, &control_state)?;
8446                        run_scan_pipeline_controlled(&mut store, &plan, &symbol_options, control)?;
8447                        Ok(())
8448                    },
8449                )?;
8450                return Self::encode_named_payload(MCP_PAYLOAD_TASK_START, &task);
8451            }
8452            let control = index_work_control(&symbol_options);
8453            let plan = ScanRuntimePlan::for_path_controlled(
8454                state.config_path.as_deref(),
8455                &path,
8456                text_index_max_bytes,
8457                &control,
8458            )?;
8459            let mut store = Self::open_mut_store(&state, &self.control_state)?;
8460            let report =
8461                run_scan_pipeline_controlled(&mut store, &plan, &symbol_options, &control)?;
8462            Self::encode_named_payload(MCP_PAYLOAD_SCAN, &report)
8463        })())
8464    }
8465
8466    /// Render one verified overview response with optional request cancellation.
8467    fn atlas_overview_response(
8468        &self,
8469        params: AtlasProjectParams,
8470        context: Option<RequestContext<RoleServer>>,
8471    ) -> McpToolTextResult {
8472        Self::as_mcp_text((|| {
8473            let state = self.state_for_target(params.project_path, params.worktree)?;
8474            self.with_fresh_string_and_usage_for_request(&state, context, |store, stamp| {
8475                let overview = store.overview()?;
8476                let toon = render_overview(&overview);
8477                let usage = Self::telemetry_enabled()
8478                    .then(|| self.estimated_source_tokens_cached(&state, store, &stamp, None, None))
8479                    .and_then(Result::ok)
8480                    .map(|baseline_tokens| {
8481                        McpUsageIntent::directory_walk(
8482                            MCP_EVENT_ATLAS_OVERVIEW,
8483                            None,
8484                            None,
8485                            baseline_tokens,
8486                        )
8487                    });
8488                Ok((toon, usage))
8489            })
8490        })())
8491    }
8492
8493    /// Return the indexed repository overview.
8494    #[tool(
8495        name = "atlas_overview",
8496        description = "Return a compact TOON overview of indexed files, folders, and purpose coverage."
8497    )]
8498    fn atlas_overview(
8499        &self,
8500        Parameters(params): Parameters<AtlasProjectParams>,
8501        context: RequestContext<RoleServer>,
8502    ) -> McpToolTextResult {
8503        self.atlas_overview_response(params, Some(context))
8504    }
8505
8506    /// Rank folders before an agent chooses a work area.
8507    #[tool(
8508        name = "atlas_folders",
8509        description = "Rank repository folders by query and purpose so agents choose a work area before opening files."
8510    )]
8511    fn atlas_folders(
8512        &self,
8513        Parameters(params): Parameters<AtlasQueryParams>,
8514        context: RequestContext<RoleServer>,
8515    ) -> McpToolTextResult {
8516        self.atlas_folders_response(params, Some(context))
8517    }
8518
8519    /// Build the folders response with optional request telemetry context.
8520    fn atlas_folders_response(
8521        &self,
8522        params: AtlasQueryParams,
8523        context: Option<RequestContext<RoleServer>>,
8524    ) -> McpToolTextResult {
8525        Self::as_mcp_text((|| {
8526            let state = self.state_for_target(params.project_path, params.worktree)?;
8527            let query = Self::query_or_empty(params.query);
8528            self.with_fresh_string_and_usage_for_request(&state, context, |store, stamp| {
8529                let selected =
8530                    ranked_folder_nodes_with_reasons(store, &query, params.limit.unwrap_or(10))?;
8531                let toon = render_ranked_nodes(NODE_LABEL_FOLDERS, &selected);
8532                let usage = Self::telemetry_enabled()
8533                    .then(|| self.estimated_source_tokens_cached(&state, store, &stamp, None, None))
8534                    .and_then(Result::ok)
8535                    .map(|baseline_tokens| {
8536                        McpUsageIntent::directory_walk(
8537                            MCP_EVENT_ATLAS_FOLDERS,
8538                            None,
8539                            Some(query.clone()),
8540                            baseline_tokens,
8541                        )
8542                    });
8543                Ok((toon, usage))
8544            })
8545        })())
8546    }
8547
8548    /// Rank files after an agent has chosen a folder or query.
8549    #[tool(
8550        name = "atlas_files",
8551        description = "Rank repository files by query, purpose, optional folder, and optional indexed text fallback before an agent opens source."
8552    )]
8553    fn atlas_files(
8554        &self,
8555        Parameters(params): Parameters<AtlasFilesParams>,
8556        context: RequestContext<RoleServer>,
8557    ) -> McpToolTextResult {
8558        self.atlas_files_response(params, Some(context))
8559    }
8560
8561    /// Build the files response with optional request telemetry context.
8562    fn atlas_files_response(
8563        &self,
8564        params: AtlasFilesParams,
8565        context: Option<RequestContext<RoleServer>>,
8566    ) -> McpToolTextResult {
8567        Self::as_mcp_text((|| {
8568            let content_selection = parse_content_selection(params.content_selection.as_deref())?;
8569            let nearest_project = self.nearest_project_enabled(params.nearest_project);
8570            let (state, folder_filter, routed_project) = self.state_and_optional_folder_filter(
8571                params.project_path.as_deref(),
8572                params.worktree.as_deref(),
8573                params.folder.as_deref(),
8574                nearest_project,
8575            )?;
8576            let query = Self::query_or_empty(params.query);
8577            self.with_fresh_string_and_usage_for_request(&state, context, |store, stamp| {
8578                let selected = classified_ranked_file_nodes_with_reasons(
8579                    store,
8580                    &query,
8581                    folder_filter.as_deref(),
8582                    params.file_pattern.as_deref(),
8583                    params.limit.unwrap_or(10),
8584                    params.include_content.unwrap_or(false),
8585                    content_selection,
8586                )?;
8587                let toon = Self::with_selected_project_audit(
8588                    &state,
8589                    routed_project,
8590                    encode_agent_payload(&serde_json::json!({
8591                        NODE_LABEL_FILES: render_classified_ranked_file_rows(&selected),
8592                    })),
8593                )?;
8594                let usage = Self::telemetry_enabled()
8595                    .then(|| {
8596                        self.estimated_source_tokens_cached(
8597                            &state,
8598                            store,
8599                            &stamp,
8600                            folder_filter.as_deref(),
8601                            params.file_pattern.as_deref(),
8602                        )
8603                    })
8604                    .and_then(Result::ok)
8605                    .map(|baseline_tokens| {
8606                        McpUsageIntent::estimate(
8607                            MCP_EVENT_ATLAS_FILES,
8608                            params
8609                                .file_pattern
8610                                .clone()
8611                                .or_else(|| folder_filter.clone()),
8612                            Some(query.clone()),
8613                            baseline_tokens,
8614                        )
8615                    });
8616                Ok((toon, usage))
8617            })
8618        })())
8619    }
8620
8621    /// Recommend the next indexed folders, files, and inspection commands.
8622    #[tool(
8623        name = "atlas_next",
8624        description = "Recommend top indexed folders/files with reasons and deterministic follow-up commands for a task query."
8625    )]
8626    fn atlas_next(
8627        &self,
8628        Parameters(params): Parameters<AtlasNextParams>,
8629        context: RequestContext<RoleServer>,
8630    ) -> McpToolTextResult {
8631        Self::as_mcp_text((|| {
8632            let content_selection = parse_content_selection(params.content_selection.as_deref())?;
8633            let state = self.state_for_target(params.project_path, params.worktree)?;
8634            let query = Self::query_or_empty(params.query);
8635            self.with_fresh_string_and_usage_for_request(&state, Some(context), |store, stamp| {
8636                let report = next_step_report_with_selection(
8637                    store,
8638                    &query,
8639                    params.limit,
8640                    content_selection,
8641                )?;
8642                let payload = next_step_report_payload(&report);
8643                let toon = Self::encode_named_payload(MCP_PAYLOAD_NEXT, &payload)?;
8644                let usage = Self::telemetry_enabled()
8645                    .then(|| self.estimated_source_tokens_cached(&state, store, &stamp, None, None))
8646                    .and_then(Result::ok)
8647                    .map(|baseline_tokens| {
8648                        McpUsageIntent::directory_walk(
8649                            MCP_EVENT_ATLAS_NEXT,
8650                            None,
8651                            Some(query.clone()),
8652                            baseline_tokens,
8653                        )
8654                    });
8655                Ok((toon, usage))
8656            })
8657        })())
8658    }
8659
8660    /// Build a compact file outline.
8661    #[tool(
8662        name = "atlas_outline",
8663        description = "Return compact TOON outline and preview context for a selected file."
8664    )]
8665    fn atlas_outline(
8666        &self,
8667        Parameters(params): Parameters<AtlasOutlineParams>,
8668        context: RequestContext<RoleServer>,
8669    ) -> McpToolTextResult {
8670        Self::as_mcp_text((|| {
8671            let nearest_project = self.nearest_project_enabled(params.nearest_project);
8672            let resolved = self.state_and_file_key(
8673                params.project_path.as_deref(),
8674                params.worktree.as_deref(),
8675                &params.file,
8676                nearest_project,
8677            )?;
8678            let state = resolved.state;
8679            self.with_fresh_string_and_usage_for_request(&state, Some(context), |store, _stamp| {
8680                let file_key = validated_indexed_file_key(store, Path::new(&resolved.key))?;
8681                let content = read_indexed_file_content(store, &file_key)?;
8682                let language = store
8683                    .load_node_by_path(&file_key)?
8684                    .and_then(|node| node.node.language);
8685                let outline =
8686                    build_outline(&file_key, language, &content, params.lines.unwrap_or(12));
8687                let toon = Self::with_selected_project_audit(
8688                    &state,
8689                    resolved.routed_project,
8690                    render_outline(&outline),
8691                )?;
8692                let usage = Some(McpUsageIntent::text(
8693                    MCP_EVENT_ATLAS_OUTLINE,
8694                    Some(file_key),
8695                    content,
8696                ));
8697                Ok((toon, usage))
8698            })
8699        })())
8700    }
8701
8702    /// Render one verified structured file-summary response.
8703    fn atlas_file_summary_response(
8704        &self,
8705        params: &AtlasFileSummaryParams,
8706        context: Option<RequestContext<RoleServer>>,
8707    ) -> McpToolTextResult {
8708        Self::as_mcp_text((|| {
8709            let content_selection = parse_content_selection(params.content_selection.as_deref())?;
8710            let nearest_project = self.nearest_project_enabled(params.nearest_project);
8711            let resolved = self.state_and_file_key(
8712                params.project_path.as_deref(),
8713                params.worktree.as_deref(),
8714                &params.file,
8715                nearest_project,
8716            )?;
8717            let state = resolved.state;
8718            self.with_fresh_string_and_usage_for_request(&state, context, |store, _stamp| {
8719                let file_key = validated_indexed_file_key(store, Path::new(&resolved.key))?;
8720                let content = read_indexed_file_content(store, &file_key)?;
8721                let report = build_file_summary_from_source_with_selection(
8722                    store,
8723                    Path::new(&file_key),
8724                    params.limit.unwrap_or(DEFAULT_FILE_SUMMARY_LIMIT),
8725                    &content,
8726                    content_selection,
8727                )?;
8728                let rendered = if params.compact.unwrap_or(false) {
8729                    encode_agent_payload(&McpFileSummaryPayload {
8730                        file_summary: McpFileSummary::from(&report),
8731                    })
8732                } else {
8733                    render_file_summary(&report)
8734                };
8735                let toon =
8736                    Self::with_selected_project_audit(&state, resolved.routed_project, rendered)?;
8737                let usage = Some(McpUsageIntent::text(
8738                    MCP_EVENT_ATLAS_FILE_SUMMARY,
8739                    Some(report.file_path),
8740                    content,
8741                ));
8742                Ok((toon, usage))
8743            })
8744        })())
8745    }
8746
8747    /// Return deterministic structured file intelligence from the deep index.
8748    #[tool(
8749        name = "atlas_file_summary",
8750        description = "Return structured TOON file intelligence: file purpose, content summary, imports, symbols, line ranges, and calls."
8751    )]
8752    fn atlas_file_summary(
8753        &self,
8754        Parameters(params): Parameters<AtlasFileSummaryParams>,
8755        context: RequestContext<RoleServer>,
8756    ) -> McpToolTextResult {
8757        self.atlas_file_summary_response(&params, Some(context))
8758    }
8759
8760    /// Search selected indexed files with optional context lines.
8761    #[tool(
8762        name = "atlas_search",
8763        description = "Search indexed files with literal, regex, or fuzzy matching, file filters, pagination, and TOON results."
8764    )]
8765    fn atlas_search(
8766        &self,
8767        Parameters(params): Parameters<AtlasSearchParams>,
8768        context: RequestContext<RoleServer>,
8769    ) -> McpToolTextResult {
8770        Self::as_mcp_text((|| {
8771            let content_selection = parse_content_selection(params.content_selection.as_deref())?;
8772            let state =
8773                self.state_for_target(params.project_path.clone(), params.worktree.clone())?;
8774            self.with_fresh_string_and_usage_controlled_for_request(
8775                &state,
8776                Some(context),
8777                |store, _stamp, control| {
8778                    let report = search_indexed_files_with_control(
8779                        store,
8780                        &SearchQuery {
8781                            pattern: &params.pattern,
8782                            regex: params.regex.unwrap_or(false),
8783                            fuzzy: params.fuzzy.unwrap_or(false),
8784                            case_sensitive: params.case_sensitive.unwrap_or(false),
8785                            file_pattern: params.file_pattern.as_deref(),
8786                            context_lines: params.context_lines.unwrap_or(0),
8787                            start_index: params.start_index.unwrap_or(0),
8788                            limit: params.limit.unwrap_or(20),
8789                            content_selection,
8790                            retrieval_mode: params.retrieval_mode.unwrap_or_default().into(),
8791                        },
8792                        Some(control),
8793                    )?;
8794                    let toon = render_search_report(&report);
8795                    let usage = Some(McpUsageIntent::estimate(
8796                        MCP_EVENT_ATLAS_SEARCH,
8797                        params.file_pattern.clone(),
8798                        Some(params.pattern.clone()),
8799                        byte_count_to_tokens(report.searched_bytes),
8800                    ));
8801                    Ok((toon, usage))
8802                },
8803            )
8804        })())
8805    }
8806
8807    /// Return an exact line or symbol slice from a selected file.
8808    #[tool(
8809        name = "atlas_slice",
8810        description = "Return exact source for a selected line range or indexed symbol, after folder/file orientation."
8811    )]
8812    fn atlas_slice(
8813        &self,
8814        Parameters(params): Parameters<AtlasSliceParams>,
8815        context: RequestContext<RoleServer>,
8816    ) -> McpToolTextResult {
8817        Self::as_mcp_text((|| {
8818            let content_selection = parse_content_selection(params.content_selection.as_deref())?;
8819            let nearest_project = self.nearest_project_enabled(params.nearest_project);
8820            let resolved = self.state_and_file_key(
8821                params.project_path.as_deref(),
8822                params.worktree.as_deref(),
8823                &params.file,
8824                nearest_project,
8825            )?;
8826            let state = resolved.state;
8827            self.with_fresh_string_and_usage_for_request(&state, Some(context), |store, _stamp| {
8828                let file_key = validated_indexed_file_key(store, Path::new(&resolved.key))?;
8829                let file = PathBuf::from(&file_key);
8830                let content = read_indexed_file_content(store, &file_key)?;
8831                let output_budget = CodeSliceBudget::new(
8832                    params
8833                        .output_bytes
8834                        .unwrap_or(CodeSliceBudget::DEFAULT_OUTPUT_BYTES),
8835                )?;
8836                let report = if let Some(symbol) = params.symbol.as_ref() {
8837                    read_symbol_slice_from_source_bounded_with_selection(
8838                        store,
8839                        &file,
8840                        &SymbolSliceSelector {
8841                            name: symbol,
8842                            parent: params.symbol_parent.as_deref().and_then(nonempty_str),
8843                            kind: params.symbol_kind.as_deref().and_then(nonempty_str),
8844                            signature: params.symbol_signature.as_deref().and_then(nonempty_str),
8845                            line: params.symbol_line,
8846                        },
8847                        &content,
8848                        output_budget,
8849                        content_selection,
8850                    )?
8851                } else {
8852                    if params
8853                        .symbol_parent
8854                        .as_deref()
8855                        .and_then(nonempty_str)
8856                        .is_some()
8857                        || params
8858                            .symbol_kind
8859                            .as_deref()
8860                            .and_then(nonempty_str)
8861                            .is_some()
8862                        || params
8863                            .symbol_signature
8864                            .as_deref()
8865                            .and_then(nonempty_str)
8866                            .is_some()
8867                        || params.symbol_line.is_some()
8868                    {
8869                        return Err(CliError::InvalidInput(
8870                            SYMBOL_DISAMBIGUATOR_WITHOUT_SYMBOL_ERROR.to_string(),
8871                        ));
8872                    }
8873                    let start_line = params.start_line.ok_or_else(|| {
8874                        CliError::InvalidInput(START_LINE_REQUIRED_ERROR.to_string())
8875                    })?;
8876                    read_indexed_code_slice_from_source_bounded_with_selection(
8877                        store,
8878                        &file,
8879                        start_line,
8880                        params.end_line,
8881                        &content,
8882                        output_budget,
8883                        content_selection,
8884                    )?
8885                };
8886                let toon = report.fit_output(|report| {
8887                    Self::with_selected_project_audit(
8888                        &state,
8889                        resolved.routed_project,
8890                        render_code_slice(report),
8891                    )
8892                })?;
8893                let usage = Some(McpUsageIntent::text(
8894                    MCP_EVENT_ATLAS_SLICE,
8895                    Some(report.slice().path.clone()),
8896                    content,
8897                ));
8898                Ok((toon, usage))
8899            })
8900        })())
8901    }
8902
8903    /// Rebuild symbol graphs for indexed files.
8904    #[tool(
8905        name = "atlas_symbols_build",
8906        description = "Rebuild ProjectAtlas symbol graphs for indexed files and return a TOON build report."
8907    )]
8908    fn atlas_symbols_build(
8909        &self,
8910        Parameters(params): Parameters<AtlasScanParams>,
8911    ) -> McpToolTextResult {
8912        Self::as_mcp_text((|| {
8913            let nearest_project = self.nearest_project_enabled(params.nearest_project);
8914            let background = params.background.unwrap_or(false);
8915            let (state, path) = if background {
8916                self.background_state_and_root_path(
8917                    params.project_path,
8918                    params.worktree,
8919                    params.path,
8920                    nearest_project,
8921                )?
8922            } else {
8923                self.state_and_root_path(
8924                    params.project_path,
8925                    params.worktree,
8926                    params.path,
8927                    nearest_project,
8928                )?
8929            };
8930            let options = SymbolBuildOptions::new(
8931                params.max_bytes.unwrap_or(MAX_SYMBOL_FILE_BYTES),
8932                params.max_workers,
8933                params.timeout_seconds,
8934            );
8935            let text_index_max_bytes = params.text_index_max_bytes;
8936            if background {
8937                let control_state = self.control_state.clone();
8938                let task = self.start_index_task(
8939                    McpTaskOperation::SymbolsBuild,
8940                    options,
8941                    MCP_TOOL_ATLAS_SYMBOLS,
8942                    move |control, options| {
8943                        let plan = ScanRuntimePlan::for_path_controlled(
8944                            state.config_path.as_deref(),
8945                            &path,
8946                            text_index_max_bytes,
8947                            control,
8948                        )?;
8949                        let mut store = Self::open_mut_store(&state, &control_state)?;
8950                        run_symbol_build_pipeline_controlled(
8951                            &mut store, &plan, &options, None, control,
8952                        )?;
8953                        Ok(())
8954                    },
8955                )?;
8956                return Self::encode_named_payload(MCP_PAYLOAD_TASK_START, &task);
8957            }
8958            let control = index_work_control(&options);
8959            let plan = ScanRuntimePlan::for_path_controlled(
8960                state.config_path.as_deref(),
8961                &path,
8962                text_index_max_bytes,
8963                &control,
8964            )?;
8965            let mut store = Self::open_mut_store(&state, &self.control_state)?;
8966            let report =
8967                run_symbol_build_pipeline_controlled(&mut store, &plan, &options, None, &control)?;
8968            Self::encode_named_payload(MCP_PAYLOAD_SYMBOLS_BUILD, &report)
8969        })())
8970    }
8971
8972    /// Render one verified symbol-list response.
8973    fn atlas_symbols_response(
8974        &self,
8975        params: &AtlasSymbolsParams,
8976        context: Option<RequestContext<RoleServer>>,
8977    ) -> McpToolTextResult {
8978        Self::as_mcp_text((|| {
8979            let content_selection = parse_content_selection(params.content_selection.as_deref())?;
8980            let nearest_project = self.nearest_project_enabled(params.nearest_project);
8981            let (state, file, routed_project) = self.state_and_optional_file_key(
8982                params.project_path.as_deref(),
8983                params.worktree.as_deref(),
8984                params.file.as_deref(),
8985                nearest_project,
8986            )?;
8987            self.with_fresh_string_and_usage_for_request(&state, context, |store, _stamp| {
8988                let file = file
8989                    .as_deref()
8990                    .map(|path| validated_indexed_file_key(store, Path::new(path)))
8991                    .transpose()?;
8992                let symbols = store.load_classified_symbols(
8993                    file.as_deref(),
8994                    params.query.as_deref(),
8995                    content_selection,
8996                    params.limit.unwrap_or(50),
8997                )?;
8998                let toon = Self::with_selected_project_audit(
8999                    &state,
9000                    routed_project,
9001                    encode_agent_payload(&serde_json::json!({
9002                        NODE_LABEL_SYMBOLS: render_classified_symbol_rows(&symbols),
9003                    })),
9004                )?;
9005                let usage = Self::telemetry_enabled()
9006                    .then(|| {
9007                        estimated_source_tokens_for_paths(
9008                            store,
9009                            symbols
9010                                .iter()
9011                                .map(|classified| classified.symbol.path.as_str()),
9012                        )
9013                    })
9014                    .and_then(Result::ok)
9015                    .map(|baseline_tokens| {
9016                        McpUsageIntent::estimate(
9017                            MCP_EVENT_ATLAS_SYMBOLS,
9018                            file.clone(),
9019                            params.query.clone(),
9020                            baseline_tokens,
9021                        )
9022                    });
9023                Ok((toon, usage))
9024            })
9025        })())
9026    }
9027
9028    /// List indexed symbols.
9029    #[tool(
9030        name = "atlas_symbols",
9031        description = "List indexed symbols by optional file and query as compact TOON."
9032    )]
9033    fn atlas_symbols(
9034        &self,
9035        Parameters(params): Parameters<AtlasSymbolsParams>,
9036        context: RequestContext<RoleServer>,
9037    ) -> McpToolTextResult {
9038        self.atlas_symbols_response(&params, Some(context))
9039    }
9040
9041    /// Render the shared detailed/analysis contract through one selected store set.
9042    fn detailed_symbol_relations_response(
9043        state: &McpProjectState,
9044        routed_project: bool,
9045        file: &str,
9046        params: &AtlasSymbolRelationsParams,
9047        content_selection: ContentSelection,
9048        analysis: bool,
9049        stores: SymbolRelationStores<'_>,
9050        control: &IndexWorkControl,
9051    ) -> Result<(String, Option<McpUsageIntent>), CliError> {
9052        if params.query.is_some() {
9053            return Err(CliError::Service(ServiceError::InvalidInput(
9054                MCP_ERROR_DETAILED_RELATION_QUERY.to_string(),
9055            )));
9056        }
9057        let primary = stores.primary();
9058        let file = validated_indexed_file_key(primary, Path::new(file))?;
9059        let graph_file = RepositoryFilePath::new(Path::new(&file))
9060            .map_err(|error| CliError::Service(ServiceError::InvalidInput(error.to_string())))?;
9061        let anchor = if let Some(symbol) = params.symbol.as_ref() {
9062            if symbol.is_empty() {
9063                return Err(CliError::Service(ServiceError::InvalidInput(
9064                    MCP_ERROR_DETAILED_RELATION_SYMBOL.to_string(),
9065                )));
9066            }
9067            RelationAnchor::Symbol {
9068                file: graph_file,
9069                name: symbol.clone(),
9070                symbol_kind: params
9071                    .symbol_kind
9072                    .as_deref()
9073                    .and_then(nonempty_str)
9074                    .map(parse_symbol_kind)
9075                    .transpose()?,
9076                parent: params
9077                    .symbol_parent
9078                    .as_deref()
9079                    .and_then(nonempty_str)
9080                    .map(ToString::to_string),
9081                signature: params
9082                    .symbol_signature
9083                    .as_deref()
9084                    .and_then(nonempty_str)
9085                    .map(ToString::to_string),
9086            }
9087        } else {
9088            if params
9089                .symbol_parent
9090                .as_deref()
9091                .and_then(nonempty_str)
9092                .is_some()
9093                || params
9094                    .symbol_kind
9095                    .as_deref()
9096                    .and_then(nonempty_str)
9097                    .is_some()
9098                || params
9099                    .symbol_signature
9100                    .as_deref()
9101                    .and_then(nonempty_str)
9102                    .is_some()
9103            {
9104                return Err(CliError::Service(ServiceError::InvalidInput(
9105                    MCP_ERROR_DETAILED_RELATION_DISAMBIGUATOR.to_string(),
9106                )));
9107            }
9108            RelationAnchor::File { file: graph_file }
9109        };
9110        let rows = u32::try_from(params.limit.unwrap_or(50)).map_err(|_overflow| {
9111            CliError::Service(ServiceError::InvalidInput(
9112                MCP_ERROR_DETAILED_RELATION_LIMIT.to_string(),
9113            ))
9114        })?;
9115        let limits = GraphLimits::new(
9116            rows,
9117            params.occurrence_limit.unwrap_or(25),
9118            params.depth.unwrap_or(1),
9119            params.output_bytes.unwrap_or(256 * 1024),
9120        )
9121        .map_err(|error| CliError::Service(ServiceError::InvalidInput(error.to_string())))?;
9122        let relations = DetailedRelationQuery {
9123            anchor,
9124            direction: parse_relation_direction(
9125                params
9126                    .direction
9127                    .as_deref()
9128                    .unwrap_or(MCP_SYMBOL_RELATION_DIRECTION_DEFAULT),
9129            )?,
9130            relation: params
9131                .relation
9132                .as_deref()
9133                .map(parse_coverage_relation)
9134                .transpose()?,
9135            minimum_confidence: parse_relation_confidence(
9136                params
9137                    .minimum_confidence
9138                    .as_deref()
9139                    .unwrap_or(MCP_SYMBOL_RELATION_CONFIDENCE_DEFAULT),
9140            )?,
9141            resolution: parse_relation_resolution(
9142                params
9143                    .resolution
9144                    .as_deref()
9145                    .unwrap_or(MCP_SYMBOL_RELATION_RESOLUTION_DEFAULT),
9146            )?,
9147            content_selection,
9148            include_occurrences: params.include_occurrences.unwrap_or(false),
9149            budget: DetailedRelationBudget::from_graph_limits(limits).with_aggregate_limits(
9150                params.edge_limit,
9151                params.node_limit,
9152                params.visited_limit,
9153                params.occurrence_total_limit,
9154                params.intermediate_bytes,
9155                params.deadline_ms,
9156            )?,
9157            cursor: params.cursor.clone(),
9158        };
9159        let usage = matches!(&stores, SymbolRelationStores::Single(_))
9160            .then(|| {
9161                Self::telemetry_enabled()
9162                    .then(|| {
9163                        estimated_source_tokens_for_paths(primary, std::iter::once(file.as_str()))
9164                    })
9165                    .and_then(Result::ok)
9166                    .map(|baseline_tokens| {
9167                        McpUsageIntent::estimate(
9168                            MCP_EVENT_ATLAS_SYMBOL_RELATIONS,
9169                            Some(file.clone()),
9170                            params.symbol.clone(),
9171                            baseline_tokens,
9172                        )
9173                    })
9174            })
9175            .flatten();
9176
9177        if analysis {
9178            let mode = match params
9179                .analysis_mode
9180                .as_deref()
9181                .unwrap_or(MCP_RELATION_ANALYSIS_MODE_ARCHITECTURE)
9182            {
9183                MCP_RELATION_ANALYSIS_MODE_ARCHITECTURE => RelationAnalysisMode::Architecture,
9184                MCP_RELATION_ANALYSIS_MODE_IMPACT => RelationAnalysisMode::Impact,
9185                MCP_RELATION_ANALYSIS_MODE_TRACE => RelationAnalysisMode::Trace,
9186                MCP_RELATION_ANALYSIS_MODE_ENTRYPOINT => RelationAnalysisMode::Entrypoint,
9187                _unsupported => {
9188                    return Err(CliError::Service(ServiceError::InvalidInput(
9189                        MCP_ERROR_UNSUPPORTED_ANALYSIS_MODE.to_string(),
9190                    )));
9191                }
9192            };
9193            let trace_target = relation_analysis_trace_target(primary, params)?;
9194            let vcs_explicit =
9195                params.vcs.is_some() || params.vcs_base.is_some() || params.vcs_head.is_some();
9196            let vcs = relation_analysis_vcs(params)?;
9197            if mode == RelationAnalysisMode::Entrypoint
9198                && matches!(&stores, SymbolRelationStores::Federated(_))
9199            {
9200                return Err(CliError::Service(ServiceError::InvalidInput(
9201                    MCP_ERROR_ENTRYPOINT_FEDERATED.to_string(),
9202                )));
9203            }
9204            if mode == RelationAnalysisMode::Entrypoint
9205                && params
9206                    .entrypoints
9207                    .as_ref()
9208                    .is_some_and(|items| !items.is_empty())
9209                && (params.symbol.is_some()
9210                    || params.symbol_parent.is_some()
9211                    || params.symbol_kind.is_some()
9212                    || params.symbol_signature.is_some())
9213            {
9214                return Err(CliError::Service(ServiceError::InvalidInput(
9215                    MCP_ERROR_ENTRYPOINT_SYMBOL_SELECTOR.to_string(),
9216                )));
9217            }
9218            let entrypoint_profile = if mode == RelationAnalysisMode::Entrypoint {
9219                let anchors = match params.entrypoints.as_ref() {
9220                    Some(values) if !values.is_empty() => values
9221                        .iter()
9222                        .map(|value| serde_json::from_str::<RelationAnchor>(value))
9223                        .collect::<Result<Vec<_>, _>>()
9224                        .map_err(|error| {
9225                            let mut message = MCP_ERROR_ENTRYPOINT_ANCHORS_PREFIX.to_string();
9226                            message.push_str(&error.to_string());
9227                            CliError::Service(ServiceError::InvalidInput(message))
9228                        })?,
9229                    _ => vec![relations.anchor.clone()],
9230                };
9231                let relation_families = match params.profile_relations.as_ref() {
9232                    Some(values) if !values.is_empty() => values
9233                        .iter()
9234                        .map(|value| parse_coverage_relation(value))
9235                        .collect::<Result<Vec<_>, _>>()?,
9236                    _ => GraphRelationKind::ALL.to_vec(),
9237                };
9238                Some(EntrypointProfile {
9239                    name: params
9240                        .profile_name
9241                        .clone()
9242                        .unwrap_or_else(|| MCP_ENTRYPOINT_PROFILE_DEFAULT_NAME.to_string()),
9243                    anchors,
9244                    relations: relation_families,
9245                })
9246            } else {
9247                if params.profile_name.is_some()
9248                    || params
9249                        .entrypoints
9250                        .as_ref()
9251                        .is_some_and(|items| !items.is_empty())
9252                    || params
9253                        .profile_relations
9254                        .as_ref()
9255                        .is_some_and(|items| !items.is_empty())
9256                {
9257                    return Err(CliError::Service(ServiceError::InvalidInput(
9258                        MCP_ERROR_ENTRYPOINT_CONTROLS_MODE.to_string(),
9259                    )));
9260                }
9261                None
9262            };
9263            let query = RelationAnalysisQuery {
9264                relations,
9265                mode,
9266                trace_target,
9267                vcs: (mode == RelationAnalysisMode::Impact || vcs_explicit).then_some(vcs),
9268                include_communities: params.include_communities.unwrap_or(false),
9269                include_cycles: params.include_cycles.unwrap_or(false),
9270                include_dead_code: params.include_dead_code.unwrap_or(false),
9271                entrypoint_profile,
9272            };
9273            let toon = match stores {
9274                SymbolRelationStores::Single(store) => {
9275                    let draft = load_relation_analysis(store, &query, Some(control))?;
9276                    draft
9277                        .fit_output(|report, control| {
9278                            Self::with_selected_project_audit_controlled(
9279                                state,
9280                                routed_project,
9281                                controlled_named_output(
9282                                    OutputFormat::Toon,
9283                                    MCP_PAYLOAD_SYMBOL_RELATIONS,
9284                                    report,
9285                                    control,
9286                                )?,
9287                                control,
9288                            )
9289                        })?
9290                        .1
9291                }
9292                SymbolRelationStores::Federated(stores) => {
9293                    let draft = load_federated_relation_analysis(stores, &query, Some(control))?;
9294                    draft
9295                        .fit_output(|report, control| {
9296                            Self::with_selected_project_audit_controlled(
9297                                state,
9298                                routed_project,
9299                                controlled_named_output(
9300                                    OutputFormat::Toon,
9301                                    MCP_PAYLOAD_SYMBOL_RELATIONS,
9302                                    report,
9303                                    control,
9304                                )?,
9305                                control,
9306                            )
9307                        })?
9308                        .1
9309                }
9310            };
9311            return Ok((toon, usage));
9312        }
9313
9314        let toon = match stores {
9315            SymbolRelationStores::Single(store) => {
9316                let draft = load_detailed_relation_page(store, &relations, Some(control))?;
9317                draft
9318                    .fit_output(Some(control), |report| {
9319                        let payload = if params.compact.unwrap_or(false) {
9320                            Self::encode_named_payload(
9321                                MCP_PAYLOAD_SYMBOL_RELATIONS,
9322                                &McpCompactDetailedRelationReport::new(report, &file, params),
9323                            )?
9324                        } else {
9325                            Self::encode_named_payload(MCP_PAYLOAD_SYMBOL_RELATIONS, report)?
9326                        };
9327                        Self::with_selected_project_audit(state, routed_project, payload)
9328                    })?
9329                    .1
9330            }
9331            SymbolRelationStores::Federated(stores) => {
9332                let draft = load_federated_detailed_relations(stores, &relations, Some(control))?;
9333                draft
9334                    .fit_output(Some(control), |report| {
9335                        let payload = if params.compact.unwrap_or(false) {
9336                            Self::encode_named_payload(
9337                                MCP_PAYLOAD_SYMBOL_RELATIONS,
9338                                &McpCompactFederatedDetailedRelationReport::new(
9339                                    report, &file, params,
9340                                ),
9341                            )?
9342                        } else {
9343                            Self::encode_named_payload(MCP_PAYLOAD_SYMBOL_RELATIONS, report)?
9344                        };
9345                        Self::with_selected_project_audit(state, routed_project, payload)
9346                    })?
9347                    .1
9348            }
9349        };
9350        Ok((toon, usage))
9351    }
9352
9353    /// Render one verified legacy or detailed symbol-relation response.
9354    fn atlas_symbol_relations_response(
9355        &self,
9356        params: &AtlasSymbolRelationsParams,
9357        context: Option<RequestContext<RoleServer>>,
9358    ) -> McpToolTextResult {
9359        Self::as_mcp_text((|| {
9360            let (detailed, analysis) = match params
9361                .view
9362                .as_deref()
9363                .unwrap_or(MCP_SYMBOL_RELATION_VIEW_LEGACY)
9364            {
9365                MCP_SYMBOL_RELATION_VIEW_LEGACY => (false, false),
9366                MCP_SYMBOL_RELATION_VIEW_DETAILED => (true, false),
9367                MCP_SYMBOL_RELATION_VIEW_ANALYSIS => (true, true),
9368                _unsupported => {
9369                    return Err(CliError::Service(ServiceError::InvalidInput(
9370                        MCP_ERROR_SYMBOL_RELATION_VIEW.to_string(),
9371                    )));
9372                }
9373            };
9374            if params.compact.unwrap_or(false) && (!detailed || analysis) {
9375                return Err(CliError::Service(ServiceError::InvalidInput(
9376                    MCP_ERROR_COMPACT_DETAILED_RELATION_VIEW.to_string(),
9377                )));
9378            }
9379            if !analysis && relation_analysis_controls_present(params) {
9380                return Err(CliError::Service(ServiceError::InvalidInput(
9381                    MCP_ERROR_ANALYSIS_VIEW_REQUIRED.to_string(),
9382                )));
9383            }
9384            let content_selection = parse_content_selection(params.content_selection.as_deref())?;
9385            if !detailed && params.content_selection.is_some() {
9386                return Err(CliError::Service(ServiceError::InvalidInput(
9387                    MCP_ERROR_CONTENT_SELECTION_RELATION_VIEW.to_string(),
9388                )));
9389            }
9390            if params.roots.is_some() && params.worktrees.is_some() {
9391                return Err(CliError::Service(ServiceError::InvalidInput(
9392                    MCP_ERROR_FEDERATED_SELECTOR_CONFLICT.to_string(),
9393                )));
9394            }
9395            let federated_worktrees = params.worktrees.as_deref();
9396            if let Some(worktrees) = federated_worktrees {
9397                validate_federated_root_count(worktrees.len()).map_err(CliError::Service)?;
9398                if params.project_path.is_some() {
9399                    return Err(CliError::Service(ServiceError::InvalidInput(
9400                        MCP_ERROR_FEDERATED_PROJECT_PATH_CONFLICT.to_string(),
9401                    )));
9402                }
9403                if params.worktree.as_deref().is_some_and(|primary| {
9404                    worktrees
9405                        .first()
9406                        .is_none_or(|first| primary.trim() != first.trim())
9407                }) {
9408                    return Err(CliError::Service(ServiceError::InvalidInput(
9409                        MCP_ERROR_FEDERATED_PRIMARY_CONFLICT.to_string(),
9410                    )));
9411                }
9412            }
9413            let nearest_project = self.nearest_project_enabled(params.nearest_project);
9414            let selected_worktree = federated_worktrees
9415                .and_then(|worktrees| worktrees.first())
9416                .map(String::as_str)
9417                .or(params.worktree.as_deref());
9418            let profile_file = if analysis
9419                && params.analysis_mode.as_deref() == Some(MCP_RELATION_ANALYSIS_MODE_ENTRYPOINT)
9420            {
9421                params
9422                    .entrypoints
9423                    .as_ref()
9424                    .and_then(|values| values.first())
9425                    .map(|value| serde_json::from_str::<RelationAnchor>(value))
9426                    .transpose()
9427                    .map_err(|error| {
9428                        let mut message = MCP_ERROR_ENTRYPOINT_ANCHORS_PREFIX.to_string();
9429                        message.push_str(&error.to_string());
9430                        CliError::Service(ServiceError::InvalidInput(message))
9431                    })?
9432                    .map(|anchor| match anchor {
9433                        RelationAnchor::File { file } | RelationAnchor::Symbol { file, .. } => {
9434                            file.as_str().to_string()
9435                        }
9436                    })
9437            } else {
9438                None
9439            };
9440            let (state, file, routed_project) = self.state_and_optional_file_key(
9441                params.project_path.as_deref(),
9442                selected_worktree,
9443                params.file.as_deref().or(profile_file.as_deref()),
9444                nearest_project,
9445            )?;
9446            if params.roots.is_some() || federated_worktrees.is_some() {
9447                if !detailed {
9448                    return Err(CliError::Service(ServiceError::InvalidInput(
9449                        MCP_ERROR_FEDERATED_RELATION_VIEW.to_string(),
9450                    )));
9451                }
9452                let file = file.as_deref().ok_or_else(|| {
9453                    CliError::Service(ServiceError::InvalidInput(
9454                        MCP_ERROR_DETAILED_RELATION_FILE.to_string(),
9455                    ))
9456                })?;
9457                let control =
9458                    index_work_control(&SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, None, None))
9459                        .with_timeout_ceiling(Duration::from_millis(
9460                            params.deadline_ms.unwrap_or(10_000).clamp(1, 60_000),
9461                        ));
9462                let bridge = context
9463                    .map(|context| McpRequestCancellationBridge::start(&context, &control))
9464                    .transpose()?;
9465                let (roots, worktree_selections) = if let Some(worktrees) = federated_worktrees {
9466                    let (roots, selections) = self.federated_worktree_roots(worktrees)?;
9467                    (roots, Some(selections))
9468                } else {
9469                    (
9470                        params
9471                            .roots
9472                            .as_deref()
9473                            .unwrap_or_default()
9474                            .iter()
9475                            .map(PathBuf::from)
9476                            .collect::<Vec<_>>(),
9477                        None,
9478                    )
9479                };
9480                let worktree_labels = worktree_selections.as_ref().map(|selections| {
9481                    selections
9482                        .iter()
9483                        .map(|selection| selection.alias.clone())
9484                        .collect::<Vec<_>>()
9485                });
9486                let stores = open_federated_atlas_stores_for_project(
9487                    &state.db_path,
9488                    &state.root,
9489                    state.config_path.as_deref(),
9490                    &roots,
9491                    worktree_labels.as_deref(),
9492                    &control,
9493                )?;
9494                let stores = if let Some(selections) = worktree_selections.as_deref() {
9495                    Self::require_federated_worktree_identities(stores, selections)?
9496                } else {
9497                    stores
9498                };
9499                let result = Self::detailed_symbol_relations_response(
9500                    &state,
9501                    routed_project,
9502                    file,
9503                    params,
9504                    content_selection,
9505                    analysis,
9506                    SymbolRelationStores::Federated(stores),
9507                    &control,
9508                )
9509                .map(|(toon, _usage)| toon);
9510                if let Some(bridge) = bridge.as_ref() {
9511                    bridge.synchronize(&control);
9512                }
9513                drop(bridge);
9514                return result;
9515            }
9516            self.with_fresh_string_and_usage_controlled_for_request(
9517                &state,
9518                context,
9519                |store, _stamp, control| {
9520                    let file = file
9521                        .as_deref()
9522                        .map(|path| validated_indexed_file_key(store, Path::new(path)))
9523                        .transpose()?;
9524                    if detailed {
9525                        let file = file.as_deref().ok_or_else(|| {
9526                            CliError::Service(ServiceError::InvalidInput(
9527                                MCP_ERROR_DETAILED_RELATION_FILE.to_string(),
9528                            ))
9529                        })?;
9530                        return Self::detailed_symbol_relations_response(
9531                            &state,
9532                            routed_project,
9533                            file,
9534                            params,
9535                            content_selection,
9536                            analysis,
9537                            SymbolRelationStores::Single(store),
9538                            control,
9539                        );
9540                    }
9541
9542                    let relations = store.load_symbol_relations(
9543                        file.as_deref(),
9544                        params.query.as_deref(),
9545                        params.limit.unwrap_or(50),
9546                    )?;
9547                    let toon = Self::with_selected_project_audit(
9548                        &state,
9549                        routed_project,
9550                        render_symbol_relations(&relations),
9551                    )?;
9552                    let usage = Self::telemetry_enabled()
9553                        .then(|| {
9554                            estimated_source_tokens_for_paths(
9555                                store,
9556                                relations.iter().map(|relation| relation.path.as_str()),
9557                            )
9558                        })
9559                        .and_then(Result::ok)
9560                        .map(|baseline_tokens| {
9561                            McpUsageIntent::estimate(
9562                                MCP_EVENT_ATLAS_SYMBOL_RELATIONS,
9563                                file.clone(),
9564                                params.query.clone(),
9565                                baseline_tokens,
9566                            )
9567                        });
9568                    Ok((toon, usage))
9569                },
9570            )
9571        })())
9572    }
9573
9574    /// List indexed symbol relations.
9575    #[tool(
9576        name = "atlas_symbol_relations",
9577        description = "List imports, calls, dependencies, and containment edges as compact TOON."
9578    )]
9579    fn atlas_symbol_relations(
9580        &self,
9581        Parameters(params): Parameters<AtlasSymbolRelationsParams>,
9582        context: RequestContext<RoleServer>,
9583    ) -> McpToolTextResult {
9584        self.atlas_symbol_relations_response(&params, Some(context))
9585    }
9586
9587    /// Return structural health findings.
9588    #[tool(
9589        name = "atlas_health",
9590        description = "Return a bounded ProjectAtlas structural health page with optional category, severity, and path-prefix filters."
9591    )]
9592    fn atlas_health(
9593        &self,
9594        Parameters(params): Parameters<AtlasHealthParams>,
9595        context: RequestContext<RoleServer>,
9596    ) -> McpToolTextResult {
9597        Self::as_mcp_text((|| {
9598            let state =
9599                self.state_for_target(params.project_path.clone(), params.worktree.clone())?;
9600            if params.coverage.unwrap_or(false) {
9601                let query = coverage_query_from_params(&params)?;
9602                return self.with_fresh_string_and_usage_controlled_for_request(
9603                    &state,
9604                    Some(context),
9605                    |store, stamp, control| {
9606                        let mut report =
9607                            load_coverage_discovery_controlled(store, query.clone(), control)?;
9608                        let toon = finalize_coverage_output(OutputFormat::Toon, &mut report)?;
9609                        let usage = Self::telemetry_enabled()
9610                            .then(|| {
9611                                self.estimated_source_tokens_cached(
9612                                    &state, store, &stamp, None, None,
9613                                )
9614                            })
9615                            .and_then(Result::ok)
9616                            .map(|baseline_tokens| {
9617                                McpUsageIntent::directory_walk(
9618                                    MCP_EVENT_ATLAS_HEALTH,
9619                                    None,
9620                                    None,
9621                                    baseline_tokens,
9622                                )
9623                            });
9624                        Ok((toon, usage))
9625                    },
9626                );
9627            }
9628            if has_coverage_filters(&params) {
9629                return Err(CliError::InvalidInput(
9630                    MCP_ERROR_COVERAGE_FILTERS_REQUIRE_COVERAGE.to_string(),
9631                ));
9632            }
9633            let scope = if params.source_only.unwrap_or(false) {
9634                HealthScope::source_only()
9635            } else {
9636                HealthScope::all()
9637            };
9638            let query = health_query_from_params(&params, scope)?;
9639            self.with_fresh_string_and_usage_for_request(&state, Some(context), |store, stamp| {
9640                let page = store.unresolved_health_findings_page_current(&query)?;
9641                let toon = render_health_page(&page, &query);
9642                let usage = Self::telemetry_enabled()
9643                    .then(|| self.estimated_source_tokens_cached(&state, store, &stamp, None, None))
9644                    .and_then(Result::ok)
9645                    .map(|baseline_tokens| {
9646                        McpUsageIntent::directory_walk(
9647                            MCP_EVENT_ATLAS_HEALTH,
9648                            None,
9649                            None,
9650                            baseline_tokens,
9651                        )
9652                    });
9653                Ok((toon, usage))
9654            })
9655        })())
9656    }
9657
9658    /// Mark an intentional deterministic health finding as resolved.
9659    #[tool(
9660        name = "atlas_health_resolve",
9661        description = "Mark a deterministic ProjectAtlas health finding as agent-resolved with rationale."
9662    )]
9663    fn atlas_health_resolve(
9664        &self,
9665        Parameters(params): Parameters<AtlasHealthResolveParams>,
9666    ) -> McpToolTextResult {
9667        Self::as_mcp_text((|| {
9668            let state = self.state_for_target(params.project_path, params.worktree)?;
9669            let store = Self::open_existing_mut_store(&state, &self.control_state)?;
9670            let resolution = HealthResolution {
9671                finding_id: params.finding_id,
9672                category: params.category,
9673                path: params.path,
9674                related_path: params.related_path,
9675                rationale: params.rationale,
9676            };
9677            store.resolve_health_finding(&resolution)?;
9678            Self::encode_named_payload(MCP_PAYLOAD_HEALTH_RESOLUTION, &resolution)
9679        })())
9680    }
9681
9682    /// Run `ProjectAtlas` lint checks without terminating the MCP transport.
9683    #[tool(
9684        name = "atlas_lint",
9685        description = "Run ProjectAtlas lint checks and return an ok flag, CLI-compatible exit code, and report text."
9686    )]
9687    fn atlas_lint(&self, Parameters(params): Parameters<AtlasLintParams>) -> McpToolTextResult {
9688        Self::as_mcp_text((|| {
9689            let state =
9690                self.admin_project_root(params.project_path.clone(), params.worktree.clone())?;
9691            let report = Self::lint_report_for_state(&state, &params)?;
9692            Self::encode_named_payload(MCP_PAYLOAD_LINT, &report)
9693        })())
9694    }
9695
9696    /// Return token savings telemetry.
9697    #[tool(
9698        name = "atlas_token_report",
9699        description = "Return ProjectAtlas token-savings telemetry for the whole index or one session."
9700    )]
9701    fn atlas_token_report(
9702        &self,
9703        Parameters(params): Parameters<AtlasTokenParams>,
9704    ) -> McpToolTextResult {
9705        Self::as_mcp_text((|| {
9706            let state =
9707                self.state_for_target(params.project_path.clone(), params.worktree.clone())?;
9708            let repository_scope = params.session.is_none()
9709                && state.root == self.control_state.root
9710                && state.db_path == self.control_state.db_path;
9711            let load_report = |request: TokenReportRequest<'_>| {
9712                if repository_scope {
9713                    load_synchronized_repository_token_report(
9714                        &state.db_path,
9715                        &state.root,
9716                        state
9717                            .worktree
9718                            .as_ref()
9719                            .and_then(|selection| selection.control_project_instance_id),
9720                        request,
9721                    )
9722                } else {
9723                    let store = Self::open_read_store(&state)?;
9724                    load_token_report(&store, request).map_err(CliError::from)
9725                }
9726            };
9727            let include_chart = params.include_chart.unwrap_or(false);
9728            let chart_theme = Self::parse_token_chart_theme(params.theme.as_deref())?;
9729            if let Some(window) = params.trend_window.as_deref() {
9730                if params.benchmark_results.is_some() {
9731                    return Err(CliError::InvalidInput(
9732                        TOKEN_TREND_BENCHMARK_ERROR.to_string(),
9733                    ));
9734                }
9735                let window = TokenTrendWindow::parse(window).ok_or_else(|| {
9736                    CliError::InvalidInput(format!(
9737                        "unsupported token trend window {window:?}; {TOKEN_TREND_WINDOW_ERROR_SUFFIX}"
9738                    ))
9739                })?;
9740                let request = if repository_scope {
9741                    TokenReportRequest::RepositoryTrends { window }
9742                } else {
9743                    TokenReportRequest::Trends {
9744                        caller_label: params.session.as_deref(),
9745                        window,
9746                    }
9747                };
9748                let report = match load_report(request)? {
9749                    TokenReport::Trends(report) => report,
9750                    TokenReport::Overview(_) => {
9751                        return Err(CliError::InvalidInput(
9752                            TOKEN_TRENDS_RESULT_VARIANT_MISMATCH.to_string(),
9753                        ));
9754                    }
9755                };
9756                if include_chart {
9757                    let chart = render_token_trend_dashboard_plain_with_theme(&report, chart_theme);
9758                    let output = Self::encode_two_named_payloads(
9759                        MCP_PAYLOAD_TOKEN_TRENDS,
9760                        &report,
9761                        MCP_PAYLOAD_CHART,
9762                        &chart,
9763                    )?;
9764                    return Self::with_selected_project_audit(
9765                        &state,
9766                        state.worktree.is_some(),
9767                        output,
9768                    );
9769                }
9770                return Self::with_selected_project_audit(
9771                    &state,
9772                    state.worktree.is_some(),
9773                    render_token_trends(&report),
9774                );
9775            }
9776            let request = if repository_scope {
9777                TokenReportRequest::RepositoryOverview {
9778                    benchmark_results: params.benchmark_results.as_deref().map(Path::new),
9779                }
9780            } else {
9781                TokenReportRequest::Overview {
9782                    caller_label: params.session.as_deref(),
9783                    benchmark_results: params.benchmark_results.as_deref().map(Path::new),
9784                }
9785            };
9786            let overview = match load_report(request)? {
9787                TokenReport::Overview(overview) => overview,
9788                TokenReport::Trends(_) => {
9789                    return Err(CliError::InvalidInput(
9790                        TOKEN_OVERVIEW_RESULT_VARIANT_MISMATCH.to_string(),
9791                    ));
9792                }
9793            };
9794            if include_chart {
9795                let chart = render_token_dashboard_plain_with_theme(
9796                    &overview,
9797                    params.session.as_deref(),
9798                    chart_theme,
9799                );
9800                let output = Self::encode_two_named_payloads(
9801                    MCP_PAYLOAD_TOKEN_SAVINGS,
9802                    &overview,
9803                    MCP_PAYLOAD_CHART,
9804                    &chart,
9805                )?;
9806                return Self::with_selected_project_audit(&state, state.worktree.is_some(), output);
9807            }
9808            Self::with_selected_project_audit(
9809                &state,
9810                state.worktree.is_some(),
9811                render_token_overview(&overview),
9812            )
9813        })())
9814    }
9815
9816    /// Return repository-intelligence parity readiness.
9817    #[tool(
9818        name = "atlas_parity_report",
9819        description = "Return a ProjectAtlas repository-intelligence parity gate report for release and agent-runtime readiness."
9820    )]
9821    fn atlas_parity_report(
9822        &self,
9823        Parameters(params): Parameters<AtlasParityParams>,
9824        context: RequestContext<RoleServer>,
9825    ) -> McpToolTextResult {
9826        Self::as_mcp_text((|| {
9827            let state = self.state_for_target(params.project_path, params.worktree)?;
9828            let profile = params
9829                .profile
9830                .unwrap_or_else(|| crate::REPOSITORY_INTELLIGENCE_PROFILE.to_string());
9831            self.with_fresh_string_for_request(&state, Some(context), |store, _stamp| {
9832                Ok(render_parity_report(&build_parity_report(store, &profile)?))
9833            })
9834        })())
9835    }
9836
9837    /// Return local settings and cache/index locations.
9838    #[tool(
9839        name = "atlas_settings",
9840        description = "Return ProjectAtlas local settings, config, and durable index paths."
9841    )]
9842    fn atlas_settings(
9843        &self,
9844        Parameters(params): Parameters<AtlasProjectParams>,
9845    ) -> McpToolTextResult {
9846        Self::as_mcp_text((|| {
9847            let state = self.state_for_target(params.project_path, params.worktree)?;
9848            self.render_settings_with_capabilities(&state)
9849        })())
9850    }
9851
9852    /// Return watcher availability and operating mode.
9853    #[tool(
9854        name = "atlas_watch_status",
9855        description = "Return ProjectAtlas watcher availability and current operating mode."
9856    )]
9857    fn atlas_watch_status(
9858        &self,
9859        Parameters(params): Parameters<AtlasProjectParams>,
9860    ) -> McpToolTextResult {
9861        let state = match self.state_for_target(params.project_path, params.worktree) {
9862            Ok(state) => state,
9863            Err(error) => return Self::as_mcp_text(Err(error)),
9864        };
9865        let mut report = watcher_status_report(false);
9866        if !state.db_path.exists() {
9867            report
9868                .recommendation
9869                .push_str(WATCH_STATUS_SCAN_RECOMMENDATION);
9870        }
9871        Self::as_mcp_text(Ok(render_watch_status(&report)))
9872    }
9873
9874    /// Run one incremental refresh pass.
9875    #[tool(
9876        name = "atlas_watch_once",
9877        description = "Run one MCP-safe watcher refresh pass over the repository and rebuild changed symbols, with optional worker, timeout, and text-index size controls."
9878    )]
9879    fn atlas_watch_once(
9880        &self,
9881        Parameters(params): Parameters<AtlasWatchOnceParams>,
9882    ) -> McpToolTextResult {
9883        Self::as_mcp_text((|| {
9884            let nearest_project = self.nearest_project_enabled(params.nearest_project);
9885            let background = params.background.unwrap_or(false);
9886            let (state, path) = if background {
9887                self.background_state_and_root_path(
9888                    params.project_path,
9889                    params.worktree,
9890                    params.path,
9891                    nearest_project,
9892                )?
9893            } else {
9894                self.state_and_root_path(
9895                    params.project_path,
9896                    params.worktree,
9897                    params.path,
9898                    nearest_project,
9899                )?
9900            };
9901            let symbol_options = SymbolBuildOptions::new(
9902                MAX_SYMBOL_FILE_BYTES,
9903                params.max_workers,
9904                params.timeout_seconds,
9905            );
9906            let text_index_max_bytes = params.text_index_max_bytes;
9907            if background {
9908                let control_state = self.control_state.clone();
9909                let task = self.start_index_task(
9910                    McpTaskOperation::WatchOnce,
9911                    symbol_options,
9912                    MCP_TOOL_ATLAS_OVERVIEW,
9913                    move |control, symbol_options| {
9914                        let plan = ScanRuntimePlan::for_path_controlled(
9915                            state.config_path.as_deref(),
9916                            &path,
9917                            text_index_max_bytes,
9918                            control,
9919                        )?;
9920                        let mut store = Self::open_mut_store(&state, &control_state)?;
9921                        run_single_watch_refresh_controlled(
9922                            &mut store,
9923                            &plan,
9924                            &symbol_options,
9925                            control,
9926                        )?;
9927                        Ok(())
9928                    },
9929                )?;
9930                return Self::encode_named_payload(MCP_PAYLOAD_TASK_START, &task);
9931            }
9932            let control = index_work_control(&symbol_options);
9933            let plan = ScanRuntimePlan::for_path_controlled(
9934                state.config_path.as_deref(),
9935                &path,
9936                text_index_max_bytes,
9937                &control,
9938            )?;
9939            let mut store = Self::open_mut_store(&state, &self.control_state)?;
9940            let report =
9941                run_single_watch_refresh_controlled(&mut store, &plan, &symbol_options, &control)?;
9942            Self::encode_named_payload(MCP_PAYLOAD_WATCH, &report)
9943        })())
9944    }
9945
9946    /// Preview or remove legacy `.purpose` files.
9947    #[tool(
9948        name = "atlas_strip_legacy_purpose",
9949        description = "Preview or remove legacy .purpose files after their metadata has been imported to SQLite."
9950    )]
9951    fn atlas_strip_legacy_purpose(
9952        &self,
9953        Parameters(params): Parameters<AtlasStripLegacyParams>,
9954    ) -> McpToolTextResult {
9955        Self::as_mcp_text((|| {
9956            let nearest_project = self.nearest_project_enabled(params.nearest_project);
9957            let (state, path) = self.state_and_root_path(
9958                params.project_path,
9959                params.worktree,
9960                params.path,
9961                nearest_project,
9962            )?;
9963            let report = strip_legacy_purpose(
9964                &path,
9965                state.config_path.as_deref(),
9966                params.apply.unwrap_or(false),
9967                params.dry_run.unwrap_or(false),
9968                params
9969                    .strip_source_headers
9970                    .unwrap_or_else(|| state.config_path.is_some()),
9971            )?;
9972            Self::encode_named_payload(MCP_PAYLOAD_LEGACY_PURPOSE_MIGRATION, &report)
9973        })())
9974    }
9975
9976    /// Preview or remove local runtime index/cache files.
9977    #[tool(
9978        name = "atlas_reset_index",
9979        description = "Preview or clear ProjectAtlas local SQLite index/cache files for recovery."
9980    )]
9981    fn atlas_reset_index(
9982        &self,
9983        Parameters(params): Parameters<AtlasResetIndexParams>,
9984    ) -> McpToolTextResult {
9985        Self::as_mcp_text((|| {
9986            let state = self.state_for_target(params.project_path, params.worktree)?;
9987            let apply = params.apply.unwrap_or(false);
9988            let dry_run = params.dry_run.unwrap_or(false);
9989            let include_mcp_config = params.include_mcp_config.unwrap_or(false);
9990            let report = if apply && !dry_run {
9991                if let Some(selection) = state
9992                    .worktree
9993                    .as_ref()
9994                    .filter(|selection| selection.registration_id.is_some())
9995                {
9996                    self.reset_registered_worktree_index(&state, selection, include_mcp_config)?
9997                } else {
9998                    reset_index_files(&state.db_path, true, false, include_mcp_config)?
9999                }
10000            } else {
10001                reset_index_files(&state.db_path, apply, dry_run, include_mcp_config)?
10002            };
10003            Self::encode_named_payload(MCP_PAYLOAD_RESET_INDEX, &report)
10004        })())
10005    }
10006
10007    /// Generate a project-local MCP config document.
10008    #[tool(
10009        name = "atlas_mcp_config",
10010        description = "Return a generated ProjectAtlas MCP config document for mcp-json, codex, claude-code, or opencode hosts."
10011    )]
10012    fn atlas_mcp_config(
10013        &self,
10014        Parameters(params): Parameters<AtlasMcpConfigParams>,
10015    ) -> McpToolTextResult {
10016        Self::as_mcp_text((|| {
10017            let state = self.admin_project_root(params.project_path, params.worktree)?;
10018            let harness = Self::parse_harness_config(params.harness.as_deref())?;
10019            let server_name = params
10020                .server_name
10021                .unwrap_or_else(|| MCP_DEFAULT_CONFIG_SERVER_NAME.to_string());
10022            let report = build_harness_mcp_config_report(
10023                harness,
10024                &server_name,
10025                &state.db_path,
10026                state.config_path.as_deref(),
10027                params.nearest_project.unwrap_or(false),
10028            )?;
10029            Self::encode_named_payload(MCP_PAYLOAD_MCP_CONFIG, &report)
10030        })())
10031    }
10032
10033    /// Return runtime identity and compiled MCP tool surface.
10034    #[tool(
10035        name = "atlas_runtime_info",
10036        description = "Return ProjectAtlas runtime identity, version, capabilities, and compiled MCP tool names."
10037    )]
10038    fn atlas_runtime_info(
10039        &self,
10040        Parameters(_params): Parameters<AtlasProjectParams>,
10041    ) -> McpToolTextResult {
10042        let _session_scope = self.session.as_str();
10043        Self::as_mcp_text(Ok(render_runtime_info(&build_runtime_info())))
10044    }
10045
10046    /// Return a compact startup brief for agents.
10047    #[tool(
10048        name = "atlas_session_brief",
10049        description = "Return selected project identity, index state, ranked candidates, blockers, and typed next-call recommendations for agent startup."
10050    )]
10051    fn atlas_session_brief(
10052        &self,
10053        Parameters(params): Parameters<AtlasSessionBriefParams>,
10054        context: RequestContext<RoleServer>,
10055    ) -> McpToolTextResult {
10056        Self::as_mcp_text((|| {
10057            if params.compact.unwrap_or(false) {
10058                let brief = self.build_compact_session_brief(params, Some(context))?;
10059                Self::encode_named_payload(MCP_PAYLOAD_SESSION_BRIEF, &brief)
10060            } else {
10061                let brief = self.build_session_brief(params, Some(context))?;
10062                Self::encode_named_payload(MCP_PAYLOAD_SESSION_BRIEF, &brief)
10063            }
10064        })())
10065    }
10066
10067    /// Return typed status for one MCP task-progress record.
10068    #[tool(
10069        name = "atlas_task_status",
10070        description = "Return typed status for a bounded MCP task-progress record."
10071    )]
10072    fn atlas_task_status(
10073        &self,
10074        Parameters(params): Parameters<AtlasTaskParams>,
10075    ) -> McpToolTextResult {
10076        Self::as_mcp_text((|| {
10077            let status = self.task_status(params.task_id)?;
10078            Self::encode_named_payload(MCP_PAYLOAD_TASK_STATUS, &status)
10079        })())
10080    }
10081
10082    /// Request cancellation for one MCP task-progress record.
10083    #[tool(
10084        name = "atlas_task_cancel",
10085        description = "Request cancellation for a bounded MCP task-progress record."
10086    )]
10087    fn atlas_task_cancel(
10088        &self,
10089        Parameters(params): Parameters<AtlasTaskParams>,
10090    ) -> McpToolTextResult {
10091        Self::as_mcp_text((|| {
10092            let cancel = self.task_cancel(params.task_id)?;
10093            Self::encode_named_payload(MCP_PAYLOAD_TASK_CANCEL, &cancel)
10094        })())
10095    }
10096
10097    /// Return a bounded purpose curation queue.
10098    #[tool(
10099        name = "atlas_purpose_queue",
10100        description = "Return a bounded folder-first queue of ProjectAtlas paths that need agent purpose curation."
10101    )]
10102    fn atlas_purpose_queue(
10103        &self,
10104        Parameters(params): Parameters<AtlasPurposeQueueParams>,
10105        context: RequestContext<RoleServer>,
10106    ) -> McpToolTextResult {
10107        Self::as_mcp_text((|| {
10108            let state = self.state_for_target(
10109                params.health.project_path.clone(),
10110                params.health.worktree.clone(),
10111            )?;
10112            let query =
10113                health_query_from_params(&params.health, purpose_queue_scope(&params.health))?;
10114            let task = params
10115                .task
10116                .as_deref()
10117                .unwrap_or(MCP_PURPOSE_TASK_QUEUE)
10118                .to_string();
10119            self.with_fresh_string_and_usage_for_request(&state, Some(context), |store, stamp| {
10120                let page = purpose_curation_page(store, &query, &task)?;
10121                let toon = render_purpose_curation_page(&page);
10122                let usage = Self::telemetry_enabled()
10123                    .then(|| self.estimated_source_tokens_cached(&state, store, &stamp, None, None))
10124                    .and_then(Result::ok)
10125                    .map(|baseline_tokens| {
10126                        McpUsageIntent::directory_walk(
10127                            MCP_EVENT_ATLAS_PURPOSE_QUEUE,
10128                            None,
10129                            None,
10130                            baseline_tokens,
10131                        )
10132                    });
10133                Ok((toon, usage))
10134            })
10135        })())
10136    }
10137
10138    /// Set an agent-approved purpose in the durable index.
10139    #[tool(
10140        name = "atlas_purpose_set",
10141        description = "Set agent-approved ProjectAtlas purpose metadata for one indexed path."
10142    )]
10143    fn atlas_purpose_set(
10144        &self,
10145        Parameters(params): Parameters<AtlasPurposeSetParams>,
10146        context: RequestContext<RoleServer>,
10147    ) -> McpToolTextResult {
10148        Self::as_mcp_text((|| {
10149            let state = self.state_for_target(params.project_path, params.worktree)?;
10150            let node_key = Self::preflight_purpose_path(&state, &params.path)?;
10151            self.with_admitted_purpose_mutation(&state, Some(context), |store| {
10152                Self::require_indexed_purpose_path(store, &node_key)?;
10153                store.set_purpose(&node_key, &params.purpose, PurposeSource::Agent)?;
10154                let classification = if store
10155                    .load_node_by_path(&node_key)?
10156                    .is_some_and(|node| node.node.kind == projectatlas_core::NodeKind::File)
10157                {
10158                    store
10159                        .file_content_classifications_for_paths(std::slice::from_ref(&node_key))?
10160                        .first()
10161                        .map(|row| row.classification)
10162                } else {
10163                    None
10164                };
10165                Self::encode_serialized_payload(McpPurposeSetResponse {
10166                    purpose_set: McpPurposeSetPayload {
10167                        path: node_key,
10168                        classification,
10169                        status: PurposeStatus::Approved,
10170                        source: PurposeSource::Agent,
10171                        agent_reviewed: true,
10172                    },
10173                })
10174            })
10175        })())
10176    }
10177
10178    /// Batch-review existing purpose records through the MCP surface.
10179    #[tool(
10180        name = "atlas_purpose_review",
10181        description = "Preview or apply agent-reviewed ProjectAtlas purpose metadata for multiple indexed paths."
10182    )]
10183    fn atlas_purpose_review(
10184        &self,
10185        Parameters(params): Parameters<AtlasPurposeReviewParams>,
10186        context: RequestContext<RoleServer>,
10187    ) -> McpToolTextResult {
10188        Self::as_mcp_text((|| {
10189            let apply = params.apply.unwrap_or(false);
10190            let requests = params
10191                .items
10192                .into_iter()
10193                .map(|item| PurposeReviewRequest {
10194                    path: item.path,
10195                    purpose: item.purpose,
10196                    confirm_existing: item.confirm_existing.unwrap_or(false),
10197                    task: item.task,
10198                    work_key: item.work_key,
10199                    state_token: item.state_token,
10200                })
10201                .collect::<Vec<_>>();
10202            validate_purpose_review_admission(&requests)?;
10203            let state = self.state_for_target(params.project_path, params.worktree)?;
10204            if apply {
10205                return self.with_admitted_purpose_mutation(&state, Some(context), |store| {
10206                    let report = review_purposes(store, &requests, true)?;
10207                    Ok(render_purpose_review_report(&report))
10208                });
10209            }
10210            self.with_fresh_string_for_request(&state, Some(context), |store, _stamp| {
10211                let report = review_purposes(store, &requests, false)?;
10212                Ok(render_purpose_review_report(&report))
10213            })
10214        })())
10215    }
10216}
10217
10218#[allow(clippy::unused_async_trait_impl)]
10219#[tool_handler(router = self.tool_router)]
10220impl ServerHandler for ProjectAtlasMcpServer {
10221    fn get_info(&self) -> ServerInfo {
10222        ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
10223            .with_server_info(Implementation::new(
10224                MCP_SERVER_NAME,
10225                env!("CARGO_PKG_VERSION"),
10226            ))
10227            .with_instructions(MCP_SERVER_INSTRUCTIONS)
10228    }
10229}
10230
10231/// Return current Unix time in milliseconds for MCP task status records.
10232fn mcp_unix_time_ms() -> u128 {
10233    SystemTime::now()
10234        .duration_since(UNIX_EPOCH)
10235        .map_or(0, |duration| duration.as_millis())
10236}
10237
10238/// Recognize cooperative cancellation through each typed adapter error layer.
10239fn task_error_is_canceled(error: &CliError) -> bool {
10240    std::iter::successors(
10241        Some(error as &(dyn std::error::Error + 'static)),
10242        |source| source.source(),
10243    )
10244    .any(|source| {
10245        matches!(
10246            source.downcast_ref::<IndexWorkFailure>(),
10247            Some(IndexWorkFailure::Cancelled { .. })
10248        )
10249    })
10250}
10251
10252/// Retain one concise task failure without letting diagnostics grow unbounded.
10253fn bounded_task_error(error: &CliError) -> String {
10254    error
10255        .to_string()
10256        .chars()
10257        .take(MCP_TASK_ERROR_MAX_CHARS)
10258        .collect()
10259}
10260
10261#[cfg(test)]
10262mod tests {
10263    use super::*;
10264    use crate::atlas_map::init_project_with_config;
10265    use notify::{Event, EventKind, event::ModifyKind};
10266    use projectatlas_core::graph::{
10267        Completeness, ConfidenceClass, EntitySelector, ExtendedRelationKind, ExternalSelector,
10268        GraphEntity, GraphIdentityText, GraphRelationKind, LogicalRelation, PackageSelector,
10269        RelationResolution, RepositoryFilePath,
10270    };
10271    use projectatlas_core::symbols::RelationKind;
10272    use projectatlas_core::{
10273        CanonicalProjectRoot, IndexCancellation, IndexWorkStage, RankedConnectionDirection,
10274        RankedConnectionKind, RankedConnectionTarget,
10275    };
10276    use projectatlas_db::ProjectRootTransition;
10277    use std::collections::BTreeSet;
10278    use std::fs;
10279    use std::io;
10280    #[cfg(unix)]
10281    use std::os::unix::ffi::OsStringExt;
10282    use std::process::Command as StdCommand;
10283    use std::time::{Duration, Instant};
10284
10285    fn require(condition: bool, message: &str) -> Result<(), Box<dyn std::error::Error>> {
10286        if condition {
10287            Ok(())
10288        } else {
10289            Err(io::Error::other(message.to_string()).into())
10290        }
10291    }
10292
10293    /// Downgrade a current fixture to the released schema-19 worktree shape.
10294    fn drop_native_worktree_identity_schema(
10295        connection: &rusqlite::Connection,
10296    ) -> rusqlite::Result<()> {
10297        connection.execute_batch(
10298            "DROP INDEX IF EXISTS idx_worktree_registrations_active_native_administrative_directory;
10299             DROP INDEX IF EXISTS idx_worktree_registrations_active_native_root;
10300             ALTER TABLE worktree_registrations DROP COLUMN git_common_directory_identity;
10301             ALTER TABLE worktree_registrations DROP COLUMN git_administrative_directory_identity;
10302             ALTER TABLE worktree_registrations DROP COLUMN last_root_identity;",
10303        )
10304    }
10305
10306    /// Execute one test-owned Git fixture command and retain exact diagnostics on failure.
10307    fn run_fixture_command(command: &mut StdCommand) -> Result<String, Box<dyn std::error::Error>> {
10308        let output = command.output()?;
10309        if !output.status.success() {
10310            return Err(io::Error::other(format!(
10311                "fixture command failed: {}{}",
10312                String::from_utf8_lossy(&output.stdout),
10313                String::from_utf8_lossy(&output.stderr)
10314            ))
10315            .into());
10316        }
10317        Ok(String::from_utf8(output.stdout)?)
10318    }
10319
10320    struct RegisteredWorktreeRaceFixture {
10321        _temp: tempfile::TempDir,
10322        primary: PathBuf,
10323        control_root: PathBuf,
10324        linked: PathBuf,
10325        control_db: PathBuf,
10326        target_db: PathBuf,
10327        server: ProjectAtlasMcpServer,
10328        alias: WorktreeAlias,
10329        registration: WorktreeRegistration,
10330        selection: McpWorktreeSelection,
10331        state: McpProjectState,
10332        administrative_directory: PathBuf,
10333        administrative_identity: String,
10334    }
10335
10336    fn registered_worktree_race_fixture(
10337        alias: &str,
10338    ) -> Result<RegisteredWorktreeRaceFixture, Box<dyn std::error::Error>> {
10339        let temp = tempfile::tempdir()?;
10340        let primary = temp.path().join("control");
10341        let linked = temp.path().join("linked");
10342        fs::create_dir_all(primary.join("src"))?;
10343        run_fixture_command(StdCommand::new("git").current_dir(&primary).arg("init"))?;
10344        for (key, value) in [
10345            ("user.name", "ProjectAtlas Test"),
10346            ("user.email", "projectatlas@example.invalid"),
10347            ("commit.gpgsign", "false"),
10348            ("core.autocrlf", "false"),
10349        ] {
10350            run_fixture_command(
10351                StdCommand::new("git")
10352                    .current_dir(&primary)
10353                    .args(["config", key, value]),
10354            )?;
10355        }
10356        fs::write(primary.join("src/lib.rs"), "pub fn control() {}\n")?;
10357        run_fixture_command(
10358            StdCommand::new("git")
10359                .current_dir(&primary)
10360                .args(["add", "."]),
10361        )?;
10362        run_fixture_command(
10363            StdCommand::new("git")
10364                .current_dir(&primary)
10365                .args(["commit", "-m", "fixture"]),
10366        )?;
10367        run_fixture_command(
10368            StdCommand::new("git")
10369                .current_dir(&primary)
10370                .args(["worktree", "add", "-b", "captured"])
10371                .arg(&linked),
10372        )?;
10373
10374        let control_root = primary.canonicalize()?;
10375        let control_config = control_root.join(PROJECTATLAS_DIR_NAME).join("config.toml");
10376        let control_db = control_root
10377            .join(PROJECTATLAS_DIR_NAME)
10378            .join(PROJECTATLAS_DB_FILE_NAME);
10379        init_project_with_config(&control_root, Some(&control_config))?;
10380        let mut control = AtlasStore::open_for_project(&control_db, &control_root)?;
10381        let plan = ScanRuntimePlan::for_path(Some(&control_config), &control_root, None)?;
10382        run_scan_pipeline(
10383            &mut control,
10384            &plan,
10385            &SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, None, None),
10386        )?;
10387        let control_project_instance_id = control
10388            .project_instance_id()?
10389            .ok_or(DbError::ProjectInstanceIdentityMissing)?;
10390        let server = ProjectAtlasMcpServer::new(
10391            control_db.clone(),
10392            Some(control_config),
10393            "worktree-race".to_string(),
10394            false,
10395        );
10396        let repository = server.control_git_repository()?;
10397        let canonical_linked = linked.canonicalize()?;
10398        let entry = repository
10399            .worktrees
10400            .iter()
10401            .find(|entry| {
10402                ProjectAtlasMcpServer::active_worktree_root(entry)
10403                    == Some(canonical_linked.as_path())
10404            })
10405            .ok_or_else(|| io::Error::other("linked worktree was not discovered"))?;
10406        let administrative_directory = entry.administrative_directory.clone();
10407        let administrative_identity = git_administrative_identity(&administrative_directory)?;
10408        let alias = WorktreeAlias::parse(alias)?;
10409        let registration = control.register_worktree(
10410            &alias,
10411            &repository.common_directory,
10412            &administrative_directory,
10413            &administrative_identity,
10414            &canonical_linked,
10415            None,
10416            1,
10417        )?;
10418        drop(control);
10419        let target_db = canonical_linked
10420            .join(PROJECTATLAS_DIR_NAME)
10421            .join(PROJECTATLAS_DB_FILE_NAME);
10422        let selection = McpWorktreeSelection {
10423            alias: alias.to_string(),
10424            registration_id: Some(registration.registration_id),
10425            project_instance_id: None,
10426            control_project_instance_id: Some(control_project_instance_id),
10427        };
10428        let state = McpProjectState {
10429            root: canonical_linked,
10430            db_path: target_db.clone(),
10431            config_path: Some(linked.join(PROJECTATLAS_DIR_NAME).join("config.toml")),
10432            worktree: Some(selection.clone()),
10433        };
10434        Ok(RegisteredWorktreeRaceFixture {
10435            _temp: temp,
10436            primary: control_root.clone(),
10437            control_root,
10438            linked,
10439            control_db,
10440            target_db,
10441            server,
10442            alias,
10443            registration,
10444            selection,
10445            state,
10446            administrative_directory,
10447            administrative_identity,
10448        })
10449    }
10450
10451    fn replace_registered_worktree(
10452        fixture: &RegisteredWorktreeRaceFixture,
10453        branch: &str,
10454    ) -> Result<GitWorktreeEntry, Box<dyn std::error::Error>> {
10455        run_fixture_command(
10456            StdCommand::new("git")
10457                .current_dir(&fixture.primary)
10458                .args(["worktree", "remove", "--force"])
10459                .arg(&fixture.linked),
10460        )?;
10461        run_fixture_command(
10462            StdCommand::new("git")
10463                .current_dir(&fixture.primary)
10464                .args(["worktree", "add", "-b", branch])
10465                .arg(&fixture.linked),
10466        )?;
10467        let repository = fixture.server.control_git_repository()?;
10468        repository
10469            .worktrees
10470            .into_iter()
10471            .find(|entry| {
10472                ProjectAtlasMcpServer::active_worktree_root(entry)
10473                    == Some(fixture.state.root.as_path())
10474            })
10475            .ok_or_else(|| io::Error::other("replacement worktree was not discovered").into())
10476    }
10477
10478    /// Prepare one fully verified hydration candidate without publishing it.
10479    fn prepared_hydration_candidate(
10480        fixture: &RegisteredWorktreeRaceFixture,
10481    ) -> Result<
10482        (
10483            PreparedWorktreeHydrationCandidate,
10484            PathBuf,
10485            IndexWorkControl,
10486        ),
10487        Box<dyn std::error::Error>,
10488    > {
10489        fs::create_dir_all(
10490            fixture
10491                .target_db
10492                .parent()
10493                .ok_or_else(|| io::Error::other("target database has no parent"))?,
10494        )?;
10495        let source =
10496            open_atlas_store_read_only_for_project(&fixture.control_db, &fixture.control_root)?;
10497        let work_control =
10498            index_work_control(&SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, None, None));
10499        let mut candidate = source.prepare_worktree_hydration(
10500            &fixture.state.root,
10501            &fixture.target_db,
10502            &work_control,
10503        )?;
10504        let candidate_path = candidate.path()?.to_path_buf();
10505        candidate.accept_verified_source_state(&work_control)?;
10506        let candidate = candidate.prepare_activation(&work_control)?;
10507        Ok((candidate, candidate_path, work_control))
10508    }
10509
10510    #[test]
10511    fn task_errors_classify_only_typed_cancellation_as_canceled() {
10512        let stage = IndexWorkStage::RepositoryTraversal;
10513        assert!(task_error_is_canceled(&CliError::IndexWork(
10514            IndexWorkFailure::Cancelled { stage },
10515        )));
10516        assert!(task_error_is_canceled(&CliError::Fs(
10517            projectatlas_fs::FsError::IndexWork(IndexWorkFailure::Cancelled { stage }),
10518        )));
10519        assert!(task_error_is_canceled(&CliError::Db(DbError::IndexWork(
10520            IndexWorkFailure::Cancelled { stage },
10521        ))));
10522        assert!(task_error_is_canceled(&CliError::Service(
10523            ServiceError::Db(DbError::IndexWork(IndexWorkFailure::Cancelled { stage })),
10524        )));
10525        assert!(!task_error_is_canceled(&CliError::IndexWork(
10526            IndexWorkFailure::DeadlineExceeded { stage },
10527        )));
10528        assert!(!task_error_is_canceled(&CliError::Db(DbError::IndexWork(
10529            IndexWorkFailure::DeadlineExceeded { stage },
10530        ))));
10531        assert!(!task_error_is_canceled(&CliError::Fs(
10532            projectatlas_fs::FsError::IndexWork(IndexWorkFailure::ResourceLimitExceeded {
10533                stage,
10534                resource: projectatlas_core::IndexWorkResource::Entries,
10535                limit: 1,
10536                observed: 2,
10537            }),
10538        )));
10539    }
10540
10541    fn usage_test_project(
10542        parent: &Path,
10543        name: &str,
10544    ) -> Result<(McpProjectState, AtlasStore), Box<dyn std::error::Error>> {
10545        let root = parent.join(name);
10546        fs::create_dir_all(root.join(".projectatlas"))?;
10547        let db_path = root.join(".projectatlas").join("projectatlas.db");
10548        let store = AtlasStore::open_for_project(&db_path, &root)?;
10549        Ok((
10550            McpProjectState {
10551                root,
10552                db_path,
10553                config_path: None,
10554                worktree: None,
10555            },
10556            store,
10557        ))
10558    }
10559
10560    fn usage_runtime_identity(
10561        server: &ProjectAtlasMcpServer,
10562        state: &McpProjectState,
10563        store: &AtlasStore,
10564    ) -> Result<UsageRuntimeInstance, Box<dyn std::error::Error>> {
10565        let binding = McpUsageProjectBinding::capture(state, store)?;
10566        let project_instance = server
10567            .usage_runtime
10568            .lock()
10569            .map_err(|_poisoned| io::Error::other("usage runtime lock poisoned"))?
10570            .entries
10571            .iter()
10572            .find(|entry| entry.binding == binding)
10573            .map(|entry| Arc::clone(&entry.instance))
10574            .ok_or_else(|| io::Error::other("operating-system entropy was unavailable"))?;
10575        let identity = *project_instance
10576            .lock()
10577            .map_err(|_poisoned| io::Error::other("project usage lock poisoned"))?;
10578        Ok(identity)
10579    }
10580
10581    #[test]
10582    fn mcp_server_clones_share_one_runtime_identity() -> Result<(), Box<dyn std::error::Error>> {
10583        let temp = tempfile::tempdir()?;
10584        let (state, store) = usage_test_project(temp.path(), "selected-project")?;
10585        let first = ProjectAtlasMcpServer::new(
10586            state.db_path.clone(),
10587            None,
10588            "shared-label".to_string(),
10589            false,
10590        );
10591        first.record_usage_for_state(&state, &store, |_usage_instance| Ok(()));
10592        let first_identity = usage_runtime_identity(&first, &state, &store)?;
10593        let cloned = first.clone();
10594        require(
10595            usage_runtime_identity(&first, &state, &store)? == first_identity,
10596            "cloning an MCP server changed the original telemetry identity",
10597        )?;
10598        let restarted = ProjectAtlasMcpServer::new(
10599            state.db_path.clone(),
10600            None,
10601            "shared-label".to_string(),
10602            false,
10603        );
10604        restarted.record_usage_for_state(&state, &store, |_usage_instance| Ok(()));
10605        let restarted_identity = usage_runtime_identity(&restarted, &state, &store)?;
10606
10607        require(
10608            usage_runtime_identity(&cloned, &state, &store)? == first_identity,
10609            "cloning an MCP server changed its process-scoped telemetry identity",
10610        )?;
10611        require(
10612            restarted_identity != first_identity,
10613            "a separately constructed MCP server reused the prior runtime identity",
10614        )
10615    }
10616
10617    #[test]
10618    fn mcp_request_cancellation_bridge_reaches_index_work() -> Result<(), Box<dyn std::error::Error>>
10619    {
10620        let cancellation = Arc::new(std::sync::atomic::AtomicBool::new(false));
10621        let observed = Arc::clone(&cancellation);
10622        let control = IndexWorkControl::new(IndexCancellation::new(), None);
10623        let bridge = McpRequestCancellationBridge::start_with_probe(
10624            move || observed.load(Ordering::Acquire),
10625            &control,
10626        )?;
10627
10628        cancellation.store(true, Ordering::Release);
10629        let mut canceled = false;
10630        for _attempt in 0..100 {
10631            if matches!(
10632                control.check(IndexWorkStage::Publication),
10633                Err(IndexWorkFailure::Cancelled { .. })
10634            ) {
10635                canceled = true;
10636                break;
10637            }
10638            thread::sleep(Duration::from_millis(2));
10639        }
10640        drop(bridge);
10641
10642        require(
10643            canceled,
10644            "RMCP cancellation probe did not reach the shared index work control",
10645        )
10646    }
10647
10648    #[test]
10649    fn purpose_preflight_preserves_admitted_mutations_and_saved_source_repair()
10650    -> Result<(), Box<dyn std::error::Error>> {
10651        let temp = tempfile::tempdir()?;
10652        let root = temp.path();
10653        fs::create_dir(root.join(".projectatlas"))?;
10654        fs::write(root.join("source.rs"), "fn current() {}\n")?;
10655        let db_path = root.join(".projectatlas/projectatlas.db");
10656        let plan = ScanRuntimePlan::for_path(None, root, None)?;
10657        let mut store = open_atlas_store_for_project(&db_path, &plan.root)?;
10658        run_scan_pipeline(
10659            &mut store,
10660            &plan,
10661            &SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, None, None),
10662        )?;
10663        drop(store);
10664        let server =
10665            ProjectAtlasMcpServer::new(db_path, None, "purpose-preflight".to_owned(), false);
10666        let state = server.state_for_target(Some(normalize_native_path_display(root)), None)?;
10667        let control = IndexWorkControl::new(IndexCancellation::new(), None);
10668        let admission = server.source_observations.admit_mutation(
10669            &state.db_path,
10670            &state.root,
10671            state.config_path.as_deref(),
10672            &control,
10673        )?;
10674        let store = ProjectAtlasMcpServer::open_existing_mut_store(&state, &server.control_state)?;
10675        let transaction = store.begin_purpose_mutation()?;
10676        store.set_purpose(".", "Accepted repository purpose", PurposeSource::Agent)?;
10677        let absolute = normalize_native_path_display(root.join("source.rs"));
10678        for path in [absolute.as_str(), "", "missing.rs"] {
10679            require(
10680                matches!(
10681                    ProjectAtlasMcpServer::preflight_purpose_path(&state, path),
10682                    Err(CliError::InvalidInput(_))
10683                ),
10684                "invalid purpose path passed admission preflight",
10685            )?;
10686            admission.verify()?;
10687        }
10688        transaction.commit()?;
10689        require(
10690            store.load_node_by_path(".")?.is_some_and(|node| {
10691                node.purpose.purpose.as_deref() == Some("Accepted repository purpose")
10692            }),
10693            "rejected purpose requests prevented the admitted mutation from committing",
10694        )?;
10695        let identity = store.captured_project_binding()?.project_instance_id;
10696        drop(store);
10697        let saved_path = root.join("added.rs");
10698        let saved_source = "fn added() {}\n";
10699        fs::write(&saved_path, saved_source)?;
10700        let node_key = ProjectAtlasMcpServer::preflight_purpose_path(&state, "added.rs")?;
10701        server.with_admitted_purpose_mutation_controlled(&state, &control, None, |store| {
10702            ProjectAtlasMcpServer::require_indexed_purpose_path(store, &node_key)?;
10703            store.set_purpose(&node_key, "New saved source", PurposeSource::Agent)?;
10704            Ok(())
10705        })?;
10706        let store = ProjectAtlasMcpServer::open_read_store(&state)?;
10707        require(
10708            store.captured_project_binding()?.project_instance_id == identity
10709                && store.load_node_by_path("added.rs")?.is_some_and(|node| {
10710                    node.purpose.purpose.as_deref() == Some("New saved source")
10711                })
10712                && fs::read_to_string(saved_path)? == saved_source,
10713            "purpose preflight prevented exact saved-source repair or changed source identity",
10714        )?;
10715        store.finish_index_read_snapshot()?;
10716        Ok(())
10717    }
10718
10719    #[test]
10720    fn purpose_preflight_preserves_selected_worktree_error_context()
10721    -> Result<(), Box<dyn std::error::Error>> {
10722        let fixture = registered_worktree_race_fixture("purpose-target")?;
10723        fs::create_dir_all(fixture.state.root.join(PROJECTATLAS_DIR_NAME))?;
10724        // The selected alias now points at an index belonging to another root.
10725        drop(AtlasStore::open_for_project(
10726            &fixture.target_db,
10727            &fixture.control_root,
10728        )?);
10729        let before = fs::read(&fixture.target_db)?;
10730        let error = ProjectAtlasMcpServer::preflight_purpose_path(&fixture.state, "src/lib.rs")
10731            .err()
10732            .ok_or_else(|| io::Error::other("mismatched preflight was accepted"))?;
10733        require(
10734            matches!(&error, CliError::ProjectMismatch(report)
10735                if report.worktree.as_deref() == Some("purpose-target")),
10736            "purpose preflight lost the selected worktree context",
10737        )?;
10738        let payload = ProjectAtlasMcpServer::encode_error_payload(&error);
10739        let value: serde_json::Value = toon_format::decode_default(&payload)?;
10740        require(
10741            value
10742                .pointer("/error/kind")
10743                .and_then(serde_json::Value::as_str)
10744                == Some("project_mismatch")
10745                && value
10746                    .pointer("/error/project_mismatch/worktree")
10747                    .and_then(serde_json::Value::as_str)
10748                    == Some("purpose-target"),
10749            "MCP mismatch payload lost the selected worktree alias",
10750        )?;
10751        let mut explicit = fixture.state.clone();
10752        explicit.worktree = None;
10753        require(
10754            matches!(
10755                ProjectAtlasMcpServer::preflight_purpose_path(&explicit, "src/lib.rs"),
10756                Err(CliError::ProjectMismatch(report)) if report.worktree.is_none()
10757            ),
10758            "explicit-project preflight acquired an unrelated worktree alias",
10759        )?;
10760        require(
10761            fs::read(&fixture.target_db)? == before,
10762            "rejected purpose preflight changed the mismatched database",
10763        )?;
10764        Ok(())
10765    }
10766
10767    #[test]
10768    fn purpose_mutation_synchronously_rolls_back_request_cancellation()
10769    -> Result<(), Box<dyn std::error::Error>> {
10770        let temp = tempfile::tempdir()?;
10771        let root = temp.path().join("purpose-cancellation");
10772        fs::create_dir_all(root.join(".projectatlas"))?;
10773        fs::write(root.join("source.rs"), "fn current() {}\n")?;
10774        let db_path = root.join(".projectatlas/projectatlas.db");
10775        let plan = ScanRuntimePlan::for_path(None, &root, None)?;
10776        let mut store = open_atlas_store_for_project(&db_path, &plan.root)?;
10777        run_scan_pipeline(
10778            &mut store,
10779            &plan,
10780            &SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, None, None),
10781        )?;
10782        let before_revision = store.authored_purpose_revision()?;
10783        let before_purpose = store
10784            .load_node_by_path("source.rs")?
10785            .ok_or_else(|| io::Error::other("indexed cancellation source missing"))?
10786            .purpose;
10787        drop(store);
10788
10789        let server = ProjectAtlasMcpServer::new(
10790            db_path.clone(),
10791            None,
10792            "purpose-cancellation".to_string(),
10793            false,
10794        );
10795        let state = server.state_for_target(Some(normalize_native_path_display(&root)), None)?;
10796        let control = IndexWorkControl::new(IndexCancellation::new(), None);
10797        let cancelled = Arc::new(std::sync::atomic::AtomicBool::new(false));
10798        let cancellation_probe = Arc::clone(&cancelled);
10799        let bridge = McpRequestCancellationBridge::start_with_probe(
10800            move || cancellation_probe.load(Ordering::Acquire),
10801            &control,
10802        )?;
10803        let result = server.with_admitted_purpose_mutation_controlled(
10804            &state,
10805            &control,
10806            Some(&bridge),
10807            |store| {
10808                store.set_purpose("source.rs", "Canceled purpose", PurposeSource::Agent)?;
10809                cancelled.store(true, Ordering::Release);
10810                Ok(())
10811            },
10812        );
10813        drop(bridge);
10814
10815        require(
10816            matches!(result, Err(CliError::IndexWork(_))),
10817            "request cancellation did not reject the purpose transaction",
10818        )?;
10819        let store = open_atlas_store_for_project(&db_path, &state.root)?;
10820        require(
10821            store.authored_purpose_revision()? == before_revision,
10822            "request cancellation advanced the authored-purpose revision",
10823        )?;
10824        require(
10825            store
10826                .load_node_by_path("source.rs")?
10827                .is_some_and(|node| node.purpose == before_purpose),
10828            "request cancellation persisted the rejected purpose",
10829        )
10830    }
10831
10832    #[test]
10833    fn mcp_same_path_project_identity_rotation_starts_a_distinct_runtime_entry()
10834    -> Result<(), Box<dyn std::error::Error>> {
10835        if telemetry_disabled() {
10836            return Ok(());
10837        }
10838        let temp = tempfile::tempdir()?;
10839        let (state, store) = usage_test_project(temp.path(), "selected-project")?;
10840        let server = ProjectAtlasMcpServer::new(
10841            state.db_path.clone(),
10842            None,
10843            "shared-label".to_string(),
10844            false,
10845        );
10846        let old_project_identity = store.captured_project_binding()?.project_instance_id;
10847        server.record_usage_for_state(&state, &store, |_usage_instance| Ok(()));
10848        let old_runtime_identity = usage_runtime_identity(&server, &state, &store)?;
10849        drop(store);
10850
10851        AtlasStore::transition_project_root(
10852            &state.db_path,
10853            &state.root,
10854            projectatlas_db::ProjectRootTransition::Detach,
10855        )?;
10856        let detached_store = AtlasStore::open_for_project(&state.db_path, &state.root)?;
10857        let detached_project_identity = detached_store
10858            .captured_project_binding()?
10859            .project_instance_id;
10860        server.record_usage_for_state(&state, &detached_store, |_usage_instance| Ok(()));
10861        let detached_runtime_identity = usage_runtime_identity(&server, &state, &detached_store)?;
10862        let tracked = server
10863            .usage_runtime
10864            .lock()
10865            .map_err(|_poisoned| io::Error::other("usage runtime lock poisoned"))?
10866            .entries
10867            .len();
10868
10869        require(
10870            detached_project_identity != old_project_identity,
10871            "detach did not rotate the captured project identity",
10872        )?;
10873        require(
10874            detached_runtime_identity != old_runtime_identity,
10875            "same-path detach reused the previous project's telemetry identity",
10876        )?;
10877        require(
10878            tracked == 2,
10879            "same-path project identities did not retain distinct bounded runtime entries",
10880        )
10881    }
10882
10883    #[test]
10884    fn mcp_telemetry_rotates_inactive_bindings_without_dropping_later_worktrees()
10885    -> Result<(), Box<dyn std::error::Error>> {
10886        if telemetry_disabled() {
10887            return Ok(());
10888        }
10889        let temp = tempfile::tempdir()?;
10890        let (state, control) = usage_test_project(temp.path(), "control")?;
10891        let server =
10892            ProjectAtlasMcpServer::new(state.db_path, None, "worktree-capacity".to_string(), false);
10893        let common = temp.path().join("common.git");
10894        let event = usage_from_estimates_with_context(
10895            "worktree-capacity",
10896            "atlas_overview",
10897            None,
10898            None,
10899            100,
10900            10,
10901            TOKEN_BUCKET_NAVIGATION_AVOIDANCE,
10902            TOKEN_BASELINE_SELECTED_CANDIDATES,
10903            TOKEN_CONFIDENCE_INFERRED,
10904        );
10905        for index in 0..=MCP_TELEMETRY_PROJECT_BINDING_LIMIT {
10906            let registration = control.register_worktree(
10907                &WorktreeAlias::parse(&format!("worktree-{index:03}"))?,
10908                &common,
10909                &common.join(format!("worktrees/{index:03}")),
10910                &format!("{:064x}", index + 1),
10911                &temp.path().join(format!("worktree-{index:03}")),
10912                Some(ProjectInstanceId::from_bytes(
10913                    [u8::try_from(index + 1)?; 16],
10914                )?),
10915                u64::try_from(index + 1)?,
10916            )?;
10917            server.record_usage_for_origin(
10918                &server.control_state,
10919                &control,
10920                Some(registration.registration_id),
10921                |usage_instance| {
10922                    usage_instance.record_for_worktree(
10923                        &control,
10924                        registration.registration_id,
10925                        &event,
10926                    )
10927                },
10928            );
10929        }
10930
10931        require(
10932            control.repository_token_overview()?.calls == MCP_TELEMETRY_PROJECT_BINDING_LIMIT + 1,
10933            "a worktree beyond the in-memory telemetry bound lost its accepted event",
10934        )?;
10935        let runtime = server
10936            .usage_runtime
10937            .lock()
10938            .map_err(|_poisoned| io::Error::other("usage runtime lock poisoned"))?;
10939        require(
10940            runtime.entries.len() == MCP_TELEMETRY_PROJECT_BINDING_LIMIT,
10941            "telemetry project binding registry exceeded its hard bound",
10942        )?;
10943        drop(runtime);
10944        require(
10945            control.telemetry_retention_state()?.active_instance_rows
10946                == MCP_TELEMETRY_PROJECT_BINDING_LIMIT,
10947            "inactive binding rotation did not seal before replacement",
10948        )?;
10949        server.seal_usage_instances_for_projects();
10950        require(
10951            control.telemetry_retention_state()?.active_instance_rows == 0,
10952            "rotated telemetry bindings were not sealed at MCP shutdown",
10953        )
10954    }
10955
10956    #[test]
10957    fn routed_worktree_telemetry_preserves_session_baseline_identity()
10958    -> Result<(), Box<dyn std::error::Error>> {
10959        if telemetry_disabled() {
10960            return Ok(());
10961        }
10962        let temp = tempfile::tempdir()?;
10963        let (state, control) = usage_test_project(temp.path(), "control")?;
10964        let server =
10965            ProjectAtlasMcpServer::new(state.db_path, None, "worktree-scale".to_string(), false);
10966        let common = temp.path().join("common.git");
10967        let event = usage_from_estimates_with_context(
10968            "worktree-scale",
10969            "atlas_overview",
10970            None,
10971            None,
10972            100,
10973            10,
10974            TOKEN_BUCKET_NAVIGATION_AVOIDANCE,
10975            TOKEN_BASELINE_SELECTED_CANDIDATES,
10976            TOKEN_CONFIDENCE_INFERRED,
10977        );
10978        let registration = control.register_worktree(
10979            &WorktreeAlias::parse("worktree-001")?,
10980            &common,
10981            &common.join("worktrees/001"),
10982            &format!("{:064x}", 1),
10983            &temp.path().join("worktree-001"),
10984            Some(ProjectInstanceId::from_bytes([1; 16])?),
10985            1,
10986        )?;
10987        for _ in 0..2 {
10988            server.record_usage_for_origin(
10989                &server.control_state,
10990                &control,
10991                Some(registration.registration_id),
10992                |usage_instance| {
10993                    usage_instance.record_for_worktree(
10994                        &control,
10995                        registration.registration_id,
10996                        &event,
10997                    )
10998                },
10999            );
11000        }
11001
11002        let overview = control.repository_token_overview()?;
11003
11004        require(
11005            overview.calls == 2,
11006            "alias-routed modeled usage did not retain both accepted calls",
11007        )?;
11008        require(
11009            overview.deduped_modeled_tokens_avoided == 80,
11010            "alias-routed modeled usage did not reuse the session baseline witness",
11011        )?;
11012        require(
11013            overview.repeated_baselines_deduped == 1,
11014            "alias-routed modeled usage did not classify the repeated baseline",
11015        )?;
11016        require(
11017            control.telemetry_retention_state()?.active_instance_rows == 1,
11018            "alias-routed usage did not retain one bounded session identity",
11019        )?;
11020        server.seal_usage_instances_for_projects();
11021        require(
11022            control.telemetry_retention_state()?.active_instance_rows == 0,
11023            "alias-routed session identity was not sealed at MCP shutdown",
11024        )
11025    }
11026
11027    #[test]
11028    fn mcp_telemetry_busy_project_does_not_block_another_project()
11029    -> Result<(), Box<dyn std::error::Error>> {
11030        if telemetry_disabled() {
11031            return Ok(());
11032        }
11033        let server = ProjectAtlasMcpServer::new(
11034            PathBuf::from("startup.db"),
11035            None,
11036            "shared-label".to_string(),
11037            false,
11038        );
11039        let temp = tempfile::tempdir()?;
11040        let (state_a, store_a) = usage_test_project(temp.path(), "project-a")?;
11041        let (state_b, store_b) = usage_test_project(temp.path(), "project-b")?;
11042        let (entered_tx, entered_rx) = std::sync::mpsc::sync_channel(1);
11043        let (release_tx, release_rx) = std::sync::mpsc::sync_channel(1);
11044        let server_a = server.clone();
11045        let a_handle = std::thread::spawn(move || -> Result<(), String> {
11046            server_a.record_usage_for_state(&state_a, &store_a, |_usage_instance| {
11047                entered_tx.send(()).map_err(|error| {
11048                    CliError::InvalidInput(format!("test coordination failed: {error}"))
11049                })?;
11050                release_rx
11051                    .recv_timeout(Duration::from_secs(5))
11052                    .map_err(|error| {
11053                        CliError::InvalidInput(format!("test coordination failed: {error}"))
11054                    })?;
11055                Ok(())
11056            });
11057            Ok(())
11058        });
11059        entered_rx.recv_timeout(Duration::from_secs(2))?;
11060
11061        let (done_tx, done_rx) = std::sync::mpsc::sync_channel(1);
11062        let server_b = server;
11063        let b_handle = std::thread::spawn(move || -> Result<(), String> {
11064            server_b.record_usage_for_state(&state_b, &store_b, |_usage_instance| {
11065                done_tx.send(()).map_err(|error| {
11066                    CliError::InvalidInput(format!("test coordination failed: {error}"))
11067                })?;
11068                Ok(())
11069            });
11070            Ok(())
11071        });
11072        let project_b_completed = done_rx.recv_timeout(Duration::from_secs(1)).is_ok();
11073        release_tx.send(())?;
11074        a_handle
11075            .join()
11076            .map_err(|_panic| io::Error::other("project A telemetry thread panicked"))?
11077            .map_err(io::Error::other)?;
11078        b_handle
11079            .join()
11080            .map_err(|_panic| io::Error::other("project B telemetry thread panicked"))?
11081            .map_err(io::Error::other)?;
11082
11083        require(
11084            project_b_completed,
11085            "one project's blocked telemetry delayed another project",
11086        )
11087    }
11088
11089    #[test]
11090    fn mcp_telemetry_keeps_identity_when_capacity_seal_fails()
11091    -> Result<(), Box<dyn std::error::Error>> {
11092        if telemetry_disabled() {
11093            return Ok(());
11094        }
11095        let temp = tempfile::tempdir()?;
11096        let (state, store) = usage_test_project(temp.path(), "selected-project")?;
11097        let server = ProjectAtlasMcpServer::new(
11098            state.db_path.clone(),
11099            None,
11100            "shared-label".to_string(),
11101            false,
11102        );
11103        server.record_usage_for_state(&state, &store, |usage_instance| {
11104            record_usage_estimate(
11105                &store,
11106                Some(usage_instance),
11107                "seal-failure-test",
11108                MCP_EVENT_ATLAS_OVERVIEW,
11109                None,
11110                None,
11111                8,
11112                "overview:\n  files: 1\n",
11113            )
11114        });
11115        let initial_identity = usage_runtime_identity(&server, &state, &store)?;
11116        let busy_connection = rusqlite::Connection::open(&state.db_path)?;
11117        busy_connection.execute_batch("BEGIN IMMEDIATE")?;
11118        let mut calls = 0usize;
11119
11120        server.record_usage_for_state(&state, &store, |_usage_instance| {
11121            calls += 1;
11122            Err(CliError::Db(DbError::TelemetryBaselineCapacity))
11123        });
11124        busy_connection.execute_batch("ROLLBACK")?;
11125
11126        require(calls == 1, "failed sealing unexpectedly retried the event")?;
11127        require(
11128            usage_runtime_identity(&server, &state, &store)? == initial_identity,
11129            "failed sealing replaced the still-active project identity",
11130        )
11131    }
11132
11133    #[test]
11134    fn mcp_telemetry_rotates_and_retries_once_when_baselines_reach_capacity()
11135    -> Result<(), Box<dyn std::error::Error>> {
11136        if telemetry_disabled() {
11137            return Ok(());
11138        }
11139        let temp = tempfile::tempdir()?;
11140        let root = temp.path().join("selected-project");
11141        fs::create_dir_all(root.join(".projectatlas"))?;
11142        let db_path = root.join(".projectatlas").join("projectatlas.db");
11143        let store = AtlasStore::open_for_project(&db_path, &root)?;
11144        let state = McpProjectState {
11145            root,
11146            db_path: db_path.clone(),
11147            config_path: None,
11148            worktree: None,
11149        };
11150        let other_root = temp.path().join("other-project");
11151        fs::create_dir_all(other_root.join(".projectatlas"))?;
11152        let other_db_path = other_root.join(".projectatlas").join("projectatlas.db");
11153        let other_store = AtlasStore::open_for_project(&other_db_path, &other_root)?;
11154        let other_state = McpProjectState {
11155            root: other_root,
11156            db_path: other_db_path,
11157            config_path: None,
11158            worktree: None,
11159        };
11160        let server = ProjectAtlasMcpServer::new(db_path, None, "shared-label".to_string(), false);
11161        server.record_usage_for_state(&state, &store, |usage_instance| {
11162            record_usage_estimate(
11163                &store,
11164                Some(usage_instance),
11165                "rotation-test",
11166                MCP_EVENT_ATLAS_OVERVIEW,
11167                None,
11168                None,
11169                8,
11170                "overview:\n  files: 1\n",
11171            )
11172        });
11173        server.record_usage_for_state(&other_state, &other_store, |_usage_instance| Ok(()));
11174        let initial_identity = usage_runtime_identity(&server, &state, &store)?;
11175        let other_identity = usage_runtime_identity(&server, &other_state, &other_store)?;
11176        let mut calls = 0usize;
11177
11178        server.record_usage_for_state(&state, &store, |_usage_instance| {
11179            calls += 1;
11180            if calls == 1 {
11181                Err(CliError::Db(DbError::TelemetryBaselineCapacity))
11182            } else {
11183                Ok(())
11184            }
11185        });
11186
11187        let tracked = server
11188            .usage_runtime
11189            .lock()
11190            .map_err(|_poisoned| io::Error::other("usage runtime lock poisoned"))?
11191            .entries
11192            .len();
11193        let rotated_identity = usage_runtime_identity(&server, &state, &store)?;
11194        require(calls == 2, "capacity handling did not retry exactly once")?;
11195        require(
11196            rotated_identity != initial_identity,
11197            "capacity handling did not rotate the bounded runtime identity",
11198        )?;
11199        require(
11200            tracked == 2,
11201            "rotation changed the bounded project binding inventory",
11202        )?;
11203        require(
11204            usage_runtime_identity(&server, &other_state, &other_store)? == other_identity,
11205            "one project's capacity rotation changed another project's identity",
11206        )
11207    }
11208
11209    #[test]
11210    fn navigation_result_survives_telemetry_write_failure() -> Result<(), Box<dyn std::error::Error>>
11211    {
11212        if telemetry_disabled() {
11213            return Ok(());
11214        }
11215        let temp = tempfile::tempdir()?;
11216        let repo = temp.path().join("repo");
11217        fs::create_dir_all(repo.join("src"))?;
11218        fs::write(repo.join("src").join("lib.rs"), "pub fn owner() {}\n")?;
11219        let config_path = repo.join(".projectatlas").join("config.toml");
11220        init_project_with_config(&repo, Some(&config_path))?;
11221        let db_path = repo.join(".projectatlas").join("projectatlas.db");
11222        let plan = ScanRuntimePlan::for_path(Some(&config_path), &repo, None)?;
11223        let mut store = open_atlas_store_for_project(&db_path, &repo)?;
11224        run_scan_pipeline(
11225            &mut store,
11226            &plan,
11227            &SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, Some(1), Some(30)),
11228        )?;
11229        drop(store);
11230
11231        let connection = rusqlite::Connection::open(&db_path)?;
11232        connection.execute_batch("BEGIN IMMEDIATE")?;
11233
11234        let server = ProjectAtlasMcpServer::new(
11235            db_path,
11236            Some(config_path),
11237            "shared-label".to_string(),
11238            false,
11239        );
11240        let result = server.atlas_overview_response(
11241            AtlasProjectParams {
11242                project_path: None,
11243                worktree: None,
11244            },
11245            None,
11246        );
11247        connection.execute_batch("ROLLBACK")?;
11248
11249        if result.contains("overview:") && result.contains("files:") {
11250            Ok(())
11251        } else {
11252            Err(io::Error::other(format!(
11253                "telemetry failure replaced an already-built navigation result: {result}"
11254            ))
11255            .into())
11256        }
11257    }
11258
11259    #[test]
11260    fn mcp_calls_share_server_identity_and_new_server_uses_another()
11261    -> Result<(), Box<dyn std::error::Error>> {
11262        if telemetry_disabled() {
11263            return Ok(());
11264        }
11265        let temp = tempfile::tempdir()?;
11266        let repo = temp.path().join("repo");
11267        fs::create_dir_all(repo.join("src"))?;
11268        fs::write(repo.join("src").join("lib.rs"), "pub fn owner() {}\n")?;
11269        let config_path = repo.join(".projectatlas").join("config.toml");
11270        init_project_with_config(&repo, Some(&config_path))?;
11271        let db_path = repo.join(".projectatlas").join("projectatlas.db");
11272        let plan = ScanRuntimePlan::for_path(Some(&config_path), &repo, None)?;
11273        let mut store = open_atlas_store_for_project(&db_path, &repo)?;
11274        run_scan_pipeline(
11275            &mut store,
11276            &plan,
11277            &SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, Some(1), Some(30)),
11278        )?;
11279        drop(store);
11280
11281        let call_overview = |server: &ProjectAtlasMcpServer| {
11282            server.atlas_overview_response(
11283                AtlasProjectParams {
11284                    project_path: None,
11285                    worktree: None,
11286                },
11287                None,
11288            )
11289        };
11290        let first = ProjectAtlasMcpServer::new(
11291            db_path.clone(),
11292            Some(config_path.clone()),
11293            "shared-label".to_string(),
11294            false,
11295        );
11296        require(
11297            call_overview(&first).contains("overview:"),
11298            "first MCP call failed",
11299        )?;
11300        require(
11301            call_overview(&first).contains("overview:"),
11302            "second MCP call failed",
11303        )?;
11304        let restarted = ProjectAtlasMcpServer::new(
11305            db_path.clone(),
11306            Some(config_path),
11307            "shared-label".to_string(),
11308            false,
11309        );
11310        require(
11311            call_overview(&restarted).contains("overview:"),
11312            "restarted MCP call failed",
11313        )?;
11314
11315        let connection = rusqlite::Connection::open(db_path)?;
11316        let instances: i64 = connection.query_row(
11317            "SELECT COUNT(*) FROM usage_instances WHERE owner = 'mcp_process' AND caller_label = 'shared-label'",
11318            [],
11319            |row| row.get(0),
11320        )?;
11321        let events: i64 =
11322            connection.query_row("SELECT COUNT(*) FROM usage_events", [], |row| row.get(0))?;
11323        require(
11324            instances == 2 && events == 3,
11325            "MCP calls did not reuse one identity per server construction",
11326        )?;
11327        drop(connection);
11328        drop(restarted);
11329        drop(first);
11330        fs::remove_dir_all(&repo)?;
11331        require(!repo.exists(), "MCP teardown left its repository in use")
11332    }
11333
11334    #[test]
11335    fn mcp_database_filesystem_failures_are_typed_and_actionable()
11336    -> Result<(), Box<dyn std::error::Error>> {
11337        let error = CliError::Db(projectatlas_db::DbError::DatabaseFilesystemUnsupported {
11338            path: PathBuf::from("project")
11339                .join(".projectatlas")
11340                .join("projectatlas.db"),
11341            mount_point: Some(PathBuf::from("project")),
11342            filesystem_type: Some("nfs".to_string()),
11343        });
11344        let payload = ProjectAtlasMcpServer::encode_error_payload(&error);
11345        require(
11346            payload.contains("kind: database_filesystem_unsupported")
11347                && payload.contains("filesystem_type: nfs")
11348                && payload.contains("supported local filesystem"),
11349            "MCP TOON lost typed filesystem details or recovery guidance",
11350        )
11351    }
11352
11353    #[test]
11354    fn mcp_project_mismatch_preserves_lossless_store_roots()
11355    -> Result<(), Box<dyn std::error::Error>> {
11356        let temp = tempfile::tempdir()?;
11357        let selected_root = temp.path().join("selected-root");
11358        let indexed_root = temp.path().join("indexed-root");
11359        fs::create_dir_all(&selected_root)?;
11360        fs::create_dir_all(&indexed_root)?;
11361        let database = indexed_root.join(".projectatlas").join("projectatlas.db");
11362        fs::create_dir_all(
11363            database
11364                .parent()
11365                .ok_or_else(|| io::Error::other("indexed database path has no parent"))?,
11366        )?;
11367        drop(AtlasStore::open_for_project(&database, &indexed_root)?);
11368
11369        let Err(error) = open_atlas_store_for_project(&database, &selected_root) else {
11370            return Err(io::Error::other("wrong-root store open unexpectedly succeeded").into());
11371        };
11372        let selected_display = CanonicalProjectRoot::from_path(&selected_root)?.display_string()?;
11373        let indexed_display = CanonicalProjectRoot::from_path(&indexed_root)?.display_string()?;
11374        let payload = ProjectAtlasMcpServer::encode_error_payload(&error);
11375        let value: serde_json::Value = toon_format::decode_default(&payload)?;
11376        require(
11377            value.pointer("/error/project_mismatch/selected_project_root")
11378                == Some(&serde_json::Value::String(selected_display.clone()))
11379                && value.pointer("/error/project_mismatch/indexed_project_root")
11380                    == Some(&serde_json::Value::String(indexed_display.clone())),
11381            "MCP omitted lossless roots from a store mismatch",
11382        )?;
11383        require(
11384            payload.contains(&selected_display) && payload.contains(&indexed_display),
11385            "MCP TOON did not retain lossless store roots",
11386        )
11387    }
11388
11389    #[cfg(unix)]
11390    #[test]
11391    fn mcp_project_mismatch_keeps_native_display_unavailable_typed()
11392    -> Result<(), Box<dyn std::error::Error>> {
11393        let temp = tempfile::tempdir()?;
11394        let raw_root = temp
11395            .path()
11396            .join(std::ffi::OsString::from_vec(b"raw-root-\x80".to_vec()));
11397        let replacement_root = temp.path().join("raw-root-�");
11398        fs::create_dir(&raw_root)?;
11399        fs::create_dir(&replacement_root)?;
11400        let raw_identity = CanonicalProjectRoot::from_path(&raw_root)?;
11401        let replacement_identity = CanonicalProjectRoot::from_path(&replacement_root)?;
11402        let error = CliError::ProjectMismatch(Box::new(IndexProjectMismatch::from_native_roots(
11403            &raw_identity,
11404            &replacement_identity,
11405        )));
11406        let payload = ProjectAtlasMcpServer::encode_error_payload(&error);
11407        require(
11408            payload.contains("selected_project_root: null")
11409                && payload.contains("indexed_project_root:")
11410                && payload.contains("raw-root-�"),
11411            "MCP promoted a lossy native root into an ambiguous structured value",
11412        )?;
11413
11414        let mapped = crate::runtime::project_store_error(DbError::ProjectRootMismatch {
11415            expected: raw_root.to_string_lossy().into_owned(),
11416            found: replacement_root.to_string_lossy().into_owned(),
11417            identities: None,
11418        });
11419        let mapped_payload = ProjectAtlasMcpServer::encode_error_payload(&mapped);
11420        require(
11421            mapped_payload.contains("selected_project_root: null")
11422                && mapped_payload.contains("indexed_project_root: null")
11423                && mapped_payload.contains("does not match"),
11424            "MCP promoted lossy store mismatch text into structured roots",
11425        )
11426    }
11427
11428    #[cfg(unix)]
11429    #[test]
11430    fn mcp_recovery_and_session_reports_omit_unavailable_native_root_selectors()
11431    -> Result<(), Box<dyn std::error::Error>> {
11432        let temp = tempfile::tempdir()?;
11433        let raw_root = temp
11434            .path()
11435            .join(std::ffi::OsString::from_vec(b"repo-\x80".to_vec()));
11436        let replacement_root = temp.path().join("repo-�");
11437        fs::create_dir(&raw_root)?;
11438        fs::create_dir(&replacement_root)?;
11439        let raw_db = raw_root.join(".projectatlas").join("projectatlas.db");
11440        let replacement_db = replacement_root
11441            .join(".projectatlas")
11442            .join("projectatlas.db");
11443        let replacement_display = lossless_project_root_display(&replacement_root)
11444            .ok_or_else(|| io::Error::other("replacement root lost its UTF-8 display"))?;
11445
11446        let init_errors = [
11447            (
11448                crate::runtime::index_init_required(&raw_root, &raw_db),
11449                false,
11450            ),
11451            (
11452                crate::runtime::index_init_required(&replacement_root, &replacement_db),
11453                true,
11454            ),
11455        ];
11456        for (error, displayable) in init_errors {
11457            let payload = ProjectAtlasMcpServer::encode_error_payload(&error);
11458            let value: serde_json::Value = toon_format::decode_default(&payload)?;
11459            if displayable {
11460                let expected = serde_json::Value::String(replacement_display.clone());
11461                require(
11462                    value.pointer("/error/init_required/project_root") == Some(&expected)
11463                        && value.pointer("/error/next/project_path") == Some(&expected),
11464                    "MCP init recovery lost a displayable root selector",
11465                )?;
11466            } else {
11467                require(
11468                    value
11469                        .pointer("/error/init_required/project_root")
11470                        .is_some_and(serde_json::Value::is_null)
11471                        && value.pointer("/error/next/project_path").is_none()
11472                        && !payload.contains("repo-�"),
11473                    "MCP init recovery exposed a lossy raw-root selector",
11474                )?;
11475            }
11476        }
11477
11478        let refresh_errors = [
11479            (
11480                CliError::RefreshRequired(Box::new(IndexRefreshRequired {
11481                    project_root: lossless_project_root_display(&raw_root),
11482                    worktree: None,
11483                    status: IndexReadStatus::RefreshRequired,
11484                    reason: IndexRefreshReason::SourceChanged,
11485                    scope: IndexRefreshScope::Incremental,
11486                    changed: 1,
11487                    added: 0,
11488                    removed: 0,
11489                    modified: 1,
11490                    sample_paths: vec!["src/lib.rs".to_string()],
11491                })),
11492                false,
11493            ),
11494            (
11495                CliError::RefreshRequired(Box::new(IndexRefreshRequired {
11496                    project_root: lossless_project_root_display(&replacement_root),
11497                    worktree: None,
11498                    status: IndexReadStatus::RefreshRequired,
11499                    reason: IndexRefreshReason::SourceChanged,
11500                    scope: IndexRefreshScope::Incremental,
11501                    changed: 1,
11502                    added: 0,
11503                    removed: 0,
11504                    modified: 1,
11505                    sample_paths: vec!["src/lib.rs".to_string()],
11506                })),
11507                true,
11508            ),
11509        ];
11510        for (error, displayable) in refresh_errors {
11511            let payload = ProjectAtlasMcpServer::encode_error_payload(&error);
11512            let value: serde_json::Value = toon_format::decode_default(&payload)?;
11513            if displayable {
11514                let expected = serde_json::Value::String(replacement_display.clone());
11515                require(
11516                    value.pointer("/error/refresh_required/project_root") == Some(&expected)
11517                        && value.pointer("/error/next/project_path") == Some(&expected),
11518                    "MCP refresh recovery lost a displayable root selector",
11519                )?;
11520            } else {
11521                require(
11522                    value
11523                        .pointer("/error/refresh_required/project_root")
11524                        .is_some_and(serde_json::Value::is_null)
11525                        && value.pointer("/error/next/project_path").is_none()
11526                        && !payload.contains("repo-�"),
11527                    "MCP refresh recovery exposed a lossy raw-root selector",
11528                )?;
11529            }
11530        }
11531
11532        let raw_server = ProjectAtlasMcpServer::new(raw_db, None, "raw-session".to_string(), false);
11533        let raw_project = serde_json::to_value(ProjectAtlasMcpServer::project_state_payload(
11534            &raw_server.control_state,
11535        ))?;
11536        let raw_capability = serde_json::to_value(
11537            ProjectAtlasMcpServer::selected_project_capability(&raw_server.control_state),
11538        )?;
11539        require(
11540            raw_project.get("root") == Some(&serde_json::Value::Null)
11541                && raw_project.get("db") == Some(&serde_json::Value::Null)
11542                && raw_capability.get("root") == Some(&serde_json::Value::Null)
11543                && raw_capability.get("db") == Some(&serde_json::Value::Null)
11544                && !raw_project.to_string().contains("repo-�")
11545                && !raw_capability.to_string().contains("repo-�"),
11546            "MCP selected-project reports exposed a lossy raw-root projection",
11547        )?;
11548        let raw_brief = raw_server.build_session_brief(
11549            AtlasSessionBriefParams {
11550                project_path: None,
11551                worktree: None,
11552                query: None,
11553                purpose_task: None,
11554                compact: None,
11555                folder_limit: None,
11556                file_limit: None,
11557                blocker_limit: None,
11558                purpose_limit: None,
11559            },
11560            None,
11561        )?;
11562        let raw_value = serde_json::to_value(&raw_brief)?;
11563        require(
11564            raw_value.pointer("/project/root") == Some(&serde_json::Value::Null)
11565                && raw_value
11566                    .pointer("/recommendations/0/arguments/project_path")
11567                    .is_none()
11568                && raw_value
11569                    .pointer("/recommendations/0/arguments/worktree")
11570                    .is_none()
11571                && !raw_value.to_string().contains("repo-�"),
11572            "MCP session brief offered a lossy sibling selector for a raw root",
11573        )?;
11574
11575        let replacement_server = ProjectAtlasMcpServer::new(
11576            replacement_db,
11577            None,
11578            "replacement-session".to_string(),
11579            false,
11580        );
11581        let replacement_project = serde_json::to_value(
11582            ProjectAtlasMcpServer::project_state_payload(&replacement_server.control_state),
11583        )?;
11584        let replacement_capability = serde_json::to_value(
11585            ProjectAtlasMcpServer::selected_project_capability(&replacement_server.control_state),
11586        )?;
11587        require(
11588            replacement_project
11589                .get("root")
11590                .and_then(|value| value.as_str())
11591                == Some(replacement_display.as_str())
11592                && replacement_capability
11593                    .get("root")
11594                    .and_then(|value| value.as_str())
11595                    == Some(replacement_display.as_str()),
11596            "MCP selected-project reports lost the displayable root",
11597        )?;
11598        let replacement_brief = replacement_server.build_session_brief(
11599            AtlasSessionBriefParams {
11600                project_path: None,
11601                worktree: None,
11602                query: None,
11603                purpose_task: None,
11604                compact: None,
11605                folder_limit: None,
11606                file_limit: None,
11607                blocker_limit: None,
11608                purpose_limit: None,
11609            },
11610            None,
11611        )?;
11612        let replacement_value = serde_json::to_value(&replacement_brief)?;
11613        require(
11614            replacement_value
11615                .pointer("/project/root")
11616                .and_then(|value| value.as_str())
11617                == Some(replacement_display.as_str())
11618                && replacement_value
11619                    .pointer("/recommendations/0/arguments/project_path")
11620                    .and_then(|value| value.as_str())
11621                    == Some(replacement_display.as_str()),
11622            "MCP session brief lost the displayable root recovery selector",
11623        )?;
11624        require(
11625            !raw_root.join(".projectatlas").exists()
11626                && !replacement_root.join(".projectatlas").exists(),
11627            "MCP missing-index recovery mutated a project root",
11628        )
11629    }
11630
11631    #[test]
11632    fn mcp_schema_version_mismatches_are_typed_and_content_free()
11633    -> Result<(), Box<dyn std::error::Error>> {
11634        let supported = projectatlas_db::CURRENT_SCHEMA_VERSION;
11635        let future = supported + 1;
11636        let error = CliError::Db(DbError::SchemaVersion {
11637            found: future,
11638            expected: supported,
11639        });
11640        let payload = ProjectAtlasMcpServer::encode_error_payload(&error);
11641        require(
11642            payload.contains("kind: schema_version_mismatch")
11643                && payload.contains(&format!("found_schema_version: {future}"))
11644                && payload.contains(&format!("supported_schema_version: {supported}"))
11645                && payload.contains(env!("CARGO_PKG_VERSION"))
11646                && payload.contains("do not reset")
11647                && !payload.contains(".projectatlas")
11648                && !payload.contains("session_id")
11649                && !payload.contains("project_root"),
11650            "MCP TOON lost typed schema-version details or exposed database context",
11651        )?;
11652
11653        let predecessor = CliError::Service(ServiceError::Db(DbError::SchemaVersion {
11654            found: 8,
11655            expected: supported,
11656        }));
11657        let McpToolTextResult(predecessor_result) =
11658            ProjectAtlasMcpServer::as_mcp_text(Err(predecessor));
11659        let predecessor_payload = predecessor_result.map_err(std::io::Error::other)?;
11660        require(
11661            predecessor_payload.contains("kind: schema_migration_required")
11662                && predecessor_payload.contains("found_schema_version: 8")
11663                && predecessor_payload.contains(&format!("supported_schema_version: {supported}"))
11664                && predecessor_payload
11665                    .contains(&format!("migration_steps_remaining: {}", supported - 8))
11666                && predecessor_payload.contains("projectatlas init")
11667                && predecessor_payload.contains("atlas_init")
11668                && predecessor_payload.contains("same global `--db`/`--config` selection")
11669                && predecessor_payload.contains("same MCP server/database binding")
11670                && !predecessor_payload.contains("schema_version_mismatch")
11671                && !predecessor_payload.contains(crate::SCHEMA_VERSION_MISMATCH_RECOVERY),
11672            "MCP omitted the supported-predecessor migration handoff",
11673        )
11674    }
11675
11676    #[test]
11677    fn mcp_search_capability_failures_are_typed_and_actionable()
11678    -> Result<(), Box<dyn std::error::Error>> {
11679        let error = CliError::Service(ServiceError::SearchCapabilityUnavailable {
11680            requested_mode: projectatlas_service::SearchRetrievalMode::Hybrid,
11681            state: "not-installed",
11682            guidance: "install and build a compatible semantic generation",
11683        });
11684        let payload = ProjectAtlasMcpServer::encode_error_payload(&error);
11685        require(
11686            payload.contains("kind: search_capability_unavailable")
11687                && payload.contains("requested_mode")
11688                && payload.contains("hybrid")
11689                && payload.contains("state")
11690                && payload.contains("not-installed")
11691                && payload.contains("compatible semantic generation"),
11692            "MCP TOON lost typed search-capability state or recovery guidance",
11693        )
11694    }
11695
11696    #[test]
11697    fn mcp_records_usage_only_for_the_accepted_verified_attempt()
11698    -> Result<(), Box<dyn std::error::Error>> {
11699        if telemetry_disabled() {
11700            return Ok(());
11701        }
11702        let temp = tempfile::tempdir()?;
11703        let repo = temp.path().join("repo");
11704        let source = repo.join("src").join("lib.rs");
11705        fs::create_dir_all(
11706            source
11707                .parent()
11708                .ok_or_else(|| io::Error::other("missing parent"))?,
11709        )?;
11710        let original = "pub fn original() {}\n";
11711        let revised = "pub fn revised() {}\n";
11712        fs::write(&source, original)?;
11713        let config_path = repo.join(".projectatlas").join("config.toml");
11714        init_project_with_config(&repo, Some(&config_path))?;
11715        let db_path = repo.join(".projectatlas").join("projectatlas.db");
11716        let plan = ScanRuntimePlan::for_path(Some(&config_path), &repo, None)?;
11717        let mut store = open_atlas_store_for_project(&db_path, &repo)?;
11718        run_scan_pipeline(
11719            &mut store,
11720            &plan,
11721            &SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, Some(1), Some(30)),
11722        )?;
11723        drop(store);
11724        let state = McpProjectState {
11725            root: repo.clone(),
11726            db_path: db_path.clone(),
11727            config_path: Some(config_path.clone()),
11728            worktree: None,
11729        };
11730        let server = ProjectAtlasMcpServer::new(
11731            db_path.clone(),
11732            Some(config_path.clone()),
11733            "accepted-attempt".to_string(),
11734            false,
11735        );
11736        let mut attempts = 0_u64;
11737
11738        let response =
11739            server.with_fresh_string_and_usage_for_request(&state, None, |store, _stamp| {
11740                attempts = attempts.saturating_add(1);
11741                let hash = store
11742                    .load_node_by_path("src/lib.rs")?
11743                    .and_then(|node| node.node.content_hash)
11744                    .ok_or_else(|| CliError::InvalidInput("source hash missing".to_string()))?;
11745                if attempts == 1 {
11746                    fs::write(&source, revised).map_err(|source_error| CliError::Io {
11747                        path: source.clone(),
11748                        source: source_error,
11749                    })?;
11750                    server.source_observations.inject_test_event(
11751                        &db_path,
11752                        &repo,
11753                        Some(&config_path),
11754                        Event::new(EventKind::Modify(ModifyKind::Any)).add_path(source.clone()),
11755                    )?;
11756                }
11757                Ok((
11758                    hash,
11759                    Some(McpUsageIntent::estimate(
11760                        MCP_EVENT_ATLAS_OVERVIEW,
11761                        None,
11762                        None,
11763                        1,
11764                    )),
11765                ))
11766            })?;
11767
11768        require(
11769            attempts >= 2,
11770            "mid-query source edit did not retry the query",
11771        )?;
11772        require(
11773            response == blake3::hash(revised.as_bytes()).to_hex().to_string(),
11774            "MCP returned the provisional pre-edit result",
11775        )?;
11776        let connection = rusqlite::Connection::open(db_path)?;
11777        let events: i64 =
11778            connection.query_row("SELECT COUNT(*) FROM usage_events", [], |row| row.get(0))?;
11779        require(
11780            events == 1,
11781            "MCP telemetry recorded a discarded provisional attempt",
11782        )
11783    }
11784
11785    /// Wait for one admitted task to reach a terminal state.
11786    fn wait_for_background_task(
11787        server: &ProjectAtlasMcpServer,
11788        task_id: &str,
11789    ) -> Result<McpTaskRecord, Box<dyn std::error::Error>> {
11790        for _attempt in 0..5_000 {
11791            let status = server.task_status(task_id.to_string())?;
11792            if let Some(record) = status.task.filter(McpTaskRecord::is_terminal_state) {
11793                return Ok(record);
11794            }
11795            thread::sleep(Duration::from_millis(1));
11796        }
11797        Err(io::Error::other("background task did not reach a terminal state").into())
11798    }
11799
11800    /// Admit one successful task and wait for completion.
11801    fn run_successful_background_task(
11802        server: &ProjectAtlasMcpServer,
11803    ) -> Result<McpTaskRecord, Box<dyn std::error::Error>> {
11804        let task = server.start_index_task(
11805            McpTaskOperation::Scan,
11806            SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, None, None),
11807            MCP_TOOL_ATLAS_OVERVIEW,
11808            |_control, _options| Ok(()),
11809        )?;
11810        wait_for_background_task(server, &task.task_id)
11811    }
11812
11813    /// Wait for the latest task of one operation to reach a terminal state.
11814    fn wait_for_background_operation(
11815        server: &ProjectAtlasMcpServer,
11816        operation: &McpTaskOperation,
11817    ) -> Result<McpTaskRecord, Box<dyn std::error::Error>> {
11818        let task_id = server
11819            .task_registry
11820            .read()
11821            .map_err(|_poisoned| io::Error::other("task registry lock poisoned"))?
11822            .latest_task_id(operation)
11823            .ok_or_else(|| io::Error::other("background task was not admitted"))?;
11824        wait_for_background_task(server, &task_id)
11825    }
11826
11827    /// Assert that persisted index work is visible through normal agent reads.
11828    fn require_agent_index_reads(
11829        server: &ProjectAtlasMcpServer,
11830        project_path: &str,
11831        expected_symbol: &str,
11832    ) -> Result<(), Box<dyn std::error::Error>> {
11833        let overview = server.atlas_overview_response(
11834            AtlasProjectParams {
11835                project_path: Some(project_path.to_string()),
11836                worktree: None,
11837            },
11838            None,
11839        );
11840        require(
11841            overview.contains("overview:"),
11842            "agent overview did not read the published index",
11843        )?;
11844
11845        let summary = server.atlas_file_summary_response(
11846            &AtlasFileSummaryParams {
11847                project_path: Some(project_path.to_string()),
11848                worktree: None,
11849                file: "src/lib.rs".to_string(),
11850                nearest_project: Some(false),
11851                compact: None,
11852                content_selection: None,
11853                limit: Some(25),
11854            },
11855            None,
11856        );
11857        require(
11858            summary.contains("file_summary:")
11859                && summary.contains("src/lib.rs")
11860                && summary.contains(expected_symbol),
11861            "agent file summary omitted published source facts",
11862        )?;
11863
11864        let symbols = server.atlas_symbols_response(
11865            &AtlasSymbolsParams {
11866                project_path: Some(project_path.to_string()),
11867                worktree: None,
11868                file: Some("src/lib.rs".to_string()),
11869                nearest_project: Some(false),
11870                query: None,
11871                content_selection: None,
11872                limit: Some(50),
11873            },
11874            None,
11875        );
11876        require(
11877            symbols.contains("symbols[") && symbols.contains(expected_symbol),
11878            "agent symbol read omitted published parser output",
11879        )?;
11880
11881        let relations = server.atlas_symbol_relations_response(
11882            &AtlasSymbolRelationsParams {
11883                project_path: Some(project_path.to_string()),
11884                file: Some("src/lib.rs".to_string()),
11885                nearest_project: Some(false),
11886                query: None,
11887                limit: Some(50),
11888                ..AtlasSymbolRelationsParams::default()
11889            },
11890            None,
11891        );
11892        require(
11893            relations.contains("relations[") && relations.contains(expected_symbol),
11894            "agent relation read omitted published graph output",
11895        )?;
11896        let explicit_legacy = server.atlas_symbol_relations_response(
11897            &AtlasSymbolRelationsParams {
11898                project_path: Some(project_path.to_string()),
11899                file: Some("src/lib.rs".to_string()),
11900                nearest_project: Some(false),
11901                view: Some("legacy".to_string()),
11902                query: None,
11903                limit: Some(50),
11904                ..AtlasSymbolRelationsParams::default()
11905            },
11906            None,
11907        );
11908        require(
11909            relations == explicit_legacy,
11910            "explicit MCP legacy relation view changed default response bytes or ordering",
11911        )?;
11912        let compact_legacy = server.atlas_symbol_relations_response(
11913            &AtlasSymbolRelationsParams {
11914                project_path: Some(project_path.to_string()),
11915                file: Some("src/lib.rs".to_string()),
11916                nearest_project: Some(false),
11917                compact: Some(true),
11918                limit: Some(50),
11919                ..AtlasSymbolRelationsParams::default()
11920            },
11921            None,
11922        );
11923        require(
11924            compact_legacy.contains(MCP_ERROR_COMPACT_DETAILED_RELATION_VIEW),
11925            "compact relation projection did not reject the legacy view",
11926        )?;
11927        let zero_limit_legacy = server.atlas_symbol_relations_response(
11928            &AtlasSymbolRelationsParams {
11929                project_path: Some(project_path.to_string()),
11930                file: Some("src/lib.rs".to_string()),
11931                nearest_project: Some(false),
11932                limit: Some(0),
11933                ..AtlasSymbolRelationsParams::default()
11934            },
11935            None,
11936        );
11937        require(
11938            zero_limit_legacy.contains("relations[1]"),
11939            "MCP legacy zero limit no longer preserves its one-row compatibility behavior",
11940        )?;
11941        let detailed = server.atlas_symbol_relations_response(
11942            &AtlasSymbolRelationsParams {
11943                project_path: Some(project_path.to_string()),
11944                file: Some("src/lib.rs".to_string()),
11945                nearest_project: Some(false),
11946                view: Some("detailed".to_string()),
11947                direction: Some("outbound".to_string()),
11948                limit: Some(50),
11949                ..AtlasSymbolRelationsParams::default()
11950            },
11951            None,
11952        );
11953        require(
11954            detailed.contains("symbol_relations:") && detailed.contains("anchor:"),
11955            "detailed MCP relation route did not return the bounded graph envelope",
11956        )?;
11957        if expected_symbol == "third" {
11958            let compact_detailed = server.atlas_symbol_relations_response(
11959                &AtlasSymbolRelationsParams {
11960                    project_path: Some(project_path.to_string()),
11961                    file: Some("src/lib.rs".to_string()),
11962                    nearest_project: Some(false),
11963                    view: Some("detailed".to_string()),
11964                    compact: Some(true),
11965                    symbol: Some("first".to_string()),
11966                    direction: Some("outbound".to_string()),
11967                    include_occurrences: Some(true),
11968                    limit: Some(1),
11969                    output_bytes: Some(8 * 1_024),
11970                    ..AtlasSymbolRelationsParams::default()
11971                },
11972                None,
11973            );
11974            require(
11975                compact_detailed.len() <= 8 * 1_024
11976                    && compact_detailed.contains("returned: 1")
11977                    && compact_detailed.contains("status: resolved")
11978                    && compact_detailed.contains("confidence: exact")
11979                    && compact_detailed.contains("completeness: complete")
11980                    && compact_detailed.contains("Own café λ relation navigation")
11981                    && compact_detailed.contains("next_call:")
11982                    && compact_detailed.contains("occurrences[1]:")
11983                    && !compact_detailed.contains("occurrences[1]:\n        - relation:"),
11984                "compact detailed relation omitted trust, purpose, occurrence, next-call, or bounded-output behavior",
11985            )?;
11986            let first_detailed_page = server.atlas_symbol_relations_response(
11987                &AtlasSymbolRelationsParams {
11988                    project_path: Some(project_path.to_string()),
11989                    file: Some("src/lib.rs".to_string()),
11990                    nearest_project: Some(false),
11991                    view: Some("detailed".to_string()),
11992                    symbol: Some("first".to_string()),
11993                    direction: Some("outbound".to_string()),
11994                    depth: Some(2),
11995                    limit: Some(1),
11996                    output_bytes: Some(64 * 1024),
11997                    ..AtlasSymbolRelationsParams::default()
11998                },
11999                None,
12000            );
12001            let first_detailed_value: serde_json::Value =
12002                toon_format::decode_default(&first_detailed_page)?;
12003            let first_detailed_report = first_detailed_value
12004                .get("symbol_relations")
12005                .ok_or_else(|| io::Error::other("first detailed MCP page omitted its envelope"))?;
12006            let continuation = first_detailed_report
12007                .get("continuation")
12008                .and_then(serde_json::Value::as_str)
12009                .ok_or_else(|| io::Error::other("first detailed MCP page omitted its cursor"))?;
12010            require(
12011                first_detailed_report
12012                    .get("returned")
12013                    .and_then(serde_json::Value::as_u64)
12014                    == Some(1)
12015                    && first_detailed_report
12016                        .get("rows")
12017                        .and_then(serde_json::Value::as_array)
12018                        .is_some_and(|rows| rows.len() == 1),
12019                "first detailed MCP page was not a nonempty bounded symbol result",
12020            )?;
12021            let second_detailed_page = server.atlas_symbol_relations_response(
12022                &AtlasSymbolRelationsParams {
12023                    project_path: Some(project_path.to_string()),
12024                    file: Some("src/lib.rs".to_string()),
12025                    nearest_project: Some(false),
12026                    view: Some("detailed".to_string()),
12027                    cursor: Some(continuation.to_string()),
12028                    symbol: Some("first".to_string()),
12029                    direction: Some("outbound".to_string()),
12030                    depth: Some(2),
12031                    limit: Some(1),
12032                    output_bytes: Some(64 * 1024),
12033                    ..AtlasSymbolRelationsParams::default()
12034                },
12035                None,
12036            );
12037            let second_detailed_value: serde_json::Value =
12038                toon_format::decode_default(&second_detailed_page)?;
12039            let second_detailed_report = second_detailed_value
12040                .get("symbol_relations")
12041                .ok_or_else(|| io::Error::other("second detailed MCP page omitted its envelope"))?;
12042            require(
12043                second_detailed_report
12044                    .get("returned")
12045                    .and_then(serde_json::Value::as_u64)
12046                    == Some(1)
12047                    && second_detailed_report.get("rows") != first_detailed_report.get("rows")
12048                    && second_detailed_page.contains("Own café λ relation navigation"),
12049                "detailed MCP cursor did not resume a distinct Unicode-safe symbol row",
12050            )?;
12051            let compact_continuation_page = server.atlas_symbol_relations_response(
12052                &AtlasSymbolRelationsParams {
12053                    project_path: Some(project_path.to_string()),
12054                    file: Some("src/lib.rs".to_string()),
12055                    nearest_project: Some(false),
12056                    view: Some("detailed".to_string()),
12057                    compact: Some(true),
12058                    symbol: Some("first".to_string()),
12059                    symbol_parent: Some(String::new()),
12060                    direction: Some("outbound".to_string()),
12061                    depth: Some(2),
12062                    limit: Some(1),
12063                    output_bytes: Some(64 * 1024),
12064                    ..AtlasSymbolRelationsParams::default()
12065                },
12066                None,
12067            );
12068            let compact_continuation_value: serde_json::Value =
12069                toon_format::decode_default(&compact_continuation_page)?;
12070            let compact_continuation_report = compact_continuation_value
12071                .get("symbol_relations")
12072                .ok_or_else(|| io::Error::other("compact relation page omitted its envelope"))?;
12073            let compact_next_call = compact_continuation_report
12074                .get("next_call")
12075                .ok_or_else(|| io::Error::other("compact relation page omitted its next call"))?;
12076            require(
12077                compact_next_call
12078                    .get("tool")
12079                    .and_then(serde_json::Value::as_str)
12080                    == Some(MCP_TOOL_ATLAS_SYMBOL_RELATIONS),
12081                "compact relation continuation did not name its owning MCP tool",
12082            )?;
12083            let compact_next_arguments = compact_next_call
12084                .get("arguments")
12085                .cloned()
12086                .ok_or_else(|| io::Error::other("compact next call omitted its arguments"))?;
12087            require(
12088                compact_next_arguments.get("cursor").is_some()
12089                    && compact_next_arguments.get("symbol_parent").is_none(),
12090                "compact next call did not preserve its cursor or normalize an empty parent",
12091            )?;
12092            let compact_next_params: AtlasSymbolRelationsParams =
12093                serde_json::from_value(compact_next_arguments)?;
12094            let compact_resumed_page =
12095                server.atlas_symbol_relations_response(&compact_next_params, None);
12096            require(
12097                compact_resumed_page.contains("symbol_relations:")
12098                    && !compact_resumed_page.contains("cursor does not match query")
12099                    && !compact_resumed_page.contains("graph symbol anchor is not available"),
12100                "compact relation next call was not directly reusable",
12101            )?;
12102        }
12103        let bounded_output_bytes = 4 * 1024_u32;
12104        let bounded = server.atlas_symbol_relations_response(
12105            &AtlasSymbolRelationsParams {
12106                project_path: Some(project_path.to_string()),
12107                file: Some("src/lib.rs".to_string()),
12108                nearest_project: Some(false),
12109                view: Some("detailed".to_string()),
12110                direction: Some("outbound".to_string()),
12111                limit: Some(50),
12112                edge_limit: Some(50),
12113                node_limit: Some(50),
12114                visited_limit: Some(50),
12115                occurrence_total_limit: Some(50),
12116                intermediate_bytes: Some(128 * 1024),
12117                deadline_ms: Some(2_000),
12118                output_bytes: Some(bounded_output_bytes),
12119                ..AtlasSymbolRelationsParams::default()
12120            },
12121            None,
12122        );
12123        require(
12124            bounded.contains("symbol_relations:")
12125                && bounded.len() <= bounded_output_bytes as usize
12126                && bounded.contains("Own café λ relation navigation")
12127                && bounded.contains(&format!("rendered_output_bytes: {}", bounded.len())),
12128            "detailed MCP relation output did not enforce or report the exact routed envelope bytes",
12129        )?;
12130
12131        let analysis = server.atlas_symbol_relations_response(
12132            &AtlasSymbolRelationsParams {
12133                project_path: Some(project_path.to_string()),
12134                file: Some("src/lib.rs".to_string()),
12135                nearest_project: Some(false),
12136                view: Some("analysis".to_string()),
12137                symbol: Some("first".to_string()),
12138                direction: Some("outbound".to_string()),
12139                depth: Some(2),
12140                limit: Some(50),
12141                output_bytes: Some(64 * 1024),
12142                include_communities: Some(true),
12143                include_cycles: Some(true),
12144                ..AtlasSymbolRelationsParams::default()
12145            },
12146            None,
12147        );
12148        require(
12149            analysis.contains("symbol_relations:")
12150                && analysis.contains("mode: architecture")
12151                && analysis.contains("findings[")
12152                && analysis.contains("next_call:")
12153                && analysis.contains("work:"),
12154            "MCP relation analysis omitted its closed mode, findings, work, or reusable next call",
12155        )?;
12156        let entrypoint_anchor = serde_json::to_string(&RelationAnchor::File {
12157            file: RepositoryFilePath::new(Path::new("src/lib.rs"))?,
12158        })?;
12159        let entrypoint = server.atlas_symbol_relations_response(
12160            &AtlasSymbolRelationsParams {
12161                project_path: Some(project_path.to_string()),
12162                file: None,
12163                nearest_project: Some(false),
12164                view: Some("analysis".to_string()),
12165                direction: Some("outbound".to_string()),
12166                resolution: Some("any".to_string()),
12167                depth: Some(2),
12168                limit: Some(50),
12169                output_bytes: Some(64 * 1024),
12170                analysis_mode: Some("entrypoint".to_string()),
12171                profile_name: Some("mcp-entrypoint".to_string()),
12172                entrypoints: Some(vec![entrypoint_anchor.clone()]),
12173                profile_relations: Some(vec!["calls".to_string()]),
12174                ..AtlasSymbolRelationsParams::default()
12175            },
12176            None,
12177        );
12178        require(
12179            entrypoint.contains("symbol_relations:")
12180                && entrypoint.contains("mode: entrypoint")
12181                && entrypoint.contains("entrypoint_profile:")
12182                && entrypoint.contains("coverage:"),
12183            "MCP entrypoint analysis did not serialize the shared profile contract",
12184        )?;
12185        let entrypoint_symbol_conflict = server.atlas_symbol_relations_response(
12186            &AtlasSymbolRelationsParams {
12187                project_path: Some(project_path.to_string()),
12188                file: None,
12189                nearest_project: Some(false),
12190                view: Some("analysis".to_string()),
12191                direction: Some("outbound".to_string()),
12192                resolution: Some("any".to_string()),
12193                depth: Some(2),
12194                limit: Some(50),
12195                output_bytes: Some(64 * 1024),
12196                analysis_mode: Some("entrypoint".to_string()),
12197                symbol: Some("first".to_string()),
12198                entrypoints: Some(vec![entrypoint_anchor]),
12199                profile_relations: Some(vec!["calls".to_string()]),
12200                ..AtlasSymbolRelationsParams::default()
12201            },
12202            None,
12203        );
12204        require(
12205            entrypoint_symbol_conflict.contains(MCP_ERROR_ENTRYPOINT_SYMBOL_SELECTOR),
12206            "MCP silently ignored a symbol selector alongside explicit entrypoint anchors",
12207        )?;
12208
12209        let state = ProjectAtlasMcpServer::project_state_from_root(Path::new(project_path))?;
12210        let publication_before = ProjectAtlasMcpServer::open_read_store(&state)?
12211            .index_publication()?
12212            .ok_or_else(|| io::Error::other("MCP impact fixture publication missing"))?;
12213        let task_records_before = server
12214            .task_registry
12215            .read()
12216            .map_err(|_poisoned| io::Error::other("task registry lock poisoned"))?
12217            .len();
12218        let impact_started = Instant::now();
12219        let impact = server.atlas_symbol_relations_response(
12220            &AtlasSymbolRelationsParams {
12221                project_path: Some(project_path.to_string()),
12222                file: Some("src/lib.rs".to_string()),
12223                nearest_project: Some(false),
12224                view: Some("analysis".to_string()),
12225                symbol: Some("first".to_string()),
12226                direction: Some("outbound".to_string()),
12227                depth: Some(2),
12228                limit: Some(8),
12229                edge_limit: Some(8),
12230                node_limit: Some(16),
12231                visited_limit: Some(16),
12232                occurrence_total_limit: Some(16),
12233                intermediate_bytes: Some(128 * 1_024),
12234                deadline_ms: Some(1_000),
12235                output_bytes: Some(64 * 1_024),
12236                analysis_mode: Some("impact".to_string()),
12237                vcs: Some("working_tree".to_string()),
12238                include_dead_code: Some(true),
12239                ..AtlasSymbolRelationsParams::default()
12240            },
12241            None,
12242        );
12243        let impact_elapsed = impact_started.elapsed();
12244        require(
12245            impact_elapsed <= Duration::from_secs(5)
12246                && impact.contains("mode: impact")
12247                && (impact.contains("state: available") || impact.contains("state: unavailable")),
12248            "MCP impact analysis exceeded its elapsed tolerance or omitted typed mode/VCS state",
12249        )?;
12250        let impact_value: serde_json::Value = toon_format::decode_default(&impact)?;
12251        let impact_report = impact_value
12252            .get(MCP_PAYLOAD_SYMBOL_RELATIONS)
12253            .ok_or_else(|| io::Error::other("MCP impact response omitted its envelope"))?;
12254        let bounded_work = [
12255            ("/returned", 8_u64),
12256            ("/work/relations/inspected_edges", 8),
12257            ("/work/relations/active_nodes", 16),
12258            ("/work/relations/visited_nodes", 16),
12259            ("/work/analyzed_nodes", 16),
12260            ("/work/analyzed_edges", 8),
12261            ("/work/peak_intermediate_bytes", 128 * 1_024),
12262            ("/work/rendered_output_bytes", 64 * 1_024),
12263        ];
12264        require(
12265            impact.len() <= 64 * 1_024
12266                && bounded_work.iter().all(|(path, limit)| {
12267                    impact_report
12268                        .pointer(path)
12269                        .and_then(serde_json::Value::as_u64)
12270                        .is_some_and(|observed| observed <= *limit)
12271                }),
12272            "MCP impact analysis crossed or omitted a declared row/node/edge/visited/intermediate/output budget",
12273        )?;
12274        require(
12275            ProjectAtlasMcpServer::open_read_store(&state)?
12276                .index_publication()?
12277                .as_ref()
12278                == Some(&publication_before)
12279                && server
12280                    .task_registry
12281                    .read()
12282                    .map_err(|_poisoned| io::Error::other("task registry lock poisoned"))?
12283                    .len()
12284                    == task_records_before,
12285            "read-only MCP impact analysis changed publication or retained a task record",
12286        )?;
12287        let follow_up_started = Instant::now();
12288        let follow_up = server.atlas_overview_response(
12289            AtlasProjectParams {
12290                project_path: Some(project_path.to_string()),
12291                worktree: None,
12292            },
12293            None,
12294        );
12295        require(
12296            follow_up_started.elapsed() <= Duration::from_secs(2)
12297                && follow_up.contains("overview:"),
12298            "immediate MCP follow-up read was not responsive after bounded impact analysis",
12299        )?;
12300
12301        let trace = server.atlas_symbol_relations_response(
12302            &AtlasSymbolRelationsParams {
12303                project_path: Some(project_path.to_string()),
12304                file: Some("src/lib.rs".to_string()),
12305                nearest_project: Some(false),
12306                view: Some("analysis".to_string()),
12307                symbol: Some("first".to_string()),
12308                direction: Some("outbound".to_string()),
12309                depth: Some(2),
12310                limit: Some(50),
12311                analysis_mode: Some("trace".to_string()),
12312                trace_target: Some("second".to_string()),
12313                trace_target_file: Some("src/lib.rs".to_string()),
12314                trace_target_kind: Some("function".to_string()),
12315                trace_target_signature: Some("fn second ( )".to_string()),
12316                ..AtlasSymbolRelationsParams::default()
12317            },
12318            None,
12319        );
12320        require(
12321            trace.contains("mode: trace")
12322                && trace.contains("kind: static_trace")
12323                && trace.contains("status: confirmed")
12324                && trace.contains("name: second")
12325                && trace.contains("capability: symbol_slice"),
12326            "MCP trace analysis omitted its confirmed path or reusable exact selector",
12327        )?;
12328
12329        let misplaced = server.atlas_symbol_relations_response(
12330            &AtlasSymbolRelationsParams {
12331                project_path: Some(project_path.to_string()),
12332                file: Some("src/lib.rs".to_string()),
12333                nearest_project: Some(false),
12334                view: Some("detailed".to_string()),
12335                analysis_mode: Some("impact".to_string()),
12336                ..AtlasSymbolRelationsParams::default()
12337            },
12338            None,
12339        );
12340        require(
12341            misplaced.contains("analysis controls require view=analysis"),
12342            "MCP detailed relation view accepted analysis-only controls",
12343        )?;
12344
12345        let missing_trace_target = server.atlas_symbol_relations_response(
12346            &AtlasSymbolRelationsParams {
12347                project_path: Some(project_path.to_string()),
12348                file: Some("src/lib.rs".to_string()),
12349                nearest_project: Some(false),
12350                view: Some("analysis".to_string()),
12351                analysis_mode: Some("trace".to_string()),
12352                ..AtlasSymbolRelationsParams::default()
12353            },
12354            None,
12355        );
12356        require(
12357            missing_trace_target.contains("analysis trace requires an exact file or symbol target"),
12358            "MCP trace analysis accepted a missing exact target",
12359        )?;
12360
12361        let misplaced_vcs = server.atlas_symbol_relations_response(
12362            &AtlasSymbolRelationsParams {
12363                project_path: Some(project_path.to_string()),
12364                file: Some("src/lib.rs".to_string()),
12365                nearest_project: Some(false),
12366                view: Some("analysis".to_string()),
12367                analysis_mode: Some("architecture".to_string()),
12368                vcs: Some("working_tree".to_string()),
12369                ..AtlasSymbolRelationsParams::default()
12370            },
12371            None,
12372        );
12373        require(
12374            misplaced_vcs.contains("VCS selection is valid only for impact analysis"),
12375            "MCP silently dropped an explicit VCS selector outside impact mode",
12376        )
12377    }
12378
12379    #[test]
12380    fn current_dir_alias_paths_use_active_mcp_project() -> Result<(), Box<dyn std::error::Error>> {
12381        let temp = tempfile::tempdir()?;
12382        let repo = temp.path().join("repo-a");
12383        fs::create_dir(&repo)?;
12384        let db_path = repo.join(".projectatlas").join("projectatlas.db");
12385        let server = ProjectAtlasMcpServer::new(db_path, None, "mcp-test".to_string(), false);
12386        let expected_root = canonical_project_root(&repo)?;
12387
12388        let (_state, root) =
12389            server.state_and_root_path(None, None, Some("./".to_string()), false)?;
12390        require(
12391            root == expected_root,
12392            "current-dir alias did not use active root",
12393        )?;
12394
12395        #[cfg(windows)]
12396        {
12397            let (_state, root) =
12398                server.state_and_root_path(None, None, Some(".\\".to_string()), false)?;
12399            require(
12400                root == expected_root,
12401                "windows current-dir alias did not use active root",
12402            )?;
12403        }
12404
12405        Ok(())
12406    }
12407
12408    #[test]
12409    fn worktree_list_retains_retired_rows_at_structural_capacity()
12410    -> Result<(), Box<dyn std::error::Error>> {
12411        let temp = tempfile::tempdir()?;
12412        let primary = temp.path().join("control");
12413        fs::create_dir(&primary)?;
12414        run_fixture_command(StdCommand::new("git").current_dir(&primary).arg("init"))?;
12415        let git_directory = primary.join(".git");
12416        let structural_registrations = git_directory.join("worktrees");
12417        fs::create_dir(&structural_registrations)?;
12418        for index in 0..MAX_GIT_WORKTREE_REGISTRATIONS {
12419            let administrative_directory =
12420                structural_registrations.join(format!("missing-{index:04}"));
12421            fs::create_dir(&administrative_directory)?;
12422            fs::write(
12423                administrative_directory.join("gitdir"),
12424                temp.path()
12425                    .join(format!("missing-{index:04}"))
12426                    .join(".git")
12427                    .to_string_lossy()
12428                    .as_bytes(),
12429            )?;
12430        }
12431
12432        let database = primary.join(".projectatlas").join("projectatlas.db");
12433        fs::create_dir_all(
12434            database
12435                .parent()
12436                .ok_or_else(|| io::Error::other("control database has no parent"))?,
12437        )?;
12438        let store = AtlasStore::open_for_project(&database, &primary)?;
12439        let retired_alias = WorktreeAlias::parse("retired-at-capacity")?;
12440        let retired_registration = store.register_worktree(
12441            &retired_alias,
12442            &git_directory,
12443            &structural_registrations.join("retired"),
12444            &"ab".repeat(32),
12445            &temp.path().join("retired"),
12446            None,
12447            1,
12448        )?;
12449        drop(store);
12450
12451        let server =
12452            ProjectAtlasMcpServer::new(database.clone(), None, "worktree-list".to_string(), false);
12453        let active = server.atlas_worktree_list(Parameters(AtlasWorktreeListParams {
12454            include_retired: Some(false),
12455        }));
12456        require(
12457            active.contains("total_worktrees: 1026")
12458                && active.contains("truncated: false")
12459                && active.contains("retired-at-capacity")
12460                && active.contains("missing-1023"),
12461            "combined capacity starved a structural or active missing registration",
12462        )?;
12463
12464        let store = AtlasStore::open_for_project(&database, &primary)?;
12465        store.retire_worktree(retired_registration.registration_id, &retired_alias, 2)?;
12466        drop(store);
12467
12468        let listed = server.atlas_worktree_list(Parameters(AtlasWorktreeListParams {
12469            include_retired: Some(true),
12470        }));
12471        require(
12472            listed.contains("total_worktrees: 1025") && listed.contains("retired-at-capacity"),
12473            "full structural inventory starved the requested retired registration",
12474        )
12475    }
12476
12477    #[test]
12478    fn worktree_remove_retains_display_for_git_known_missing_registration()
12479    -> Result<(), Box<dyn std::error::Error>> {
12480        let fixture = registered_worktree_race_fixture("missing-remove")?;
12481        fs::remove_file(fixture.linked.join(".git"))?;
12482        let repository = fixture.server.control_git_repository()?;
12483        let missing_entry = repository
12484            .worktrees
12485            .iter()
12486            .find(|entry| entry.administrative_directory == fixture.administrative_directory)
12487            .ok_or_else(|| io::Error::other("missing Git worktree entry was not discovered"))?;
12488        require(
12489            matches!(missing_entry.state, GitWorktreeState::Missing { .. }),
12490            "Git-known missing worktree did not retain its structural entry",
12491        )?;
12492        let root_display = normalize_native_path_display(&fixture.linked.canonicalize()?);
12493        let registrations =
12494            AtlasStore::open_for_project(&fixture.control_db, &fixture.server.control_state.root)?
12495                .worktree_registrations(false)?;
12496        let listed = fixture.server.worktree_list_row(
12497            &repository.common_directory,
12498            missing_entry,
12499            &registrations,
12500        );
12501        require(
12502            listed.root.as_deref() == Some(root_display.as_str())
12503                && matches!(listed.path_display, McpWorktreePathDisplayState::Available)
12504                && matches!(listed.git_state, McpGitWorktreeState::Missing)
12505                && matches!(
12506                    listed.registration,
12507                    McpWorktreeRegistrationState::Registered
12508                ),
12509            "Git-known missing worktree listing lost its retained root",
12510        )?;
12511        let removed = fixture
12512            .server
12513            .atlas_worktree_remove(Parameters(AtlasWorktreeRemoveParams {
12514                worktree: fixture.alias.to_string(),
12515            }));
12516        require(
12517            removed.contains("status: retired")
12518                && removed.contains("path_display: available")
12519                && removed.contains(&root_display)
12520                && removed.contains(MCP_WORKTREE_MISSING_RETENTION_REASON),
12521            &format!(
12522                "Git-known missing worktree retirement lost its retained display identity: {removed}"
12523            ),
12524        )?;
12525        Ok(())
12526    }
12527
12528    #[test]
12529    fn worktree_list_retains_one_invalid_registration_after_administrative_replacement()
12530    -> Result<(), Box<dyn std::error::Error>> {
12531        for replacement in ["file", "link"] {
12532            let fixture = registered_worktree_race_fixture("invalid-registration")?;
12533            let preserved = fixture.primary.join("preserved-administrative");
12534            let target = fixture.primary.join("unrelated-target");
12535            fs::create_dir(&target)?;
12536            fs::write(target.join("canary"), "unrelated")?;
12537            fs::rename(&fixture.administrative_directory, &preserved)?;
12538            if replacement == "file" {
12539                fs::write(&fixture.administrative_directory, "replacement")?;
12540            } else {
12541                #[cfg(unix)]
12542                std::os::unix::fs::symlink(&target, &fixture.administrative_directory)?;
12543                #[cfg(windows)]
12544                {
12545                    let parent = fixture
12546                        .administrative_directory
12547                        .parent()
12548                        .ok_or_else(|| io::Error::other("administrative path has no parent"))?;
12549                    let name = fixture
12550                        .administrative_directory
12551                        .file_name()
12552                        .ok_or_else(|| io::Error::other("administrative path has no name"))?;
12553                    run_fixture_command(
12554                        StdCommand::new("cmd")
12555                            .args(["/D", "/C", "mklink", "/J"])
12556                            .arg(parent.canonicalize()?.join(name))
12557                            .arg(target.canonicalize()?),
12558                    )?;
12559                }
12560            }
12561            let listed = fixture
12562                .server
12563                .atlas_worktree_list(Parameters(AtlasWorktreeListParams {
12564                    include_retired: Some(false),
12565                }));
12566            let value: serde_json::Value = toon_format::decode_default(&listed)?;
12567            let rows = value
12568                .pointer("/worktrees/worktrees")
12569                .and_then(serde_json::Value::as_array)
12570                .ok_or_else(|| io::Error::other(format!("missing worktree rows: {listed}")))?;
12571            require(
12572                rows.len() == 2
12573                    && rows
12574                        .iter()
12575                        .filter(|row| {
12576                            row.get("alias").and_then(serde_json::Value::as_str)
12577                                == Some(fixture.alias.as_str())
12578                                && row.get("git_state").and_then(serde_json::Value::as_str)
12579                                    == Some("invalid")
12580                                && row.get("registration").and_then(serde_json::Value::as_str)
12581                                    == Some("registered")
12582                                && row.get("atlas_state").and_then(serde_json::Value::as_str)
12583                                    == Some("unavailable")
12584                        })
12585                        .count()
12586                        == 1,
12587                &format!("{replacement} replacement split its registered invalid row: {listed}"),
12588            )?;
12589            let removed =
12590                fixture
12591                    .server
12592                    .atlas_worktree_remove(Parameters(AtlasWorktreeRemoveParams {
12593                        worktree: fixture.alias.to_string(),
12594                    }));
12595            let control =
12596                open_atlas_store_read_only_for_project(&fixture.control_db, &fixture.control_root)?;
12597            require(
12598                removed.contains("cannot retire worktree")
12599                    && removed.contains("Git evidence is invalid")
12600                    && control.worktree_registration(&fixture.alias)?.state
12601                        == WorktreeRegistrationState::Active,
12602                &format!(
12603                    "{replacement} invalid evidence allowed registration retirement: {removed}"
12604                ),
12605            )?;
12606            require(
12607                fixture
12608                    .server
12609                    .state_for_target(None, Some(fixture.alias.to_string()))
12610                    .is_err()
12611                    && fs::read_to_string(target.join("canary"))? == "unrelated"
12612                    && !target.join(PROJECTATLAS_DIR_NAME).exists(),
12613                "invalid administrative replacement became an actionable target",
12614            )?;
12615        }
12616        Ok(())
12617    }
12618
12619    #[cfg(windows)]
12620    #[test]
12621    fn windows_worktree_selector_survives_live_to_retained_transition()
12622    -> Result<(), Box<dyn std::error::Error>> {
12623        let common = PathBuf::from(r"C:\repo\.git");
12624        let root = PathBuf::from(r"C:\repo\linked");
12625        for (index, administrative_directory) in [
12626            PathBuf::from(r"C:\repo\.git\worktrees\linked"),
12627            PathBuf::from(r"\\?\C:\repo\.git\worktrees\CON"),
12628        ]
12629        .into_iter()
12630        .enumerate()
12631        {
12632            let entry = GitWorktreeEntry {
12633                role: GitWorktreeRole::Linked,
12634                administrative_directory: administrative_directory.clone(),
12635                state: GitWorktreeState::Active {
12636                    git_control_path: root.join(".git"),
12637                    root: root.clone(),
12638                },
12639            };
12640            let selector = ProjectAtlasMcpServer::worktree_candidate_selector(&entry);
12641            let alias = WorktreeAlias::parse(&format!("retained-{index}"))?;
12642            let registration = WorktreeRegistration {
12643                registration_id: i64::try_from(index + 1)?,
12644                alias: alias.clone(),
12645                state: WorktreeRegistrationState::Active,
12646                git_common_directory: normalize_native_path_display(&common),
12647                git_common_directory_identity: CanonicalProjectRoot::from_persisted_path(
12648                    common.clone(),
12649                )?,
12650                git_administrative_directory: normalize_native_path_display(
12651                    &administrative_directory,
12652                ),
12653                git_administrative_directory_identity: CanonicalProjectRoot::from_persisted_path(
12654                    administrative_directory,
12655                )?,
12656                git_administrative_identity: "ab".repeat(32),
12657                last_root: normalize_native_path_display(&root),
12658                last_root_identity: CanonicalProjectRoot::from_persisted_path(root.clone())?,
12659                project_instance_id: None,
12660                accepted_telemetry_revision: 0,
12661                created_at_epoch: 1,
12662                retired_at_epoch: None,
12663            };
12664            let retained = ProjectAtlasMcpServer::missing_registered_worktree_row(&registration);
12665            require(
12666                retained.selector.as_deref() == Some(selector.as_str())
12667                    && retained.alias.as_deref() == Some(alias.as_str()),
12668                "live-to-retained transition changed the selector or lost the removal alias",
12669            )?;
12670        }
12671        Ok(())
12672    }
12673
12674    #[cfg(unix)]
12675    #[test]
12676    fn non_utf8_worktree_identities_retain_native_join_and_display_state()
12677    -> Result<(), Box<dyn std::error::Error>> {
12678        let temp = tempfile::tempdir()?;
12679        let control_root = temp.path().join("control");
12680        let common_directory = control_root.join(".git");
12681        fs::create_dir_all(&common_directory)?;
12682        let database = control_root.join(".projectatlas").join("projectatlas.db");
12683        fs::create_dir_all(
12684            database
12685                .parent()
12686                .ok_or_else(|| io::Error::other("control database has no parent"))?,
12687        )?;
12688        let store = AtlasStore::open_for_project(&database, &control_root)?;
12689        let server =
12690            ProjectAtlasMcpServer::new(database, None, "non-utf8-worktrees".to_string(), false);
12691        let mut entries = Vec::new();
12692
12693        for (index, terminal_byte) in [0xff, 0xfe].into_iter().enumerate() {
12694            let name = std::ffi::OsString::from_vec(vec![b'w', b't', b'-', terminal_byte]);
12695            let administrative_directory = common_directory.join("worktrees").join(&name);
12696            let root = temp.path().join(name);
12697            fs::create_dir_all(&administrative_directory)?;
12698            fs::create_dir_all(&root)?;
12699            let entry = GitWorktreeEntry {
12700                role: GitWorktreeRole::Linked,
12701                administrative_directory: administrative_directory.clone(),
12702                state: GitWorktreeState::Active {
12703                    git_control_path: root.join(".git"),
12704                    root: root.clone(),
12705                },
12706            };
12707            let shadow = WorktreeRegistration {
12708                registration_id: i64::try_from(index + 1)?,
12709                alias: WorktreeAlias::parse(&format!("shadow-{index}"))?,
12710                state: WorktreeRegistrationState::Active,
12711                git_common_directory: normalize_native_path_display(&common_directory),
12712                git_common_directory_identity: CanonicalProjectRoot::from_path(&common_directory)?,
12713                git_administrative_directory: normalize_native_path_display(
12714                    &administrative_directory,
12715                ),
12716                git_administrative_directory_identity: CanonicalProjectRoot::from_path(
12717                    &administrative_directory,
12718                )?,
12719                git_administrative_identity: "ab".repeat(32),
12720                last_root: normalize_native_path_display(&root),
12721                last_root_identity: CanonicalProjectRoot::from_path(&root)?,
12722                project_instance_id: None,
12723                accepted_telemetry_revision: 0,
12724                created_at_epoch: 1,
12725                retired_at_epoch: None,
12726            };
12727            let row = server.worktree_list_row(&common_directory, &entry, &[shadow]);
12728            require(
12729                row.selector.is_some()
12730                    && row
12731                        .alias
12732                        .as_deref()
12733                        .is_some_and(|alias| alias == format!("shadow-{index}"))
12734                    && matches!(row.registration, McpWorktreeRegistrationState::Registered)
12735                    && matches!(row.atlas_state, McpWorktreeAtlasState::Invalid)
12736                    && row.root.is_none()
12737                    && matches!(row.path_display, McpWorktreePathDisplayState::Unavailable),
12738                "non-UTF-8 structural row lost its native selector or typed display state",
12739            )?;
12740            entries.push(entry);
12741        }
12742
12743        let first_selector = ProjectAtlasMcpServer::worktree_candidate_selector(&entries[0]);
12744        require(
12745            first_selector != ProjectAtlasMcpServer::worktree_candidate_selector(&entries[1]),
12746            "native administrative identities collapsed into one selector",
12747        )?;
12748        let repository = GitRepositoryStructure {
12749            common_directory,
12750            selection: projectatlas_fs::worktree::GitRepositorySelection::CommonManager {
12751                source_selection: projectatlas_fs::worktree::GitManagerSourceSelection::Ambiguous {
12752                    worktree_count: entries.len(),
12753                },
12754            },
12755            worktrees: entries,
12756        };
12757        let candidates = server.matching_worktree_candidates(&repository, &first_selector);
12758        require(
12759            candidates.len() == 1
12760                && ProjectAtlasMcpServer::worktree_candidate(
12761                    &repository.common_directory,
12762                    candidates[0],
12763                )
12764                .is_some_and(|candidate| {
12765                    candidate.selector == first_selector
12766                        && candidate.root.is_none()
12767                        && matches!(
12768                            candidate.path_display,
12769                            McpWorktreePathDisplayState::Unavailable
12770                        )
12771                }),
12772            "non-UTF-8 structural identity was not selectable with typed display state",
12773        )?;
12774        require(
12775            store.worktree_registrations(true)?.is_empty(),
12776            "non-UTF-8 structural identity reached the registration store",
12777        )?;
12778
12779        let non_utf8_common = temp
12780            .path()
12781            .join(std::ffi::OsString::from_vec(b"common-\xff".to_vec()));
12782        let non_utf8_administrative = non_utf8_common.join("worktrees/utf8");
12783        let non_utf8_root = temp.path().join("utf8");
12784        fs::create_dir_all(&non_utf8_administrative)?;
12785        fs::create_dir(&non_utf8_root)?;
12786        let utf8_entry = GitWorktreeEntry {
12787            role: GitWorktreeRole::Linked,
12788            administrative_directory: non_utf8_administrative,
12789            state: GitWorktreeState::Active {
12790                git_control_path: non_utf8_root.join(".git"),
12791                root: non_utf8_root,
12792            },
12793        };
12794        let row = server.worktree_list_row(&non_utf8_common, &utf8_entry, &[]);
12795        require(
12796            row.selector.is_some()
12797                && row.root.is_some()
12798                && matches!(row.path_display, McpWorktreePathDisplayState::Unavailable)
12799                && row.blocker.as_deref() == Some(MCP_ERROR_WORKTREE_PATH_NON_UTF8)
12800                && ProjectAtlasMcpServer::worktree_candidate(&non_utf8_common, &utf8_entry)
12801                    .is_some_and(|candidate| {
12802                        candidate.root.is_some()
12803                            && matches!(
12804                                candidate.path_display,
12805                                McpWorktreePathDisplayState::Unavailable
12806                            )
12807                    }),
12808            "non-UTF-8 common-directory identity lost its selector or display state",
12809        )
12810    }
12811
12812    #[cfg(unix)]
12813    fn relocate_fixture_administrative_directory(
12814        linked: &Path,
12815        current: &Path,
12816        name: std::ffi::OsString,
12817    ) -> Result<PathBuf, Box<dyn std::error::Error>> {
12818        let relocated = current
12819            .parent()
12820            .ok_or_else(|| io::Error::other("worktree administrative directory has no parent"))?
12821            .join(name);
12822        fs::rename(current, &relocated)?;
12823        let relocated = relocated.canonicalize()?;
12824        let mut pointer = b"gitdir: ".to_vec();
12825        pointer.extend_from_slice(relocated.as_os_str().as_encoded_bytes());
12826        pointer.push(b'\n');
12827        fs::write(linked.join(".git"), pointer)?;
12828        Ok(relocated)
12829    }
12830
12831    #[cfg(unix)]
12832    fn require_real_native_worktree_lifecycle(
12833        case: &str,
12834        primary_name: std::ffi::OsString,
12835        common_name: Option<std::ffi::OsString>,
12836        linked_name: std::ffi::OsString,
12837        administrative_name: Option<std::ffi::OsString>,
12838        expected_utf8: (bool, bool, bool),
12839    ) -> Result<(), Box<dyn std::error::Error>> {
12840        let temp = tempfile::tempdir()?;
12841        let primary = temp.path().join(primary_name);
12842        let linked = temp.path().join(linked_name);
12843        fs::create_dir_all(primary.join("src"))?;
12844        if let Some(common_name) = common_name {
12845            run_fixture_command(
12846                StdCommand::new("git")
12847                    .current_dir(temp.path())
12848                    .args(["init", "--quiet", "--separate-git-dir"])
12849                    .arg(temp.path().join(common_name))
12850                    .arg(&primary),
12851            )?;
12852        } else {
12853            run_fixture_command(
12854                StdCommand::new("git")
12855                    .current_dir(&primary)
12856                    .args(["init", "--quiet"]),
12857            )?;
12858        }
12859        for (key, value) in [
12860            ("user.name", "ProjectAtlas Test"),
12861            ("user.email", "projectatlas@example.invalid"),
12862            ("commit.gpgsign", "false"),
12863            ("core.autocrlf", "false"),
12864        ] {
12865            run_fixture_command(
12866                StdCommand::new("git")
12867                    .current_dir(&primary)
12868                    .args(["config", key, value]),
12869            )?;
12870        }
12871        fs::write(primary.join("src/lib.rs"), "pub fn baseline() {}\n")?;
12872        run_fixture_command(
12873            StdCommand::new("git")
12874                .current_dir(&primary)
12875                .args(["add", "."]),
12876        )?;
12877        run_fixture_command(
12878            StdCommand::new("git")
12879                .current_dir(&primary)
12880                .args(["commit", "--quiet", "-m", "fixture"]),
12881        )?;
12882        run_fixture_command(
12883            StdCommand::new("git")
12884                .current_dir(&primary)
12885                .args(["worktree", "add", "--quiet", "-b", case])
12886                .arg(&linked),
12887        )?;
12888
12889        let linked = linked.canonicalize()?;
12890        if let Some(name) = administrative_name {
12891            let RepositoryStructure::Git(initial) = discover_repository_structure(&linked)? else {
12892                return Err(io::Error::other("fixture was not discovered as Git").into());
12893            };
12894            let current = initial
12895                .worktrees
12896                .iter()
12897                .find(|entry| {
12898                    ProjectAtlasMcpServer::active_worktree_root(entry) == Some(linked.as_path())
12899                })
12900                .ok_or_else(|| io::Error::other("linked fixture was not discovered"))?
12901                .administrative_directory
12902                .clone();
12903            relocate_fixture_administrative_directory(&linked, &current, name)?;
12904        }
12905        require(
12906            run_fixture_command(
12907                StdCommand::new("git")
12908                    .current_dir(&linked)
12909                    .args(["status", "--short"]),
12910            )?
12911            .is_empty(),
12912            "Git could not use the native fixture paths",
12913        )?;
12914
12915        let primary = primary.canonicalize()?;
12916        let control_config = primary.join(PROJECTATLAS_DIR_NAME).join("config.toml");
12917        let control_db = primary
12918            .join(PROJECTATLAS_DIR_NAME)
12919            .join(PROJECTATLAS_DB_FILE_NAME);
12920        init_project_with_config(&primary, Some(&control_config))?;
12921        let mut control = AtlasStore::open_for_project(&control_db, &primary)?;
12922        let plan = ScanRuntimePlan::for_path(Some(&control_config), &primary, None)?;
12923        run_scan_pipeline(
12924            &mut control,
12925            &plan,
12926            &SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, Some(1), None),
12927        )?;
12928        drop(control);
12929        let server = ProjectAtlasMcpServer::new(
12930            control_db.clone(),
12931            Some(control_config),
12932            format!("native-{case}"),
12933            false,
12934        );
12935        let repository = server.control_git_repository()?;
12936        let entry = repository
12937            .worktrees
12938            .iter()
12939            .find(|entry| {
12940                ProjectAtlasMcpServer::active_worktree_root(entry) == Some(linked.as_path())
12941            })
12942            .ok_or_else(|| io::Error::other("native linked worktree was not discovered"))?;
12943        let common_identity = CanonicalProjectRoot::from_path(&repository.common_directory)?;
12944        let administrative_identity =
12945            CanonicalProjectRoot::from_path(&entry.administrative_directory)?;
12946        let root_identity = CanonicalProjectRoot::from_path(&linked)?;
12947        require(
12948            (
12949                linked.to_str().is_some(),
12950                repository.common_directory.to_str().is_some(),
12951                entry.administrative_directory.to_str().is_some(),
12952            ) == expected_utf8,
12953            "fixture did not isolate the expected native path component",
12954        )?;
12955        let selector = ProjectAtlasMcpServer::worktree_candidate_selector(entry);
12956        let display_available = expected_utf8 == (true, true, true);
12957        let listed = server.atlas_worktree_list(Parameters(AtlasWorktreeListParams {
12958            include_retired: Some(false),
12959        }));
12960        let listed_value: serde_json::Value = toon_format::decode_default(&listed)?;
12961        let expected_display = if display_available {
12962            "available"
12963        } else {
12964            "unavailable"
12965        };
12966        require(
12967            listed_value
12968                .pointer("/worktrees/worktrees")
12969                .and_then(serde_json::Value::as_array)
12970                .is_some_and(|rows| {
12971                    rows.iter().any(|row| {
12972                        row.get("selector").and_then(serde_json::Value::as_str)
12973                            == Some(selector.as_str())
12974                            && row.get("path_display").and_then(serde_json::Value::as_str)
12975                                == Some(expected_display)
12976                    })
12977                }),
12978            &format!("native worktree discovery was not publicly selectable: {listed}"),
12979        )?;
12980        let added = server.atlas_worktree_add(Parameters(AtlasWorktreeAddParams {
12981            worktree: selector,
12982            alias: Some(case.to_string()),
12983        }));
12984        require(
12985            added.contains("status: registered")
12986                && added.contains(&format!("alias: \"{case}\""))
12987                && added.contains(if display_available {
12988                    "path_display: available"
12989                } else {
12990                    "path_display: unavailable"
12991                }),
12992            &format!("native worktree registration failed: {added}"),
12993        )?;
12994        let alias = WorktreeAlias::parse(case)?;
12995        let control = open_atlas_store_read_only_for_project(&control_db, &primary)?;
12996        let registration = control.worktree_registration(&alias)?;
12997        require(
12998            registration.git_common_directory_identity == common_identity
12999                && registration.git_administrative_directory_identity == administrative_identity
13000                && registration.last_root_identity == root_identity,
13001            "registration did not retain the exact native identities",
13002        )?;
13003        drop(control);
13004
13005        let initialized = server.atlas_init(Parameters(AtlasInitParams {
13006            project_path: None,
13007            worktree: Some(case.to_string()),
13008            no_scan: Some(false),
13009            force_rescan: Some(false),
13010            text_index_max_bytes: None,
13011        }));
13012        require(
13013            initialized.contains("status: hydrated"),
13014            &format!("alias-routed native initialization failed: {initialized}"),
13015        )?;
13016        fs::write(linked.join("src/native.rs"), "pub fn native_refresh() {}\n")?;
13017        let watched = server.atlas_watch_once(Parameters(AtlasWatchOnceParams {
13018            project_path: None,
13019            worktree: Some(case.to_string()),
13020            path: None,
13021            nearest_project: Some(false),
13022            max_workers: Some(1),
13023            timeout_seconds: None,
13024            text_index_max_bytes: None,
13025            background: Some(false),
13026        }));
13027        require(
13028            watched.contains(MCP_PAYLOAD_WATCH),
13029            &format!("alias-routed native watcher failed: {watched}"),
13030        )?;
13031        let target_db = linked
13032            .join(PROJECTATLAS_DIR_NAME)
13033            .join(PROJECTATLAS_DB_FILE_NAME);
13034        let target = open_atlas_store_read_only_for_project(&target_db, &linked)?;
13035        require(
13036            target.load_node_by_path("src/native.rs")?.is_some(),
13037            "alias-routed watcher did not publish the changed native worktree",
13038        )?;
13039        drop(target);
13040        require(
13041            run_fixture_command(StdCommand::new("git").current_dir(&linked).args([
13042                "status",
13043                "--short",
13044                "--untracked-files=all",
13045            ]))?
13046            .contains("src/native.rs"),
13047            "Git command invocation lost the native worktree path",
13048        )?;
13049
13050        let git_pointer = fs::read(linked.join(".git"))?;
13051        fs::remove_file(linked.join(".git"))?;
13052        let missing_repository = server.control_git_repository()?;
13053        let missing_entry = missing_repository
13054            .worktrees
13055            .iter()
13056            .find(|entry| entry.administrative_directory == administrative_identity.as_path())
13057            .ok_or_else(|| io::Error::other("native missing worktree entry was not discovered"))?;
13058        let missing_row = server.worktree_list_row(
13059            &missing_repository.common_directory,
13060            missing_entry,
13061            std::slice::from_ref(&registration),
13062        );
13063        require(
13064            matches!(missing_row.git_state, McpGitWorktreeState::Missing)
13065                && missing_row.alias.as_deref() == Some(case)
13066                && missing_row.root == root_identity.display_string().ok()
13067                && matches!(
13068                    missing_row.path_display,
13069                    McpWorktreePathDisplayState::Available
13070                ) == display_available,
13071            "Git-known missing native worktree lost its retained root or display state",
13072        )?;
13073        fs::write(linked.join(".git"), git_pointer)?;
13074
13075        let preserved_administrative = temp.path().join("preserved-administrative");
13076        fs::rename(administrative_identity.as_path(), &preserved_administrative)?;
13077        std::os::unix::fs::symlink(&preserved_administrative, administrative_identity.as_path())?;
13078        let invalid_listing = server.atlas_worktree_list(Parameters(AtlasWorktreeListParams {
13079            include_retired: Some(false),
13080        }));
13081        let invalid_value: serde_json::Value = toon_format::decode_default(&invalid_listing)?;
13082        let invalid_rows = invalid_value
13083            .pointer("/worktrees/worktrees")
13084            .and_then(serde_json::Value::as_array)
13085            .ok_or_else(|| io::Error::other("native invalid worktree rows are missing"))?;
13086        require(
13087            invalid_rows.len() == 2
13088                && invalid_rows.iter().any(|row| {
13089                    row.get("alias").and_then(serde_json::Value::as_str) == Some(case)
13090                        && row.get("git_state").and_then(serde_json::Value::as_str)
13091                            == Some("invalid")
13092                        && row.get("registration").and_then(serde_json::Value::as_str)
13093                            == Some("registered")
13094                })
13095                && server
13096                    .state_for_target(None, Some(case.to_string()))
13097                    .is_err(),
13098            &format!("native invalid registration was split or admitted: {invalid_listing}"),
13099        )?;
13100        let invalid_removal = server.atlas_worktree_remove(Parameters(AtlasWorktreeRemoveParams {
13101            worktree: case.to_string(),
13102        }));
13103        require(
13104            invalid_removal.contains("cannot retire worktree")
13105                && invalid_removal.contains("Git evidence is invalid"),
13106            &format!("native invalid evidence allowed retirement: {invalid_removal}"),
13107        )?;
13108        fs::remove_file(administrative_identity.as_path())?;
13109        fs::rename(&preserved_administrative, administrative_identity.as_path())?;
13110
13111        let removed = server.atlas_worktree_remove(Parameters(AtlasWorktreeRemoveParams {
13112            worktree: case.to_string(),
13113        }));
13114        require(
13115            removed.contains("status: retired")
13116                && removed.contains(if display_available {
13117                    "path_display: available"
13118                } else {
13119                    "path_display: unavailable"
13120                }),
13121            &format!("native worktree retirement failed: {removed}"),
13122        )?;
13123        let control = open_atlas_store_read_only_for_project(&control_db, &primary)?;
13124        let retired = control
13125            .worktree_registrations(true)?
13126            .into_iter()
13127            .find(|registration| registration.alias == alias)
13128            .ok_or_else(|| io::Error::other("retired native registration is missing"))?;
13129        require(
13130            retired.state == WorktreeRegistrationState::Retired
13131                && retired.git_common_directory_identity == common_identity
13132                && retired.git_administrative_directory_identity == administrative_identity
13133                && retired.last_root_identity == root_identity,
13134            "retirement changed the persisted native identities",
13135        )?;
13136        require(
13137            server
13138                .state_for_target(None, Some(case.to_string()))
13139                .is_err(),
13140            "retired native alias remained routable",
13141        )
13142    }
13143
13144    // APFS rejects invalid UTF-8 filenames before Git or ProjectAtlas can use them.
13145    #[cfg(all(unix, not(target_os = "macos")))]
13146    #[test]
13147    fn real_non_utf8_worktree_lifecycle_routes_alias_and_preserves_native_identities()
13148    -> Result<(), Box<dyn std::error::Error>> {
13149        for (case, primary, common, linked, administrative, expected_utf8) in [
13150            (
13151                "invalid-root",
13152                std::ffi::OsString::from("root-control"),
13153                None,
13154                std::ffi::OsString::from_vec(b"root-\xff".to_vec()),
13155                Some(std::ffi::OsString::from("root-administrative")),
13156                (false, true, true),
13157            ),
13158            (
13159                "invalid-common",
13160                std::ffi::OsString::from("common-control"),
13161                Some(std::ffi::OsString::from_vec(b"common-\xfe ".to_vec())),
13162                std::ffi::OsString::from("common-linked"),
13163                None,
13164                (true, false, false),
13165            ),
13166            (
13167                "invalid-administrative",
13168                std::ffi::OsString::from("administrative-control"),
13169                None,
13170                std::ffi::OsString::from("administrative-linked"),
13171                Some(std::ffi::OsString::from_vec(
13172                    b"administrative-\xfd\t".to_vec(),
13173                )),
13174                (true, true, false),
13175            ),
13176        ] {
13177            require_real_native_worktree_lifecycle(
13178                case,
13179                primary,
13180                common,
13181                linked,
13182                administrative,
13183                expected_utf8,
13184            )?;
13185        }
13186        Ok(())
13187    }
13188
13189    #[cfg(unix)]
13190    #[test]
13191    fn real_unicode_worktree_lifecycle_routes_alias_and_preserves_native_identities()
13192    -> Result<(), Box<dyn std::error::Error>> {
13193        require_real_native_worktree_lifecycle(
13194            "valid-unicode",
13195            std::ffi::OsString::from("unicode-control-λ"),
13196            Some(std::ffi::OsString::from("unicode-common-共同 ")),
13197            std::ffi::OsString::from("unicode-linked-工作树"),
13198            Some(std::ffi::OsString::from("unicode-administrative-管理\t")),
13199            (true, true, true),
13200        )
13201    }
13202
13203    #[test]
13204    fn worktree_registration_revalidates_captured_local_atlas_identity()
13205    -> Result<(), Box<dyn std::error::Error>> {
13206        let temp = tempfile::tempdir()?;
13207        let root = temp.path().join("worktree");
13208        fs::create_dir(&root)?;
13209        let state = root.join(PROJECTATLAS_DIR_NAME);
13210        let db_path = state.join(PROJECTATLAS_DB_FILE_NAME);
13211        let config_path = state.join("config.toml");
13212        init_project_with_config(&root, Some(&config_path))?;
13213        drop(AtlasStore::open_for_project(&db_path, &root)?);
13214        let captured = ProjectAtlasMcpServer::local_worktree_atlas(&root)?
13215            .ok_or_else(|| io::Error::other("captured worktree atlas is missing"))?;
13216        ProjectAtlasMcpServer::revalidate_local_worktree_atlas_identity(
13217            &root,
13218            Some(captured.project_instance_id),
13219        )?;
13220
13221        let preserved = root.join(".projectatlas-captured-registration");
13222        fs::rename(&state, &preserved)?;
13223        fs::create_dir(&state)?;
13224        let replacement = AtlasStore::open_for_project(&db_path, &root)?;
13225        require(
13226            replacement.project_instance_id()? != Some(captured.project_instance_id),
13227            "replacement atlas reused the captured registration identity",
13228        )?;
13229        drop(replacement);
13230        let rejected = ProjectAtlasMcpServer::revalidate_local_worktree_atlas_identity(
13231            &root,
13232            Some(captured.project_instance_id),
13233        );
13234        require(
13235            rejected.as_ref().is_err_and(|error| {
13236                error
13237                    .to_string()
13238                    .contains(MCP_ERROR_WORKTREE_IDENTITY_CONFLICT)
13239            }),
13240            "registration guard accepted a replacement local atlas",
13241        )?;
13242        fs::remove_dir_all(&state)?;
13243        let missing = ProjectAtlasMcpServer::revalidate_local_worktree_atlas_identity(
13244            &root,
13245            Some(captured.project_instance_id),
13246        );
13247        require(
13248            missing.as_ref().is_err_and(|error| {
13249                error
13250                    .to_string()
13251                    .contains(MCP_ERROR_WORKTREE_IDENTITY_CONFLICT)
13252            }),
13253            "registration guard accepted a missing captured local atlas",
13254        )?;
13255
13256        let uninitialized = temp.path().join("uninitialized");
13257        fs::create_dir(&uninitialized)?;
13258        ProjectAtlasMcpServer::revalidate_local_worktree_atlas_identity(&uninitialized, None)
13259            .map_err(Into::into)
13260    }
13261
13262    #[test]
13263    fn registered_init_rejects_replaced_git_lifecycle_before_or_during_activation_and_binding()
13264    -> Result<(), Box<dyn std::error::Error>> {
13265        let fixture = registered_worktree_race_fixture("init-race")?;
13266        let (candidate, candidate_path, work_control) = prepared_hydration_candidate(&fixture)?;
13267
13268        let replacement_entry = replace_registered_worktree(&fixture, "replacement-init")?;
13269        require(
13270            replacement_entry.administrative_directory == fixture.administrative_directory
13271                && git_administrative_identity(&replacement_entry.administrative_directory)?
13272                    != fixture.administrative_identity,
13273            "replacement fixture did not reuse the administrative path with a new lifecycle",
13274        )?;
13275        fs::create_dir_all(
13276            fixture
13277                .target_db
13278                .parent()
13279                .ok_or_else(|| io::Error::other("replacement database has no parent"))?,
13280        )?;
13281        let replacement = AtlasStore::open_for_project(&fixture.target_db, &fixture.state.root)?;
13282        let replacement_project = replacement
13283            .project_instance_id()?
13284            .ok_or(DbError::ProjectInstanceIdentityMissing)?;
13285        drop(replacement);
13286
13287        let activation = fixture.server.activate_registered_worktree_hydration(
13288            &fixture.state,
13289            &fixture.selection,
13290            candidate,
13291            &work_control,
13292        );
13293        require(
13294            activation.as_ref().is_err_and(|error| {
13295                error
13296                    .to_string()
13297                    .contains("administrative lifecycle changed")
13298            }),
13299            "hydration candidate activated into a replacement Git lifecycle",
13300        )?;
13301        require(
13302            !candidate_path.exists()
13303                && open_atlas_store_read_only_for_project(&fixture.target_db, &fixture.state.root)?
13304                    .project_instance_id()?
13305                    == Some(replacement_project),
13306            "rejected hydration changed the replacement atlas or retained its candidate",
13307        )?;
13308
13309        let binding = fixture
13310            .server
13311            .bind_initialized_worktree(&fixture.selection, &fixture.state);
13312        require(
13313            binding.as_ref().is_err_and(|error| {
13314                error
13315                    .to_string()
13316                    .contains("administrative lifecycle changed")
13317            }),
13318            "final init binding accepted a replacement Git lifecycle",
13319        )?;
13320        let control =
13321            open_atlas_store_read_only_for_project(&fixture.control_db, &fixture.control_root)?;
13322        require(
13323            control
13324                .worktree_registration(&fixture.alias)?
13325                .project_instance_id
13326                .is_none(),
13327            "failed activation or binding attached the replacement atlas",
13328        )?;
13329
13330        let during = registered_worktree_race_fixture("init-publish-race")?;
13331        let (candidate, _candidate_path, work_control) = prepared_hydration_candidate(&during)?;
13332        let replacement_project = std::cell::Cell::new(None);
13333        let activation = during
13334            .server
13335            .activate_registered_worktree_hydration_with_post_publication(
13336                &during.state,
13337                &during.selection,
13338                candidate,
13339                &work_control,
13340                || {
13341                    replace_registered_worktree(&during, "replacement-during-publish")
13342                        .map_err(|error| CliError::InvalidInput(error.to_string()))?;
13343                    fs::create_dir_all(during.target_db.parent().ok_or_else(|| {
13344                        CliError::InvalidInput("replacement database has no parent".to_string())
13345                    })?)
13346                    .map_err(|source| CliError::Io {
13347                        path: during.state.root.clone(),
13348                        source,
13349                    })?;
13350                    let replacement =
13351                        AtlasStore::open_for_project(&during.target_db, &during.state.root)?;
13352                    replacement_project.set(Some(
13353                        replacement
13354                            .project_instance_id()?
13355                            .ok_or(DbError::ProjectInstanceIdentityMissing)?,
13356                    ));
13357                    Ok(())
13358                },
13359            );
13360        let activation_error = activation.as_ref().err().map(ToString::to_string);
13361        require(
13362            activation_error
13363                .as_deref()
13364                .is_some_and(|error| error.contains("administrative lifecycle changed")),
13365            &format!(
13366                "hydration bound a lifecycle replaced after candidate publication: {activation_error:?}"
13367            ),
13368        )?;
13369        let replacement =
13370            open_atlas_store_read_only_for_project(&during.target_db, &during.state.root)?;
13371        let control =
13372            open_atlas_store_read_only_for_project(&during.control_db, &during.control_root)?;
13373        require(
13374            replacement.project_instance_id()? == replacement_project.get()
13375                && control
13376                    .worktree_registration(&during.alias)?
13377                    .project_instance_id
13378                    .is_none(),
13379            "post-publication lifecycle rejection changed or bound the replacement atlas",
13380        )
13381    }
13382
13383    #[test]
13384    fn final_retirement_does_not_import_replacement_lifecycle_telemetry()
13385    -> Result<(), Box<dyn std::error::Error>> {
13386        let fixture = registered_worktree_race_fixture("retire-race")?;
13387        let replacement_entry = replace_registered_worktree(&fixture, "replacement-retire")?;
13388        fs::create_dir_all(
13389            fixture
13390                .target_db
13391                .parent()
13392                .ok_or_else(|| io::Error::other("replacement database has no parent"))?,
13393        )?;
13394        let replacement = AtlasStore::open_for_project(&fixture.target_db, &fixture.state.root)?;
13395        replacement.record_usage(&usage_from_text(
13396            "replacement",
13397            "atlas_overview",
13398            None,
13399            None,
13400            "pub fn replacement() {}",
13401            "repository overview",
13402        ))?;
13403        let replacement_project = replacement
13404            .project_instance_id()?
13405            .ok_or(DbError::ProjectInstanceIdentityMissing)?;
13406        drop(replacement);
13407        let control = open_atlas_store_for_project(&fixture.control_db, &fixture.control_root)?;
13408        let (retired, synchronized, blocker) = ProjectAtlasMcpServer::retire_registered_worktree(
13409            &control,
13410            &fixture.registration,
13411            Some(&fixture.state.root),
13412            2,
13413            None,
13414        )?;
13415        require(
13416            retired.state == WorktreeRegistrationState::Retired
13417                && retired.project_instance_id.is_none()
13418                && retired.accepted_telemetry_revision == 0
13419                && synchronized.is_none()
13420                && blocker.as_deref() == Some(MCP_ERROR_WORKTREE_LIFECYCLE_CHANGED),
13421            "replacement lifecycle telemetry was bound, imported, or hidden during retirement",
13422        )?;
13423
13424        let repository = fixture.server.control_git_repository()?;
13425        let replacement_alias = WorktreeAlias::parse("replacement-owner")?;
13426        let replacement_registration = control.register_worktree(
13427            &replacement_alias,
13428            &repository.common_directory,
13429            &replacement_entry.administrative_directory,
13430            &git_administrative_identity(&replacement_entry.administrative_directory)?,
13431            &fixture.state.root,
13432            Some(replacement_project),
13433            3,
13434        )?;
13435        require(
13436            replacement_registration.project_instance_id == Some(replacement_project),
13437            "retired origin stranded the replacement atlas identity",
13438        )?;
13439
13440        let open_race = registered_worktree_race_fixture("retire-open-race")?;
13441        let control = open_atlas_store_for_project(&open_race.control_db, &open_race.primary)?;
13442        let sentinel = b"replacement atlas must remain untouched";
13443        let (retired, synchronized, blocker) =
13444            ProjectAtlasMcpServer::retire_registered_worktree_with_pre_open(
13445                &control,
13446                &open_race.registration,
13447                Some(&open_race.state.root),
13448                2,
13449                None,
13450                || {
13451                    replace_registered_worktree(&open_race, "replacement-before-local-open")
13452                        .map_err(|error| CliError::InvalidInput(error.to_string()))?;
13453                    fs::create_dir_all(open_race.target_db.parent().ok_or_else(|| {
13454                        CliError::InvalidInput("replacement database has no parent".to_string())
13455                    })?)
13456                    .map_err(|source| CliError::Io {
13457                        path: open_race.state.root.clone(),
13458                        source,
13459                    })?;
13460                    fs::write(&open_race.target_db, sentinel).map_err(|source| CliError::Io {
13461                        path: open_race.target_db.clone(),
13462                        source,
13463                    })
13464                },
13465            )?;
13466        require(
13467            retired.state == WorktreeRegistrationState::Retired
13468                && retired.project_instance_id.is_none()
13469                && synchronized.is_none()
13470                && blocker.as_deref() == Some(MCP_ERROR_WORKTREE_LIFECYCLE_CHANGED)
13471                && fs::read(&open_race.target_db)? == sentinel,
13472            "replacement database failure blocked stale retirement or changed replacement bytes",
13473        )
13474    }
13475
13476    #[test]
13477    fn retirement_reclassifies_identity_and_snapshot_failures_after_lifecycle_replacement()
13478    -> Result<(), Box<dyn std::error::Error>> {
13479        for (suffix, failure) in [
13480            ("retire-identity-failure", "project identity read failed"),
13481            ("retire-snapshot-failure", "usage snapshot export failed"),
13482        ] {
13483            let fixture = registered_worktree_race_fixture(suffix)?;
13484            let replacement_entry = replace_registered_worktree(&fixture, suffix)?;
13485            fs::create_dir_all(
13486                fixture
13487                    .target_db
13488                    .parent()
13489                    .ok_or_else(|| io::Error::other("replacement database has no parent"))?,
13490            )?;
13491            let replacement =
13492                AtlasStore::open_for_project(&fixture.target_db, &fixture.state.root)?;
13493            replacement.record_usage(&usage_from_text(
13494                suffix,
13495                "atlas_token_report",
13496                None,
13497                None,
13498                "pub fn replacement() {}",
13499                "replacement telemetry",
13500            ))?;
13501            let replacement_project = replacement
13502                .project_instance_id()?
13503                .ok_or(DbError::ProjectInstanceIdentityMissing)?;
13504            drop(replacement);
13505
13506            let control = open_atlas_store_for_project(&fixture.control_db, &fixture.control_root)?;
13507            let classified = control.with_active_worktree_registration(
13508                fixture.registration.registration_id,
13509                &fixture.alias,
13510                |guard| {
13511                    ProjectAtlasMcpServer::classify_retirement_failure(
13512                        guard,
13513                        &fixture.state.root,
13514                        2,
13515                        CliError::InvalidInput(failure.to_string()),
13516                    )
13517                },
13518            )?;
13519            let (retired, synchronized, blocker) = classified?;
13520            let replacement =
13521                open_atlas_store_read_only_for_project(&fixture.target_db, &fixture.state.root)?;
13522            require(
13523                retired.state == WorktreeRegistrationState::Retired
13524                    && retired.project_instance_id.is_none()
13525                    && synchronized.is_none()
13526                    && blocker.as_deref() == Some(MCP_ERROR_WORKTREE_LIFECYCLE_CHANGED)
13527                    && replacement.project_instance_id()? == Some(replacement_project)
13528                    && replacement.token_overview(Some(suffix))?.calls == 1,
13529                "retirement failure classification changed or imported replacement state",
13530            )?;
13531
13532            let repository = fixture.server.control_git_repository()?;
13533            let replacement_alias = WorktreeAlias::parse(&format!("{suffix}-owner"))?;
13534            let replacement_registration = control.register_worktree(
13535                &replacement_alias,
13536                &repository.common_directory,
13537                &replacement_entry.administrative_directory,
13538                &git_administrative_identity(&replacement_entry.administrative_directory)?,
13539                &fixture.state.root,
13540                Some(replacement_project),
13541                3,
13542            )?;
13543            require(
13544                replacement_registration.project_instance_id == Some(replacement_project),
13545                "retirement failure classification stranded replacement ownership",
13546            )?;
13547        }
13548        Ok(())
13549    }
13550
13551    #[test]
13552    fn registered_reset_and_binding_linearize_without_recreating_a_reset_winner()
13553    -> Result<(), Box<dyn std::error::Error>> {
13554        let bind_wins = registered_worktree_race_fixture("reset-bind-wins")?;
13555        fs::create_dir_all(
13556            bind_wins
13557                .target_db
13558                .parent()
13559                .ok_or_else(|| io::Error::other("bind-wins database has no parent"))?,
13560        )?;
13561        let target = AtlasStore::open_for_project(&bind_wins.target_db, &bind_wins.state.root)?;
13562        let project = target
13563            .project_instance_id()?
13564            .ok_or(DbError::ProjectInstanceIdentityMissing)?;
13565        drop(target);
13566        let before = fs::read(&bind_wins.target_db)?;
13567        let control = open_atlas_store_for_project(&bind_wins.control_db, &bind_wins.primary)?;
13568        control.bind_worktree_project(
13569            bind_wins.registration.registration_id,
13570            &bind_wins.alias,
13571            &bind_wins.state.root,
13572            project,
13573        )?;
13574        let rejected = bind_wins.server.reset_registered_worktree_index(
13575            &bind_wins.state,
13576            &bind_wins.selection,
13577            false,
13578        );
13579        require(
13580            rejected.as_ref().is_err_and(|error| {
13581                error
13582                    .to_string()
13583                    .contains(MCP_ERROR_BOUND_WORKTREE_RESET_UNSUPPORTED)
13584            }) && fs::read(&bind_wins.target_db)? == before,
13585            "reset deleted a target after a concurrent binding won",
13586        )?;
13587
13588        let reset_wins = registered_worktree_race_fixture("reset-delete-wins")?;
13589        fs::create_dir_all(
13590            reset_wins
13591                .target_db
13592                .parent()
13593                .ok_or_else(|| io::Error::other("reset-wins database has no parent"))?,
13594        )?;
13595        drop(AtlasStore::open_for_project(
13596            &reset_wins.target_db,
13597            &reset_wins.state.root,
13598        )?);
13599        reset_wins.server.reset_registered_worktree_index(
13600            &reset_wins.state,
13601            &reset_wins.selection,
13602            false,
13603        )?;
13604        require(
13605            !reset_wins.target_db.exists(),
13606            "winning reset did not delete the unbound target atlas",
13607        )?;
13608        let late_bind = ProjectAtlasMcpServer::open_registered_worktree_mut_store(
13609            &reset_wins.state,
13610            &reset_wins.server.control_state,
13611            &reset_wins.selection,
13612        );
13613        let control =
13614            open_atlas_store_read_only_for_project(&reset_wins.control_db, &reset_wins.primary)?;
13615        require(
13616            late_bind.is_err()
13617                && !reset_wins.target_db.exists()
13618                && control
13619                    .worktree_registration(&reset_wins.alias)?
13620                    .project_instance_id
13621                    .is_none(),
13622            "late binding recreated or attached the atlas deleted by reset",
13623        )?;
13624
13625        let replaced = registered_worktree_race_fixture("reset-replaced-lifecycle")?;
13626        let replacement_files = [
13627            replaced.target_db.clone(),
13628            db_sidecar_path(&replaced.target_db, "wal"),
13629            db_sidecar_path(&replaced.target_db, "shm"),
13630            db_sidecar_path(&replaced.target_db, "journal"),
13631            mcp_config_path_for_db(&replaced.target_db),
13632        ];
13633        let replacement_bytes = replacement_files
13634            .iter()
13635            .enumerate()
13636            .map(|(index, _)| format!("replacement-owned-{index}").into_bytes())
13637            .collect::<Vec<_>>();
13638        let rejected = replaced
13639            .server
13640            .reset_registered_worktree_index_with_post_validation(
13641                &replaced.state,
13642                &replaced.selection,
13643                true,
13644                || {
13645                    replace_registered_worktree(&replaced, "replacement-during-reset")
13646                        .map_err(|error| CliError::InvalidInput(error.to_string()))?;
13647                    fs::create_dir_all(replaced.target_db.parent().ok_or_else(|| {
13648                        CliError::InvalidInput("replacement database has no parent".to_string())
13649                    })?)
13650                    .map_err(|source| CliError::Io {
13651                        path: replaced.state.root.clone(),
13652                        source,
13653                    })?;
13654                    for (path, bytes) in replacement_files.iter().zip(&replacement_bytes) {
13655                        fs::write(path, bytes).map_err(|source| CliError::Io {
13656                            path: path.clone(),
13657                            source,
13658                        })?;
13659                    }
13660                    Ok(())
13661                },
13662            );
13663        let control =
13664            open_atlas_store_read_only_for_project(&replaced.control_db, &replaced.primary)?;
13665        require(
13666            rejected.as_ref().is_err_and(|error| {
13667                error
13668                    .to_string()
13669                    .contains("administrative lifecycle changed")
13670            }) && replacement_files
13671                .iter()
13672                .zip(&replacement_bytes)
13673                .all(|(path, bytes)| {
13674                    fs::read(path).is_ok_and(|found| found.as_slice() == bytes.as_slice())
13675                })
13676                && control
13677                    .worktree_registration(&replaced.alias)?
13678                    .project_instance_id
13679                    .is_none(),
13680            "guarded reset deleted replacement lifecycle database, sidecars, or MCP config",
13681        )
13682    }
13683
13684    #[test]
13685    fn registered_worktree_missing_atlas_does_not_refresh_control_root()
13686    -> Result<(), Box<dyn std::error::Error>> {
13687        let fixture = registered_worktree_race_fixture("missing-atlas")?;
13688        let expected_project = ProjectInstanceId::from_bytes([0xA; 16])?;
13689        let control = AtlasStore::open_for_project(&fixture.control_db, &fixture.control_root)?;
13690        control.bind_worktree_project(
13691            fixture.registration.registration_id,
13692            &fixture.alias,
13693            &fixture.state.root,
13694            expected_project,
13695        )?;
13696        drop(control);
13697
13698        let sidecars = ["-wal", "-shm", "-journal"].map(|suffix| {
13699            let mut path = fixture.control_db.as_os_str().to_os_string();
13700            path.push(suffix);
13701            PathBuf::from(path)
13702        });
13703        let before = std::iter::once(&fixture.control_db)
13704            .chain(sidecars.iter())
13705            .map(|path| fs::read(path).ok())
13706            .collect::<Vec<_>>();
13707        let result = fixture
13708            .server
13709            .state_for_target(None, Some(fixture.alias.to_string()));
13710        require(
13711            result.is_err(),
13712            "missing bound atlas was accepted during registered resolution",
13713        )?;
13714        let after = std::iter::once(&fixture.control_db)
13715            .chain(sidecars.iter())
13716            .map(|path| fs::read(path).ok())
13717            .collect::<Vec<_>>();
13718        require(
13719            before == after,
13720            "failed atlas validation refreshed the control registration",
13721        )?;
13722        Ok(())
13723    }
13724
13725    #[test]
13726    fn unbound_registered_worktree_move_refreshes_root_before_missing_retirement()
13727    -> Result<(), Box<dyn std::error::Error>> {
13728        let fixture = registered_worktree_race_fixture("unbound-move")?;
13729        let original_root_display = normalize_native_path_display(&fixture.state.root);
13730        let moved = fixture
13731            .primary
13732            .parent()
13733            .ok_or_else(|| io::Error::other("moved worktree has no parent"))?
13734            .join("moved-unbound-worktree");
13735        run_fixture_command(
13736            StdCommand::new("git")
13737                .current_dir(&fixture.primary)
13738                .args(["worktree", "move"])
13739                .arg(&fixture.linked)
13740                .arg(&moved),
13741        )?;
13742        let moved_root = moved.canonicalize()?;
13743        let unresolved = fixture
13744            .server
13745            .state_for_target(None, Some(fixture.alias.to_string()));
13746        require(
13747            unresolved.is_err(),
13748            "unbound moved worktree without an atlas was accepted as initialized",
13749        )?;
13750
13751        let control = AtlasStore::open_for_project(&fixture.control_db, &fixture.control_root)?;
13752        let refreshed = control.worktree_registration(&fixture.alias)?;
13753        require(
13754            refreshed.project_instance_id.is_none()
13755                && refreshed.last_root == normalize_native_path_display(&moved_root),
13756            "unbound Git move did not refresh its registered root",
13757        )?;
13758        drop(control);
13759
13760        run_fixture_command(
13761            StdCommand::new("git")
13762                .current_dir(&fixture.primary)
13763                .args(["worktree", "remove", "--force"])
13764                .arg(&moved),
13765        )?;
13766        let missing = fixture
13767            .server
13768            .atlas_worktree_list(Parameters(AtlasWorktreeListParams {
13769                include_retired: Some(false),
13770            }));
13771        require(
13772            missing.contains(&normalize_native_path_display(&moved_root))
13773                && !missing.contains(&original_root_display)
13774                && missing.contains("\"unbound-move\",linked,available,missing,registered"),
13775            &format!("missing worktree reporting retained the pre-move root: {missing}"),
13776        )?;
13777        let retired = fixture
13778            .server
13779            .atlas_worktree_remove(Parameters(AtlasWorktreeRemoveParams {
13780                worktree: fixture.alias.to_string(),
13781            }));
13782        require(
13783            retired.contains("status: retired")
13784                && retired.contains(&normalize_native_path_display(&moved_root))
13785                && !retired.contains(&original_root_display),
13786            &format!("retirement reporting retained the pre-move root: {retired}"),
13787        )
13788    }
13789
13790    #[test]
13791    fn registered_worktree_move_refreshes_registry_root_after_local_rebind()
13792    -> Result<(), Box<dyn std::error::Error>> {
13793        let fixture = registered_worktree_race_fixture("moved-root")?;
13794        let original_root = fixture.state.root.clone();
13795        let original_root_display = normalize_native_path_display(&original_root);
13796        let target_config = fixture
13797            .linked
13798            .join(PROJECTATLAS_DIR_NAME)
13799            .join(PROJECTATLAS_CONFIG_FILE_NAME);
13800        init_project_with_config(&fixture.linked, Some(&target_config))?;
13801        let target_store = AtlasStore::open_for_project(&fixture.target_db, &fixture.linked)?;
13802        let target_project = target_store
13803            .project_instance_id()?
13804            .ok_or(DbError::ProjectInstanceIdentityMissing)?;
13805        drop(target_store);
13806
13807        let registered = {
13808            let control = AtlasStore::open_for_project(&fixture.control_db, &fixture.control_root)?;
13809            control.register_worktree(
13810                &fixture.alias,
13811                &fixture.control_root.join(".git"),
13812                &fixture.administrative_directory,
13813                &fixture.registration.git_administrative_identity,
13814                &original_root,
13815                Some(target_project),
13816                1,
13817            )?
13818        };
13819        require(
13820            registered.project_instance_id == Some(target_project),
13821            "moved worktree fixture remained unbound",
13822        )?;
13823        let original_registration = registered;
13824
13825        let moved = fixture
13826            .primary
13827            .parent()
13828            .ok_or_else(|| io::Error::other("moved worktree has no parent"))?
13829            .join("moved-worktree");
13830        run_fixture_command(
13831            StdCommand::new("git")
13832                .current_dir(&fixture.primary)
13833                .args(["worktree", "move"])
13834                .arg(&fixture.linked)
13835                .arg(&moved),
13836        )?;
13837        let moved_root = moved.canonicalize()?;
13838        let moved_db = moved_root
13839            .join(PROJECTATLAS_DIR_NAME)
13840            .join(PROJECTATLAS_DB_FILE_NAME);
13841        let moved_binding = AtlasStore::transition_project_root(
13842            &moved_db,
13843            &moved_root,
13844            ProjectRootTransition::Move,
13845        )?;
13846        require(
13847            moved_binding.project_instance_id == target_project,
13848            "local root rebind changed the registered project identity",
13849        )?;
13850        require(
13851            read_project_root_identity_read_only(&moved_db)?
13852                == Some(CanonicalProjectRoot::from_path(&moved_root)?),
13853            "local root rebind did not establish the moved native identity",
13854        )?;
13855
13856        let resolved = fixture
13857            .server
13858            .state_for_target(None, Some(fixture.alias.to_string()))?;
13859        require(
13860            resolved.root == moved_root,
13861            "alias resolution did not use the moved Git root",
13862        )?;
13863        let control = AtlasStore::open_for_project(&fixture.control_db, &fixture.control_root)?;
13864        let refreshed = control.worktree_registration(&fixture.alias)?;
13865        require(
13866            refreshed.last_root == normalize_native_path_display(&moved_root)
13867                && refreshed.last_root != original_root_display,
13868            "alias resolution retained the stale registry root",
13869        )?;
13870        require(
13871            refreshed.registration_id == original_registration.registration_id
13872                && refreshed.alias == original_registration.alias
13873                && refreshed.git_common_directory == original_registration.git_common_directory
13874                && refreshed.git_administrative_directory
13875                    == original_registration.git_administrative_directory
13876                && refreshed.git_administrative_identity
13877                    == original_registration.git_administrative_identity
13878                && refreshed.project_instance_id == original_registration.project_instance_id
13879                && refreshed.accepted_telemetry_revision
13880                    == original_registration.accepted_telemetry_revision,
13881            "moved root refresh changed administrative identity or telemetry state",
13882        )?;
13883        drop(control);
13884
13885        run_fixture_command(
13886            StdCommand::new("git")
13887                .current_dir(&fixture.primary)
13888                .args(["worktree", "remove", "--force"])
13889                .arg(&moved),
13890        )?;
13891        let missing = fixture
13892            .server
13893            .atlas_worktree_list(Parameters(AtlasWorktreeListParams {
13894                include_retired: Some(false),
13895            }));
13896        require(
13897            missing.contains(&normalize_native_path_display(&moved_root))
13898                && !missing.contains(&original_root_display)
13899                && missing.contains("\"moved-root\",linked,available,missing,registered"),
13900            &format!("missing worktree reporting regressed to the pre-move root: {missing}"),
13901        )?;
13902        let retired = fixture
13903            .server
13904            .atlas_worktree_remove(Parameters(AtlasWorktreeRemoveParams {
13905                worktree: fixture.alias.to_string(),
13906            }));
13907        require(
13908            retired.contains("status: retired")
13909                && retired.contains(&normalize_native_path_display(&moved_root))
13910                && !retired.contains(&original_root_display),
13911            &format!("retirement did not preserve the moved root: {retired}"),
13912        )
13913    }
13914
13915    #[test]
13916    fn worktree_tools_register_route_and_retire_without_git_or_file_lifecycle_mutation()
13917    -> Result<(), Box<dyn std::error::Error>> {
13918        let temp = tempfile::tempdir()?;
13919        let primary = temp.path().join("selected control");
13920        let worktree_a = temp.path().join("unrelated one").join("checkout");
13921        let worktree_b = temp.path().join("unrelated two").join("checkout");
13922        fs::create_dir_all(&primary)?;
13923        fs::create_dir_all(
13924            worktree_a
13925                .parent()
13926                .ok_or_else(|| io::Error::other("worktree A has no parent"))?,
13927        )?;
13928        fs::create_dir_all(
13929            worktree_b
13930                .parent()
13931                .ok_or_else(|| io::Error::other("worktree B has no parent"))?,
13932        )?;
13933        run_fixture_command(StdCommand::new("git").current_dir(&primary).arg("init"))?;
13934        for (key, value) in [
13935            ("user.name", "ProjectAtlas Test"),
13936            ("user.email", "projectatlas@example.invalid"),
13937            ("commit.gpgsign", "false"),
13938            ("core.autocrlf", "false"),
13939        ] {
13940            run_fixture_command(
13941                StdCommand::new("git")
13942                    .current_dir(&primary)
13943                    .args(["config", key, value]),
13944            )?;
13945        }
13946        fs::create_dir(primary.join("src"))?;
13947        fs::write(
13948            primary.join("src").join("lib.rs"),
13949            "mod child;\npub fn main_only() { child::helper(); }\n",
13950        )?;
13951        fs::write(primary.join("src").join("child.rs"), "pub fn helper() {}\n")?;
13952        run_fixture_command(
13953            StdCommand::new("git")
13954                .current_dir(&primary)
13955                .args(["add", "."]),
13956        )?;
13957        run_fixture_command(
13958            StdCommand::new("git")
13959                .current_dir(&primary)
13960                .args(["commit", "-m", "fixture"]),
13961        )?;
13962        run_fixture_command(
13963            StdCommand::new("git")
13964                .current_dir(&primary)
13965                .args(["worktree", "add", "-b", "issue-430-a"])
13966                .arg(&worktree_a),
13967        )?;
13968        run_fixture_command(
13969            StdCommand::new("git")
13970                .current_dir(&primary)
13971                .args(["worktree", "add", "-b", "issue-430-b"])
13972                .arg(&worktree_b),
13973        )?;
13974        fs::write(
13975            worktree_a.join("src").join("branch.rs"),
13976            "pub fn worktree_only() {}\n",
13977        )?;
13978        let git_before = run_fixture_command(StdCommand::new("git").current_dir(&primary).args([
13979            "worktree",
13980            "list",
13981            "--porcelain",
13982        ]))?;
13983
13984        let control_db = primary.join(".projectatlas").join("projectatlas.db");
13985        let control_config = primary.join(".projectatlas").join("config.toml");
13986        init_project_with_config(&primary, Some(&control_config))?;
13987        let mut control_store = AtlasStore::open_for_project(&control_db, &primary)?;
13988        let control_plan = ScanRuntimePlan::for_path(Some(&control_config), &primary, None)?;
13989        run_scan_pipeline(
13990            &mut control_store,
13991            &control_plan,
13992            &SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, None, None),
13993        )?;
13994        control_store.set_purpose(
13995            "src/lib.rs",
13996            "Own the shared library contract.",
13997            PurposeSource::Agent,
13998        )?;
13999        drop(control_store);
14000        let server = ProjectAtlasMcpServer::new(
14001            control_db.clone(),
14002            Some(control_config),
14003            "worktree-tools".to_string(),
14004            false,
14005        );
14006        let control_before_blank_selector = fs::read(&control_db)?;
14007        let blank_selector = server.atlas_reset_index(Parameters(AtlasResetIndexParams {
14008            project_path: None,
14009            worktree: Some("   ".to_string()),
14010            apply: Some(true),
14011            dry_run: Some(false),
14012            include_mcp_config: Some(true),
14013        }));
14014        require(
14015            blank_selector.contains(MCP_ERROR_WORKTREE_SELECTOR_EMPTY),
14016            "blank worktree selector fell back to the control atlas",
14017        )?;
14018        require(
14019            fs::read(&control_db)? == control_before_blank_selector,
14020            "blank worktree selector changed the control database",
14021        )?;
14022        let repository = server.control_git_repository()?;
14023        let canonical_a = worktree_a.canonicalize()?;
14024        let entry_a = repository
14025            .worktrees
14026            .iter()
14027            .find(|entry| {
14028                ProjectAtlasMcpServer::active_worktree_root(entry) == Some(canonical_a.as_path())
14029            })
14030            .ok_or_else(|| io::Error::other("worktree A was not structurally discovered"))?;
14031        let selector_a = ProjectAtlasMcpServer::worktree_candidate_selector(entry_a);
14032        let canonical_b = worktree_b.canonicalize()?;
14033        let entry_b = repository
14034            .worktrees
14035            .iter()
14036            .find(|entry| {
14037                ProjectAtlasMcpServer::active_worktree_root(entry) == Some(canonical_b.as_path())
14038            })
14039            .ok_or_else(|| io::Error::other("worktree B was not structurally discovered"))?;
14040        let selector_b = ProjectAtlasMcpServer::worktree_candidate_selector(entry_b);
14041
14042        let listed = server.atlas_worktree_list(Parameters(AtlasWorktreeListParams {
14043            include_retired: Some(false),
14044        }));
14045        require(
14046            listed.contains("control_alias: main")
14047                && listed.contains(&selector_a)
14048                && listed.contains(&normalize_native_path_display(&canonical_a)),
14049            "worktree list omitted control, stable selector, or arbitrary exact root",
14050        )?;
14051        let ambiguous = server.atlas_worktree_add(Parameters(AtlasWorktreeAddParams {
14052            worktree: "checkout".to_string(),
14053            alias: Some("issue-430".to_string()),
14054        }));
14055        require(
14056            ambiguous.contains("status: ambiguous")
14057                && ambiguous.matches(MCP_WORKTREE_SELECTOR_PREFIX).count() >= 2,
14058            "ambiguous human selector guessed or omitted bounded stable candidates",
14059        )?;
14060        let control_before_blank_alias = fs::read(&control_db)?;
14061        let blank_alias = server.atlas_worktree_add(Parameters(AtlasWorktreeAddParams {
14062            worktree: selector_a.clone(),
14063            alias: Some("   ".to_string()),
14064        }));
14065        require(
14066            blank_alias.contains("alias is empty"),
14067            "blank explicit alias fell back to the selected directory name",
14068        )?;
14069        require(
14070            fs::read(&control_db)? == control_before_blank_alias,
14071            "blank explicit alias changed the control database",
14072        )?;
14073
14074        let target_b_config = worktree_b.join(PROJECTATLAS_DIR_NAME).join("config.toml");
14075        init_project_with_config(&worktree_b, Some(&target_b_config))?;
14076        let target_b_db = worktree_b
14077            .join(PROJECTATLAS_DIR_NAME)
14078            .join(PROJECTATLAS_DB_FILE_NAME);
14079        let target_b_store = AtlasStore::open_for_project(&target_b_db, &worktree_b)?;
14080        target_b_store.record_usage(&usage_from_text(
14081            "snapshot-blocked",
14082            "atlas_overview",
14083            None,
14084            None,
14085            "pub fn main_only() {}",
14086            "repository overview",
14087        ))?;
14088        let target_b_project = target_b_store
14089            .project_instance_id()?
14090            .ok_or_else(|| io::Error::other("worktree B project identity is missing"))?;
14091        drop(target_b_store);
14092        let corrupt = rusqlite::Connection::open(&target_b_db)?;
14093        corrupt.execute_batch("PRAGMA ignore_check_constraints = ON;")?;
14094        let corrupted = corrupt.execute("UPDATE usage_global_aggregates SET calls = -1", [])?;
14095        require(
14096            corrupted > 0,
14097            "worktree B fixture did not invalidate an aggregate row",
14098        )?;
14099        drop(corrupt);
14100        let rejected_snapshot = server.atlas_worktree_add(Parameters(AtlasWorktreeAddParams {
14101            worktree: selector_b.clone(),
14102            alias: Some("snapshot-blocked".to_string()),
14103        }));
14104        require(
14105            rejected_snapshot.contains("telemetry integer overflow")
14106                && !rejected_snapshot.contains("status: registered"),
14107            &format!(
14108                "worktree registration survived a failed local usage snapshot: {rejected_snapshot}"
14109            ),
14110        )?;
14111        let control_after_rejection =
14112            open_atlas_store_read_only_for_project(&control_db, &primary)?;
14113        require(
14114            matches!(
14115                control_after_rejection
14116                    .worktree_registration(&WorktreeAlias::parse("snapshot-blocked")?),
14117                Err(DbError::WorktreeRegistrationNotFound { .. })
14118            ),
14119            "telemetry export failure committed a partial worktree registration",
14120        )?;
14121        drop(control_after_rejection);
14122        let repaired = rusqlite::Connection::open(&target_b_db)?;
14123        repaired.execute("UPDATE usage_global_aggregates SET calls = 1", [])?;
14124        drop(repaired);
14125        let registered_snapshot = server.atlas_worktree_add(Parameters(AtlasWorktreeAddParams {
14126            worktree: selector_b.clone(),
14127            alias: Some("snapshot-blocked".to_string()),
14128        }));
14129        require(
14130            registered_snapshot.contains("status: registered"),
14131            &format!(
14132                "repaired local usage snapshot did not register atomically: {registered_snapshot}"
14133            ),
14134        )?;
14135        let control_after_snapshot = open_atlas_store_read_only_for_project(&control_db, &primary)?;
14136        require(
14137            control_after_snapshot
14138                .worktree_registration(&WorktreeAlias::parse("snapshot-blocked")?)?
14139                .project_instance_id
14140                == Some(target_b_project)
14141                && control_after_snapshot
14142                    .registered_worktree_token_overview(&WorktreeAlias::parse("snapshot-blocked")?)?
14143                    .calls
14144                    == 1,
14145            "successful registration did not bind identity and import telemetry atomically",
14146        )?;
14147        drop(control_after_snapshot);
14148        let target_b_state = worktree_b.join(PROJECTATLAS_DIR_NAME);
14149        let preserved_target_b_state = worktree_b.join(".projectatlas-snapshot-blocked");
14150        fs::rename(&target_b_state, &preserved_target_b_state)?;
14151        fs::create_dir(&target_b_state)?;
14152        let replacement_b = AtlasStore::open_for_project(&target_b_db, &worktree_b)?;
14153        require(
14154            replacement_b.project_instance_id()? != Some(target_b_project),
14155            "worktree B replacement reused the registered project identity",
14156        )?;
14157        drop(replacement_b);
14158        let replacement_b_error =
14159            server.state_for_target(None, Some("snapshot-blocked".to_string()));
14160        require(
14161            replacement_b_error.as_ref().is_err_and(|error| {
14162                error
14163                    .to_string()
14164                    .contains(MCP_ERROR_WORKTREE_IDENTITY_CONFLICT)
14165            }),
14166            "snapshot-backed registration routed to a replacement atlas",
14167        )?;
14168        fs::remove_dir_all(&target_b_state)?;
14169        let refused_snapshot =
14170            server.atlas_worktree_remove(Parameters(AtlasWorktreeRemoveParams {
14171                worktree: "snapshot-blocked".to_string(),
14172            }));
14173        require(
14174            refused_snapshot.contains(MCP_ERROR_BOUND_WORKTREE_ATLAS_MISSING),
14175            &format!(
14176                "bound registration retired without its required final snapshot: {refused_snapshot}"
14177            ),
14178        )?;
14179        let control_after_refusal = open_atlas_store_read_only_for_project(&control_db, &primary)?;
14180        require(
14181            control_after_refusal
14182                .worktree_registration(&WorktreeAlias::parse("snapshot-blocked")?)?
14183                .state
14184                == WorktreeRegistrationState::Active,
14185            "missing bound atlas retirement changed the active registration",
14186        )?;
14187        drop(control_after_refusal);
14188        fs::rename(&preserved_target_b_state, &target_b_state)?;
14189        let retired_snapshot =
14190            server.atlas_worktree_remove(Parameters(AtlasWorktreeRemoveParams {
14191                worktree: "snapshot-blocked".to_string(),
14192            }));
14193        require(
14194            retired_snapshot.contains("status: retired"),
14195            &format!(
14196                "restored bound atlas did not permit final-sync retirement: {retired_snapshot}"
14197            ),
14198        )?;
14199        fs::remove_dir_all(&target_b_state)?;
14200
14201        let legacy_added = server.atlas_worktree_add(Parameters(AtlasWorktreeAddParams {
14202            worktree: selector_b.clone(),
14203            alias: Some("legacy-init".to_string()),
14204        }));
14205        require(
14206            legacy_added.contains("status: registered"),
14207            &format!("legacy-init fixture registration failed: {legacy_added}"),
14208        )?;
14209        init_project_with_config(&worktree_b, Some(&target_b_config))?;
14210        let control_before_legacy_sync =
14211            open_atlas_store_read_only_for_project(&control_db, &primary)?;
14212        require(
14213            control_before_legacy_sync
14214                .worktree_registration(&WorktreeAlias::parse("legacy-init")?)?
14215                .project_instance_id
14216                .is_none(),
14217            "independent exact-path init unexpectedly mutated the control registration",
14218        )?;
14219        drop(control_before_legacy_sync);
14220        let legacy_target = AtlasStore::open_for_project(&target_b_db, &worktree_b)?;
14221        legacy_target.record_usage(&usage_from_text(
14222            "legacy-init",
14223            "atlas_overview",
14224            None,
14225            None,
14226            "pub fn independent() {}",
14227            "repository overview",
14228        ))?;
14229        let legacy_project = legacy_target
14230            .project_instance_id()?
14231            .ok_or_else(|| io::Error::other("legacy-init target identity is missing"))?;
14232        drop(legacy_target);
14233        let legacy_state = server.state_for_target(None, Some("legacy-init".to_string()))?;
14234        let legacy_selection = legacy_state
14235            .worktree
14236            .as_ref()
14237            .ok_or_else(|| io::Error::other("legacy-init selection is missing"))?;
14238        let corrupt = rusqlite::Connection::open(&target_b_db)?;
14239        corrupt.execute_batch("PRAGMA ignore_check_constraints = ON;")?;
14240        corrupt.execute("UPDATE usage_global_aggregates SET calls = -1", [])?;
14241        drop(corrupt);
14242        let rejected_bind = ProjectAtlasMcpServer::open_registered_worktree_mut_store(
14243            &legacy_state,
14244            &server.control_state,
14245            legacy_selection,
14246        );
14247        let control_after_rejected_bind =
14248            open_atlas_store_read_only_for_project(&control_db, &primary)?;
14249        require(
14250            rejected_bind
14251                .as_ref()
14252                .is_err_and(|error| error.to_string().contains("telemetry integer overflow"))
14253                && control_after_rejected_bind
14254                    .worktree_registration(&WorktreeAlias::parse("legacy-init")?)?
14255                    .project_instance_id
14256                    .is_none()
14257                && control_after_rejected_bind
14258                    .registered_worktree_token_overview(&WorktreeAlias::parse("legacy-init")?)?
14259                    .calls
14260                    == 0,
14261            "failed deferred snapshot synchronization committed its project binding or aggregate",
14262        )?;
14263        drop(control_after_rejected_bind);
14264        let repaired = rusqlite::Connection::open(&target_b_db)?;
14265        repaired.execute("UPDATE usage_global_aggregates SET calls = 1", [])?;
14266        drop(repaired);
14267        drop(ProjectAtlasMcpServer::open_registered_worktree_mut_store(
14268            &legacy_state,
14269            &server.control_state,
14270            legacy_selection,
14271        )?);
14272        let control_after_legacy_init =
14273            open_atlas_store_read_only_for_project(&control_db, &primary)?;
14274        require(
14275            control_after_legacy_init
14276                .worktree_registration(&WorktreeAlias::parse("legacy-init")?)?
14277                .project_instance_id
14278                == Some(legacy_project),
14279            "first alias mutation did not bind the independently initialized alias",
14280        )?;
14281        require(
14282            control_after_legacy_init
14283                .registered_worktree_token_overview(&WorktreeAlias::parse("legacy-init")?)?
14284                .calls
14285                == 1,
14286            "first alias mutation omitted independently initialized worktree usage",
14287        )?;
14288        drop(control_after_legacy_init);
14289        let preserved_legacy_state = worktree_b.join(".projectatlas-legacy-init");
14290        fs::rename(&target_b_state, &preserved_legacy_state)?;
14291        fs::create_dir(&target_b_state)?;
14292        let legacy_replacement = AtlasStore::open_for_project(&target_b_db, &worktree_b)?;
14293        require(
14294            legacy_replacement.project_instance_id()? != Some(legacy_project),
14295            "legacy-init replacement reused the registered project identity",
14296        )?;
14297        drop(legacy_replacement);
14298        let legacy_replacement_error =
14299            server.state_for_target(None, Some("legacy-init".to_string()));
14300        require(
14301            legacy_replacement_error.as_ref().is_err_and(|error| {
14302                error
14303                    .to_string()
14304                    .contains(MCP_ERROR_WORKTREE_IDENTITY_CONFLICT)
14305            }),
14306            "legacy exact-path init left its alias unbound to a replacement atlas",
14307        )?;
14308        fs::remove_dir_all(&target_b_state)?;
14309        let refused_legacy = server.atlas_worktree_remove(Parameters(AtlasWorktreeRemoveParams {
14310            worktree: "legacy-init".to_string(),
14311        }));
14312        require(
14313            refused_legacy.contains(MCP_ERROR_BOUND_WORKTREE_ATLAS_MISSING),
14314            &format!("legacy-init registration retired without its atlas: {refused_legacy}"),
14315        )?;
14316        fs::rename(&preserved_legacy_state, &target_b_state)?;
14317        let retired_legacy = server.atlas_worktree_remove(Parameters(AtlasWorktreeRemoveParams {
14318            worktree: "legacy-init".to_string(),
14319        }));
14320        require(
14321            retired_legacy.contains("status: retired"),
14322            &format!("restored legacy-init registration could not be retired: {retired_legacy}"),
14323        )?;
14324        fs::remove_dir_all(&target_b_state)?;
14325
14326        init_project_with_config(&worktree_b, Some(&target_b_config))?;
14327        let migratable_store = AtlasStore::open_for_project(&target_b_db, &worktree_b)?;
14328        let migratable_project = migratable_store
14329            .project_instance_id()?
14330            .ok_or_else(|| io::Error::other("migratable target identity is missing"))?;
14331        drop(migratable_store);
14332        let incomplete_current = rusqlite::Connection::open(&target_b_db)?;
14333        incomplete_current.execute(
14334            "UPDATE metadata SET value = ?1 WHERE key = 'project_root'",
14335            [worktree_b.join(".").to_string_lossy().into_owned()],
14336        )?;
14337        // Exercise the cross-platform typed predecessor migration path. The
14338        // schema-19 table-absent route is supported only where its legacy
14339        // display is lossless (Windows); Unix must reject that non-injective
14340        // predecessor before any write. Schema 20 retains the typed singleton
14341        // and is the common predecessor for this alias-routed lifecycle test.
14342        incomplete_current.execute_batch(
14343            "DROP INDEX IF EXISTS idx_worktree_registrations_active_native_administrative_directory;
14344             DROP INDEX IF EXISTS idx_worktree_registrations_active_native_root;
14345             ALTER TABLE worktree_registrations DROP COLUMN git_common_directory_identity;
14346             ALTER TABLE worktree_registrations DROP COLUMN git_administrative_directory_identity;
14347             ALTER TABLE worktree_registrations DROP COLUMN last_root_identity;
14348             DROP TABLE graph_identity_rejections;
14349             UPDATE metadata SET value = '20' WHERE key = 'schema_version';",
14350        )?;
14351        drop(incomplete_current);
14352        let migratable_added = server.atlas_worktree_add(Parameters(AtlasWorktreeAddParams {
14353            worktree: selector_b,
14354            alias: Some("migratable-atlas".to_string()),
14355        }));
14356        require(
14357            migratable_added.contains("status: registered")
14358                && migratable_added
14359                    .contains("registration committed without local telemetry import"),
14360            &format!(
14361                "current repair atlas did not retain its explicit unbound registration: {migratable_added}"
14362            ),
14363        )?;
14364        let control_before_migration =
14365            open_atlas_store_read_only_for_project(&control_db, &primary)?;
14366        require(
14367            control_before_migration
14368                .worktree_registration(&WorktreeAlias::parse("migratable-atlas")?)?
14369                .project_instance_id
14370                .is_none(),
14371            "current repair fixture unexpectedly bound before scan",
14372        )?;
14373        drop(control_before_migration);
14374        let migrated = server.atlas_scan(Parameters(AtlasScanParams {
14375            project_path: None,
14376            worktree: Some("migratable-atlas".to_string()),
14377            path: None,
14378            nearest_project: Some(false),
14379            max_bytes: None,
14380            max_workers: Some(1),
14381            timeout_seconds: None,
14382            text_index_max_bytes: None,
14383            background: Some(false),
14384        }));
14385        require(
14386            migrated.contains("scan:"),
14387            &format!("alias-routed current repair did not complete its scan: {migrated}"),
14388        )?;
14389        let migrated_target = open_atlas_store_read_only_for_project(&target_b_db, &worktree_b)?;
14390        require(
14391            migrated_target.project_instance_id()? == Some(migratable_project),
14392            "current repair changed the worktree project identity",
14393        )?;
14394        drop(migrated_target);
14395        let control_after_migration =
14396            open_atlas_store_read_only_for_project(&control_db, &primary)?;
14397        require(
14398            control_after_migration
14399                .worktree_registration(&WorktreeAlias::parse("migratable-atlas")?)?
14400                .project_instance_id
14401                == Some(migratable_project),
14402            "alias-routed current repair left its registration unbound",
14403        )?;
14404        drop(control_after_migration);
14405        let retired_migratable =
14406            server.atlas_worktree_remove(Parameters(AtlasWorktreeRemoveParams {
14407                worktree: "migratable-atlas".to_string(),
14408            }));
14409        require(
14410            retired_migratable.contains("status: retired"),
14411            &format!("current-repair registration could not be retired: {retired_migratable}"),
14412        )?;
14413
14414        let added = server.atlas_worktree_add(Parameters(AtlasWorktreeAddParams {
14415            worktree: selector_a,
14416            alias: Some("issue-430".to_string()),
14417        }));
14418        require(
14419            added.contains("status: registered")
14420                && added.contains("alias: \"issue-430\"")
14421                && added.contains("git_unchanged: true")
14422                && added.contains("files_unchanged: true"),
14423            &format!("stable selector did not create one lifecycle-neutral registration: {added}"),
14424        )?;
14425        require(
14426            !worktree_a.join(".projectatlas").exists(),
14427            "registration created a target atlas before explicit init",
14428        )?;
14429
14430        let missing = server.atlas_overview_response(
14431            AtlasProjectParams {
14432                project_path: None,
14433                worktree: Some("issue-430".to_string()),
14434            },
14435            None,
14436        );
14437        require(
14438            missing.contains("init_required")
14439                && missing.contains("tool: atlas_init")
14440                && missing.contains("worktree: \"issue-430\"")
14441                && !missing.contains("project_path:"),
14442            "missing alias target did not preserve its short selector in typed init guidance",
14443        )?;
14444        let conflict = server.atlas_overview_response(
14445            AtlasProjectParams {
14446                project_path: Some(primary.to_string_lossy().to_string()),
14447                worktree: Some("issue-430".to_string()),
14448            },
14449            None,
14450        );
14451        require(
14452            conflict.contains(MCP_WORKTREE_PROJECT_PATH_CONFLICT),
14453            "worktree/project_path conflict was not rejected by the shared resolver",
14454        )?;
14455
14456        let initialized = server.atlas_init(Parameters(AtlasInitParams {
14457            project_path: None,
14458            worktree: Some("issue-430".to_string()),
14459            no_scan: Some(false),
14460            force_rescan: Some(false),
14461            text_index_max_bytes: None,
14462        }));
14463        require(
14464            initialized.contains("status: hydrated")
14465                && initialized.contains("source_project_instance_id:")
14466                && initialized.contains("target_project_instance_id:")
14467                && initialized.contains("reconciled_generation:")
14468                && initialized.contains("parsed: 1"),
14469            &format!("registered alias init did not expose one completed hydration: {initialized}"),
14470        )?;
14471        let target_db = worktree_a.join(".projectatlas").join("projectatlas.db");
14472        let target_store = open_atlas_store_read_only_for_project(&target_db, &worktree_a)?;
14473        require(
14474            target_store.load_node_by_path("src/branch.rs")?.is_some(),
14475            "hydration reconciliation omitted a target-only dirty source file",
14476        )?;
14477        require(
14478            target_store
14479                .load_node_by_path("src/lib.rs")?
14480                .is_some_and(|node| {
14481                    node.purpose.purpose.as_deref() == Some("Own the shared library contract.")
14482                        && node.purpose.source == PurposeSource::Agent
14483                }),
14484            "hydration did not preserve an applicable approved main purpose",
14485        )?;
14486        let target_project = target_store
14487            .project_instance_id()?
14488            .ok_or_else(|| io::Error::other("hydrated target identity is missing"))?;
14489        drop(target_store);
14490        let captured_alias_state = server.state_for_target(None, Some("issue-430".to_string()))?;
14491        let captured_federated_aliases = ["main".to_string(), "issue-430".to_string()];
14492        let (captured_federated_roots, captured_federated_selections) =
14493            server.federated_worktree_roots(&captured_federated_aliases)?;
14494        let captured_main_state = server.state_for_target(None, Some("main".to_string()))?;
14495        let control_after_init = open_atlas_store_read_only_for_project(&control_db, &primary)?;
14496        let control_project = control_after_init
14497            .project_instance_id()?
14498            .ok_or_else(|| io::Error::other("control project identity is missing"))?;
14499        require(
14500            captured_main_state
14501                .worktree
14502                .as_ref()
14503                .and_then(|selection| selection.project_instance_id)
14504                == Some(control_project),
14505            "main alias did not capture the current control atlas identity",
14506        )?;
14507        require(
14508            [&captured_main_state, &captured_alias_state]
14509                .iter()
14510                .all(|state| {
14511                    state
14512                        .worktree
14513                        .as_ref()
14514                        .and_then(|selection| selection.control_project_instance_id)
14515                        == Some(control_project)
14516                }),
14517            "alias selection did not capture its control atlas identity",
14518        )?;
14519        require(
14520            control_after_init
14521                .load_node_by_path("src/branch.rs")?
14522                .is_none(),
14523            "target-only source state bled into the control graph",
14524        )?;
14525        require(
14526            control_after_init
14527                .worktree_registration(&WorktreeAlias::parse("issue-430")?)?
14528                .project_instance_id
14529                == Some(target_project),
14530            "successful alias init did not bind the exact target atlas identity",
14531        )?;
14532        drop(control_after_init);
14533        let target_before_reset = fs::read(&target_db)?;
14534        let bound_reset = server.atlas_reset_index(Parameters(AtlasResetIndexParams {
14535            project_path: None,
14536            worktree: Some("issue-430".to_string()),
14537            apply: Some(true),
14538            dry_run: Some(false),
14539            include_mcp_config: Some(true),
14540        }));
14541        require(
14542            bound_reset.contains(MCP_ERROR_BOUND_WORKTREE_RESET_UNSUPPORTED),
14543            "applied reset did not reject a bound worktree alias",
14544        )?;
14545        require(
14546            fs::read(&target_db)? == target_before_reset,
14547            "rejected bound worktree reset changed its database",
14548        )?;
14549        let control_state = primary.join(PROJECTATLAS_DIR_NAME);
14550        let preserved_control_state = primary.join(".projectatlas-captured-main");
14551        fs::rename(&control_state, &preserved_control_state)?;
14552        fs::create_dir(&control_state)?;
14553        init_project_with_config(&primary, captured_main_state.config_path.as_deref())?;
14554        let mut replacement_control = AtlasStore::open_for_project(&control_db, &primary)?;
14555        let replacement_control_plan =
14556            ScanRuntimePlan::for_path(captured_main_state.config_path.as_deref(), &primary, None)?;
14557        run_scan_pipeline(
14558            &mut replacement_control,
14559            &replacement_control_plan,
14560            &SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, None, None),
14561        )?;
14562        require(
14563            replacement_control.project_instance_id()? != Some(control_project),
14564            "replacement control atlas reused the captured main identity",
14565        )?;
14566        let captured_registration_id = captured_alias_state
14567            .worktree
14568            .as_ref()
14569            .and_then(|selection| selection.registration_id)
14570            .ok_or_else(|| io::Error::other("captured alias registration identity is missing"))?;
14571        let common = primary.join(".git");
14572        let mut replacement_alias = None;
14573        for index in 1..=captured_registration_id {
14574            let alias = WorktreeAlias::parse(&format!("replacement-{index}"))?;
14575            let registration = replacement_control.register_worktree(
14576                &alias,
14577                &common,
14578                &common
14579                    .join("worktrees")
14580                    .join(format!("replacement-{index}")),
14581                &format!("{index:064x}"),
14582                &temp.path().join(format!("replacement-worktree-{index}")),
14583                Some(ProjectInstanceId::from_bytes([u8::try_from(index)?; 16])?),
14584                u64::try_from(index)?,
14585            )?;
14586            require(
14587                registration.registration_id == index,
14588                "replacement control did not reuse the expected registration identity",
14589            )?;
14590            if index == captured_registration_id {
14591                replacement_alias = Some(alias);
14592            }
14593        }
14594        let replacement_alias = replacement_alias
14595            .ok_or_else(|| io::Error::other("replacement alias was not created"))?;
14596        let replacement_calls_before = replacement_control
14597            .registered_worktree_token_overview(&replacement_alias)?
14598            .calls;
14599        require(
14600            ProjectAtlasMcpServer::require_captured_control_identity(
14601                captured_alias_state.worktree.as_ref(),
14602                &replacement_control,
14603            )
14604            .as_ref()
14605            .is_err_and(|error| {
14606                error
14607                    .to_string()
14608                    .contains(MCP_ERROR_WORKTREE_CONTROL_IDENTITY_CONFLICT)
14609            }),
14610            "captured alias accepted a replacement control atlas",
14611        )?;
14612        drop(replacement_control);
14613        let accepted = server.with_fresh_string_and_usage_for_request(
14614            &captured_alias_state,
14615            None,
14616            |_store, _stamp| {
14617                Ok((
14618                    "accepted result".to_string(),
14619                    Some(McpUsageIntent::estimate(
14620                        MCP_EVENT_ATLAS_OVERVIEW,
14621                        None,
14622                        None,
14623                        1,
14624                    )),
14625                ))
14626            },
14627        )?;
14628        require(
14629            accepted == "accepted result",
14630            "accepted target read was lost",
14631        )?;
14632        let replacement_control = open_atlas_store_read_only_for_project(&control_db, &primary)?;
14633        require(
14634            replacement_control
14635                .registered_worktree_token_overview(&replacement_alias)?
14636                .calls
14637                == replacement_calls_before,
14638            "deferred telemetry was attributed through a replacement control catalog",
14639        )?;
14640        drop(replacement_control);
14641        require(
14642            matches!(
14643                synchronize_registered_worktree_usage(
14644                    &captured_main_state.db_path,
14645                    &captured_main_state.root,
14646                    Some(control_project),
14647                ),
14648                Err(CliError::InvalidInput(message))
14649                    if message.contains("control atlas identity changed")
14650            ),
14651            "main token synchronization did not revalidate its captured project identity",
14652        )?;
14653        let captured_main_direct_read =
14654            ProjectAtlasMcpServer::open_read_store(&captured_main_state);
14655        require(
14656            captured_main_direct_read.as_ref().is_err_and(|error| {
14657                error
14658                    .to_string()
14659                    .contains(MCP_ERROR_WORKTREE_IDENTITY_CONFLICT)
14660            }),
14661            "main direct token read did not revalidate its captured project identity",
14662        )?;
14663        let captured_main_read =
14664            server.with_fresh_store(&captured_main_state, |_store, _stamp| Ok(()));
14665        require(
14666            captured_main_read.as_ref().is_err_and(|error| {
14667                error
14668                    .to_string()
14669                    .contains(MCP_ERROR_WORKTREE_IDENTITY_CONFLICT)
14670            }),
14671            "main read snapshot did not revalidate its captured project identity",
14672        )?;
14673        let captured_main_write = ProjectAtlasMcpServer::open_existing_mut_store(
14674            &captured_main_state,
14675            &server.control_state,
14676        );
14677        require(
14678            captured_main_write.as_ref().is_err_and(|error| {
14679                error
14680                    .to_string()
14681                    .contains(MCP_ERROR_WORKTREE_IDENTITY_CONFLICT)
14682            }),
14683            "main mutation did not revalidate its captured project identity",
14684        )?;
14685        let captured_federated_labels = captured_federated_selections
14686            .iter()
14687            .map(|selection| selection.alias.clone())
14688            .collect::<Vec<_>>();
14689        let federated_control =
14690            index_work_control(&SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, None, None));
14691        let captured_federated_stores = open_federated_atlas_stores_for_project(
14692            &captured_main_state.db_path,
14693            &captured_main_state.root,
14694            captured_main_state.config_path.as_deref(),
14695            &captured_federated_roots,
14696            Some(&captured_federated_labels),
14697            &federated_control,
14698        )?;
14699        let captured_main_federation = ProjectAtlasMcpServer::require_federated_worktree_identities(
14700            captured_federated_stores,
14701            &captured_federated_selections,
14702        );
14703        require(
14704            captured_main_federation.as_ref().is_err_and(|error| {
14705                error
14706                    .to_string()
14707                    .contains(MCP_ERROR_WORKTREE_IDENTITY_CONFLICT)
14708                    && error.to_string().contains(MCP_MAIN_WORKTREE_ALIAS)
14709            }),
14710            "federation did not revalidate the captured main identity",
14711        )?;
14712        fs::remove_dir_all(&control_state)?;
14713        fs::rename(&preserved_control_state, &control_state)?;
14714        let target_state = worktree_a.join(PROJECTATLAS_DIR_NAME);
14715        let preserved_target_state = worktree_a.join(".projectatlas-registered");
14716        fs::rename(&target_state, &preserved_target_state)?;
14717        fs::create_dir(&target_state)?;
14718        init_project_with_config(&worktree_a, captured_alias_state.config_path.as_deref())?;
14719        let mut replacement_store = AtlasStore::open_for_project(&target_db, &worktree_a)?;
14720        let replacement_plan = ScanRuntimePlan::for_path(
14721            captured_alias_state.config_path.as_deref(),
14722            &worktree_a,
14723            None,
14724        )?;
14725        run_scan_pipeline(
14726            &mut replacement_store,
14727            &replacement_plan,
14728            &SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, None, None),
14729        )?;
14730        let replacement_project = replacement_store
14731            .project_instance_id()?
14732            .ok_or_else(|| io::Error::other("replacement target identity is missing"))?;
14733        require(
14734            replacement_project != target_project,
14735            "replacement atlas reused the registered project identity",
14736        )?;
14737        drop(replacement_store);
14738        let captured_read = server.with_fresh_store(&captured_alias_state, |_store, _stamp| Ok(()));
14739        require(
14740            captured_read.as_ref().is_err_and(|error| {
14741                error
14742                    .to_string()
14743                    .contains(MCP_ERROR_WORKTREE_IDENTITY_CONFLICT)
14744            }),
14745            "alias read snapshot did not revalidate its captured project identity",
14746        )?;
14747        let captured_federated_labels = captured_federated_selections
14748            .iter()
14749            .map(|selection| selection.alias.clone())
14750            .collect::<Vec<_>>();
14751        let federated_control =
14752            index_work_control(&SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, None, None));
14753        let captured_federated_stores = open_federated_atlas_stores_for_project(
14754            &captured_main_state.db_path,
14755            &captured_main_state.root,
14756            captured_main_state.config_path.as_deref(),
14757            &captured_federated_roots,
14758            Some(&captured_federated_labels),
14759            &federated_control,
14760        )?;
14761        let captured_federation = ProjectAtlasMcpServer::require_federated_worktree_identities(
14762            captured_federated_stores,
14763            &captured_federated_selections,
14764        );
14765        require(
14766            captured_federation.as_ref().is_err_and(|error| {
14767                error
14768                    .to_string()
14769                    .contains(MCP_ERROR_WORKTREE_IDENTITY_CONFLICT)
14770                    && error.to_string().contains("issue-430")
14771            }),
14772            "federated snapshots did not retain their captured alias identities",
14773        )?;
14774        let replacement_error = server.state_for_target(None, Some("issue-430".to_string()));
14775        require(
14776            replacement_error.as_ref().is_err_and(|error| {
14777                error
14778                    .to_string()
14779                    .contains(MCP_ERROR_WORKTREE_IDENTITY_CONFLICT)
14780            }),
14781            "alias routing accepted a replacement atlas at the registered root",
14782        )?;
14783        fs::remove_dir_all(&target_state)?;
14784        fs::rename(&preserved_target_state, &target_state)?;
14785        let selected = server.state_for_target(None, Some("issue-430".to_string()))?;
14786        require(
14787            selected.root == canonical_a
14788                && selected
14789                    .worktree
14790                    .as_ref()
14791                    .is_some_and(|selection| selection.alias == "issue-430"),
14792            "short alias did not capture the exact target root and identity",
14793        )?;
14794        let selected_diagnostics = ProjectAtlasMcpServer::render_project_state(&selected)?;
14795        require(
14796            selected_diagnostics.contains("worktree: \"issue-430\"")
14797                && selected_diagnostics.contains("registration_id:"),
14798            &format!(
14799                "selected-project diagnostics omitted the captured alias or registration identity: {selected_diagnostics}"
14800            ),
14801        )?;
14802        let settings = server.atlas_settings(Parameters(AtlasProjectParams {
14803            project_path: None,
14804            worktree: Some("issue-430".to_string()),
14805        }));
14806        require(
14807            settings.contains("worktree: \"issue-430\"") && settings.contains("registration_id:"),
14808            &format!(
14809                "alias-routed settings omitted the captured alias or registration identity: {settings}"
14810            ),
14811        )?;
14812        let root_report = server.atlas_root(Parameters(AtlasRootParams {
14813            project_path: None,
14814            worktree: Some("issue-430".to_string()),
14815            verify: Some(false),
14816            control_root: None,
14817        }));
14818        require(
14819            root_report.contains("worktree: \"issue-430\"")
14820                && root_report.contains("registration_id:"),
14821            &format!(
14822                "alias-routed root diagnostics omitted the captured alias or registration identity: {root_report}"
14823            ),
14824        )?;
14825        let routed_overview = server.atlas_overview_response(
14826            AtlasProjectParams {
14827                project_path: None,
14828                worktree: Some("issue-430".to_string()),
14829            },
14830            None,
14831        );
14832        require(
14833            routed_overview.contains("overview:") && routed_overview.contains("files:"),
14834            &format!("alias-routed overview failed: {routed_overview}"),
14835        )?;
14836        let alias = WorktreeAlias::parse("issue-430")?;
14837        let control_after_routed = open_atlas_store_read_only_for_project(&control_db, &primary)?;
14838        require(
14839            control_after_routed
14840                .registered_worktree_token_overview(&alias)?
14841                .calls
14842                == 1
14843                && control_after_routed.repository_token_overview()?.calls == 3,
14844            "alias-routed MCP usage or retained independently initialized usage was miscounted",
14845        )?;
14846        drop(control_after_routed);
14847        let local_event = usage_from_text(
14848            "worktree-local",
14849            "atlas_summary",
14850            Some("src/lib.rs".to_string()),
14851            None,
14852            "pub fn main_only() { child::helper(); }",
14853            "Own the shared library contract.",
14854        );
14855        let local_target = AtlasStore::open_for_project(&target_db, &worktree_a)?;
14856        local_target.record_usage(&local_event)?;
14857        require(
14858            local_target.token_overview(None)?.calls == 1,
14859            "independent worktree usage was not retained in its exact local atlas",
14860        )?;
14861        drop(local_target);
14862        let repository_tokens = server.atlas_token_report(Parameters(AtlasTokenParams {
14863            project_path: None,
14864            worktree: Some("main".to_string()),
14865            session: None,
14866            include_chart: Some(false),
14867            trend_window: None,
14868            benchmark_results: None,
14869            theme: None,
14870        }));
14871        require(
14872            repository_tokens.contains("worktree: main") && repository_tokens.contains("calls: 4"),
14873            &format!(
14874                "control token report did not combine routed and synchronized worktree usage: {repository_tokens}"
14875            ),
14876        )?;
14877        let worktree_tokens = server.atlas_token_report(Parameters(AtlasTokenParams {
14878            project_path: None,
14879            worktree: Some("issue-430".to_string()),
14880            session: None,
14881            include_chart: Some(false),
14882            trend_window: None,
14883            benchmark_results: None,
14884            theme: None,
14885        }));
14886        require(
14887            worktree_tokens.contains("worktree: \"issue-430\"")
14888                && worktree_tokens.contains("calls: 1"),
14889            &format!(
14890                "exact worktree token report included routed or sibling usage: {worktree_tokens}"
14891            ),
14892        )?;
14893        let federated = server.atlas_symbol_relations_response(
14894            &AtlasSymbolRelationsParams {
14895                file: Some("src/lib.rs".to_string()),
14896                view: Some(MCP_SYMBOL_RELATION_VIEW_DETAILED.to_string()),
14897                compact: Some(true),
14898                worktrees: Some(vec!["main".to_string(), "issue-430".to_string()]),
14899                limit: Some(1),
14900                ..AtlasSymbolRelationsParams::default()
14901            },
14902            None,
14903        );
14904        require(
14905            federated.contains("primary_worktree: main")
14906                && federated.contains("participants[2]{order,worktree")
14907                && federated.contains("0,main,")
14908                && federated.contains("1,\"issue-430\","),
14909            &format!(
14910                "alias federation did not label the primary and every participant: {federated}"
14911            ),
14912        )?;
14913        let federation_conflict = server.atlas_symbol_relations_response(
14914            &AtlasSymbolRelationsParams {
14915                file: Some("src/lib.rs".to_string()),
14916                view: Some(MCP_SYMBOL_RELATION_VIEW_DETAILED.to_string()),
14917                roots: Some(vec![
14918                    normalize_native_path_display(&primary),
14919                    normalize_native_path_display(&worktree_a),
14920                ]),
14921                worktrees: Some(vec!["main".to_string(), "issue-430".to_string()]),
14922                ..AtlasSymbolRelationsParams::default()
14923            },
14924            None,
14925        );
14926        require(
14927            federation_conflict.contains(MCP_ERROR_FEDERATED_SELECTOR_CONFLICT),
14928            "alias federation did not reject legacy roots before opening participants",
14929        )?;
14930        let target_database_before_blocker = fs::read(&target_db)?;
14931        fs::write(
14932            worktree_a.join("src").join("branch.rs"),
14933            "pub fn worktree_only_changed() {}\n",
14934        )?;
14935        let blocked_federation = server.atlas_symbol_relations_response(
14936            &AtlasSymbolRelationsParams {
14937                file: Some("src/lib.rs".to_string()),
14938                view: Some(MCP_SYMBOL_RELATION_VIEW_DETAILED.to_string()),
14939                worktrees: Some(vec!["main".to_string(), "issue-430".to_string()]),
14940                ..AtlasSymbolRelationsParams::default()
14941            },
14942            None,
14943        );
14944        require(
14945            blocked_federation.contains("refresh_required")
14946                && blocked_federation.contains("worktree: \"issue-430\""),
14947            &format!(
14948                "stale federated participant blocker omitted its exact worktree alias: {blocked_federation}"
14949            ),
14950        )?;
14951        require(
14952            fs::read(&target_db)? == target_database_before_blocker,
14953            "read-only alias federation repaired or changed a sibling database",
14954        )?;
14955        let main = server.state_for_target(None, Some("main".to_string()))?;
14956        require(
14957            main.root == primary.canonicalize()? && selected.root != main.root,
14958            "interleaved main and worktree selections bled into one target",
14959        )?;
14960        let source_before = fs::read(worktree_a.join("src").join("lib.rs"))?;
14961        let removed = server.atlas_worktree_remove(Parameters(AtlasWorktreeRemoveParams {
14962            worktree: "issue-430".to_string(),
14963        }));
14964        require(
14965            removed.contains("status: retired")
14966                && removed.contains("git_unchanged: true")
14967                && removed.contains("files_unchanged: true"),
14968            "unregister did not retire the alias with lifecycle-neutral status",
14969        )?;
14970        require(
14971            target_db.is_file()
14972                && worktree_a.join(".git").is_file()
14973                && fs::read(worktree_a.join("src").join("lib.rs"))? == source_before,
14974            "unregister deleted or modified target-owned Git, source, or atlas state",
14975        )?;
14976        let git_after = run_fixture_command(StdCommand::new("git").current_dir(&primary).args([
14977            "worktree",
14978            "list",
14979            "--porcelain",
14980        ]))?;
14981        require(
14982            git_after == git_before,
14983            "ProjectAtlas worktree operations changed Git lifecycle state",
14984        )?;
14985        let control = open_atlas_store_read_only_for_project(&control_db, &primary)?;
14986        let registrations = control.worktree_registrations(true)?;
14987        require(
14988            registrations.iter().any(|registration| {
14989                registration.alias.as_str() == "issue-430"
14990                    && registration.state == WorktreeRegistrationState::Retired
14991            }),
14992            "retired registration and retained telemetry identity were not durable",
14993        )?;
14994        require(
14995            server
14996                .state_for_target(None, Some("issue-430".to_string()))
14997                .is_err(),
14998            "retired alias remained selectable for source operations",
14999        )
15000    }
15001
15002    #[test]
15003    fn uninitialized_alias_rejects_a_reused_git_administrative_path()
15004    -> Result<(), Box<dyn std::error::Error>> {
15005        let temp = tempfile::tempdir()?;
15006        let primary = temp.path().join("control");
15007        let original = temp.path().join("first location").join("checkout");
15008        let replacement = temp.path().join("second location").join("checkout");
15009        fs::create_dir_all(&primary)?;
15010        fs::create_dir_all(
15011            original
15012                .parent()
15013                .ok_or_else(|| io::Error::other("original worktree has no parent"))?,
15014        )?;
15015        fs::create_dir_all(
15016            replacement
15017                .parent()
15018                .ok_or_else(|| io::Error::other("replacement worktree has no parent"))?,
15019        )?;
15020        run_fixture_command(StdCommand::new("git").current_dir(&primary).arg("init"))?;
15021        for (key, value) in [
15022            ("user.name", "ProjectAtlas Test"),
15023            ("user.email", "projectatlas@example.invalid"),
15024            ("commit.gpgsign", "false"),
15025        ] {
15026            run_fixture_command(
15027                StdCommand::new("git")
15028                    .current_dir(&primary)
15029                    .args(["config", key, value]),
15030            )?;
15031        }
15032        fs::write(primary.join("lib.rs"), "pub fn control() {}\n")?;
15033        run_fixture_command(
15034            StdCommand::new("git")
15035                .current_dir(&primary)
15036                .args(["add", "."]),
15037        )?;
15038        run_fixture_command(
15039            StdCommand::new("git")
15040                .current_dir(&primary)
15041                .args(["commit", "-m", "fixture"]),
15042        )?;
15043        run_fixture_command(
15044            StdCommand::new("git")
15045                .current_dir(&primary)
15046                .args(["worktree", "add", "-b", "original"])
15047                .arg(&original),
15048        )?;
15049
15050        let control_db = primary.join(".projectatlas").join("projectatlas.db");
15051        let control_config = primary.join(".projectatlas").join("config.toml");
15052        init_project_with_config(&primary, Some(&control_config))?;
15053        drop(AtlasStore::open_for_project(&control_db, &primary)?);
15054        let server = ProjectAtlasMcpServer::new(
15055            control_db,
15056            Some(control_config),
15057            "worktree-lifecycle".to_string(),
15058            false,
15059        );
15060        let repository = server.control_git_repository()?;
15061        let original_root = original.canonicalize()?;
15062        let original_entry = repository
15063            .worktrees
15064            .iter()
15065            .find(|entry| {
15066                ProjectAtlasMcpServer::active_worktree_root(entry) == Some(original_root.as_path())
15067            })
15068            .ok_or_else(|| io::Error::other("original worktree was not discovered"))?;
15069        let administrative_directory = original_entry.administrative_directory.clone();
15070        let administrative_identity = git_administrative_identity(&administrative_directory)?;
15071        let added = server.atlas_worktree_add(Parameters(AtlasWorktreeAddParams {
15072            worktree: ProjectAtlasMcpServer::worktree_candidate_selector(original_entry),
15073            alias: Some("replacement-check".to_string()),
15074        }));
15075        require(
15076            added.contains("status: registered"),
15077            &format!("original uninitialized worktree was not registered: {added}"),
15078        )?;
15079
15080        run_fixture_command(
15081            StdCommand::new("git")
15082                .current_dir(&primary)
15083                .args(["worktree", "remove", "--force"])
15084                .arg(&original),
15085        )?;
15086        let missing_registration =
15087            server.atlas_worktree_list(Parameters(AtlasWorktreeListParams {
15088                include_retired: Some(false),
15089            }));
15090        require(
15091            missing_registration
15092                .contains("\"replacement-check\",linked,available,missing,registered")
15093                && missing_registration.contains(&normalize_native_path_display(&original))
15094                && missing_registration.contains(",unavailable,unavailable,0,")
15095                && missing_registration.contains(MCP_WORKTREE_MISSING_RETENTION_REASON),
15096            &format!(
15097                "active alias disappeared after external Git worktree removal: {missing_registration}"
15098            ),
15099        )?;
15100        run_fixture_command(
15101            StdCommand::new("git")
15102                .current_dir(&primary)
15103                .args(["worktree", "add", "-b", "replacement"])
15104                .arg(&replacement),
15105        )?;
15106        let replacement_repository = server.control_git_repository()?;
15107        let replacement_root = replacement.canonicalize()?;
15108        let replacement_entry = replacement_repository
15109            .worktrees
15110            .iter()
15111            .find(|entry| {
15112                ProjectAtlasMcpServer::active_worktree_root(entry)
15113                    == Some(replacement_root.as_path())
15114            })
15115            .ok_or_else(|| io::Error::other("replacement worktree was not discovered"))?;
15116        require(
15117            replacement_entry.administrative_directory == administrative_directory,
15118            "Git fixture did not reuse the administrative path",
15119        )?;
15120        require(
15121            git_administrative_identity(&replacement_entry.administrative_directory)?
15122                != administrative_identity,
15123            "replacement Git worktree reused the prior lifecycle identity",
15124        )?;
15125        let add_revalidation = server.revalidate_worktree_candidate(
15126            &repository,
15127            original_entry,
15128            &administrative_identity,
15129        );
15130        require(
15131            add_revalidation.as_ref().is_err_and(|error| {
15132                error
15133                    .to_string()
15134                    .contains(MCP_ERROR_WORKTREE_LIFECYCLE_CHANGED)
15135            }),
15136            "add revalidation combined the old root with a replacement lifecycle",
15137        )?;
15138
15139        let resolution_error = server
15140            .state_for_target(None, Some("replacement-check".to_string()))
15141            .err()
15142            .ok_or_else(|| io::Error::other("reused administrative path was accepted"))?;
15143        require(
15144            resolution_error
15145                .to_string()
15146                .contains(MCP_ERROR_WORKTREE_LIFECYCLE_CHANGED),
15147            "reused administrative path did not return the lifecycle blocker",
15148        )?;
15149        require(
15150            !replacement.join(".projectatlas").exists(),
15151            "failed lifecycle validation initialized the replacement worktree",
15152        )?;
15153        let before_remove =
15154            run_fixture_command(StdCommand::new("git").current_dir(&primary).args([
15155                "worktree",
15156                "list",
15157                "--porcelain",
15158            ]))?;
15159        let removed = server.atlas_worktree_remove(Parameters(AtlasWorktreeRemoveParams {
15160            worktree: "replacement-check".to_string(),
15161        }));
15162        require(
15163            removed.contains("status: retired")
15164                && removed.contains(MCP_ERROR_WORKTREE_LIFECYCLE_CHANGED),
15165            &format!("stale registration could not be retired safely: {removed}"),
15166        )?;
15167        let after_remove =
15168            run_fixture_command(StdCommand::new("git").current_dir(&primary).args([
15169                "worktree",
15170                "list",
15171                "--porcelain",
15172            ]))?;
15173        require(
15174            after_remove == before_remove,
15175            "retiring a stale registration changed Git lifecycle state",
15176        )
15177    }
15178
15179    #[test]
15180    fn reused_git_administrative_path_cannot_synchronize_replacement_telemetry()
15181    -> Result<(), Box<dyn std::error::Error>> {
15182        let temp = tempfile::tempdir()?;
15183        let primary = temp.path().join("control");
15184        let worktree = temp.path().join("checkout");
15185        fs::create_dir_all(&primary)?;
15186        run_fixture_command(StdCommand::new("git").current_dir(&primary).arg("init"))?;
15187        for (key, value) in [
15188            ("user.name", "ProjectAtlas Test"),
15189            ("user.email", "projectatlas@example.invalid"),
15190            ("commit.gpgsign", "false"),
15191        ] {
15192            run_fixture_command(
15193                StdCommand::new("git")
15194                    .current_dir(&primary)
15195                    .args(["config", key, value]),
15196            )?;
15197        }
15198        fs::write(primary.join("lib.rs"), "pub fn control() {}\n")?;
15199        run_fixture_command(
15200            StdCommand::new("git")
15201                .current_dir(&primary)
15202                .args(["add", "."]),
15203        )?;
15204        run_fixture_command(
15205            StdCommand::new("git")
15206                .current_dir(&primary)
15207                .args(["commit", "-m", "fixture"]),
15208        )?;
15209        run_fixture_command(
15210            StdCommand::new("git")
15211                .current_dir(&primary)
15212                .args(["worktree", "add", "-b", "original-telemetry"])
15213                .arg(&worktree),
15214        )?;
15215
15216        let control_db = primary.join(".projectatlas").join("projectatlas.db");
15217        let target_db = worktree.join(".projectatlas").join("projectatlas.db");
15218        fs::create_dir_all(
15219            control_db
15220                .parent()
15221                .ok_or_else(|| io::Error::other("control database has no parent"))?,
15222        )?;
15223        fs::create_dir_all(
15224            target_db
15225                .parent()
15226                .ok_or_else(|| io::Error::other("target database has no parent"))?,
15227        )?;
15228        let event = usage_from_text(
15229            "worktree-lifecycle",
15230            "atlas_overview",
15231            None,
15232            None,
15233            "pub fn source() {}",
15234            "repository overview",
15235        );
15236        let target = AtlasStore::open_for_project(&target_db, &worktree)?;
15237        target.record_usage(&event)?;
15238        let target_project = target
15239            .project_instance_id()?
15240            .ok_or_else(|| io::Error::other("target project identity is missing"))?;
15241        drop(target);
15242
15243        let RepositoryStructure::Git(repository) = discover_repository_structure(&primary)? else {
15244            return Err(io::Error::other("Git repository was not discovered").into());
15245        };
15246        let canonical_worktree = worktree.canonicalize()?;
15247        let entry = repository
15248            .worktrees
15249            .iter()
15250            .find(|entry| {
15251                ProjectAtlasMcpServer::active_worktree_root(entry)
15252                    == Some(canonical_worktree.as_path())
15253            })
15254            .ok_or_else(|| io::Error::other("worktree was not discovered"))?;
15255        let administrative_directory = entry.administrative_directory.clone();
15256        let administrative_identity = git_administrative_identity(&administrative_directory)?;
15257        let alias = WorktreeAlias::parse("lifecycle-telemetry")?;
15258        let control = AtlasStore::open_for_project(&control_db, &primary)?;
15259        control.register_worktree(
15260            &alias,
15261            &repository.common_directory,
15262            &administrative_directory,
15263            &administrative_identity,
15264            &canonical_worktree,
15265            Some(target_project),
15266            1,
15267        )?;
15268        drop(control);
15269        synchronize_registered_worktree_usage(&control_db, &primary, None)?;
15270        let control = AtlasStore::open_for_project(&control_db, &primary)?;
15271        require(
15272            control.registered_worktree_token_overview(&alias)?.calls == 1,
15273            "initial worktree telemetry was not synchronized",
15274        )?;
15275        drop(control);
15276
15277        let saved_db = temp.path().join("saved-projectatlas.db");
15278        fs::copy(&target_db, &saved_db)?;
15279        run_fixture_command(
15280            StdCommand::new("git")
15281                .current_dir(&primary)
15282                .args(["worktree", "remove", "--force"])
15283                .arg(&worktree),
15284        )?;
15285        require(
15286            matches!(
15287                synchronize_registered_worktree_usage(&control_db, &primary, None),
15288                Err(CliError::InvalidInput(message))
15289                    if message.contains("aggregate token totals cannot be synchronized")
15290            ),
15291            "externally removed bound worktree reported stale aggregate success",
15292        )?;
15293        run_fixture_command(
15294            StdCommand::new("git")
15295                .current_dir(&primary)
15296                .args(["worktree", "add", "-b", "replacement-telemetry"])
15297                .arg(&worktree),
15298        )?;
15299        fs::create_dir_all(
15300            target_db
15301                .parent()
15302                .ok_or_else(|| io::Error::other("target database has no parent"))?,
15303        )?;
15304        fs::copy(&saved_db, &target_db)?;
15305        let replacement = AtlasStore::open_for_project(&target_db, &worktree)?;
15306        replacement.record_usage(&event)?;
15307        drop(replacement);
15308
15309        let RepositoryStructure::Git(replacement_repository) =
15310            discover_repository_structure(&primary)?
15311        else {
15312            return Err(io::Error::other("replacement Git repository was not discovered").into());
15313        };
15314        let replacement_entry = replacement_repository
15315            .worktrees
15316            .iter()
15317            .find(|entry| {
15318                ProjectAtlasMcpServer::active_worktree_root(entry)
15319                    == Some(canonical_worktree.as_path())
15320            })
15321            .ok_or_else(|| io::Error::other("replacement worktree was not discovered"))?;
15322        require(
15323            replacement_entry.administrative_directory == administrative_directory,
15324            "Git fixture did not reuse the administrative path",
15325        )?;
15326        require(
15327            git_administrative_identity(&replacement_entry.administrative_directory)?
15328                != administrative_identity,
15329            "replacement Git worktree reused the prior lifecycle identity",
15330        )?;
15331
15332        require(
15333            matches!(
15334                synchronize_registered_worktree_usage(&control_db, &primary, None),
15335                Err(CliError::InvalidInput(message))
15336                    if message.contains("aggregate token totals cannot be synchronized")
15337            ),
15338            "replacement lifecycle reported stale aggregate success",
15339        )?;
15340        let control = open_atlas_store_read_only_for_project(&control_db, &primary)?;
15341        require(
15342            control.registered_worktree_token_overview(&alias)?.calls == 1,
15343            "replacement lifecycle telemetry was imported into the retired origin",
15344        )
15345    }
15346
15347    #[test]
15348    fn aggregate_synchronization_propagates_local_atlas_and_identity_failures()
15349    -> Result<(), Box<dyn std::error::Error>> {
15350        let temp = tempfile::tempdir()?;
15351        let primary = temp.path().join("control");
15352        let worktree = temp.path().join("external").join("worktree");
15353        fs::create_dir_all(&primary)?;
15354        fs::create_dir_all(
15355            worktree
15356                .parent()
15357                .ok_or_else(|| io::Error::other("worktree has no parent"))?,
15358        )?;
15359        run_fixture_command(StdCommand::new("git").current_dir(&primary).arg("init"))?;
15360        for (key, value) in [
15361            ("user.name", "ProjectAtlas Test"),
15362            ("user.email", "projectatlas@example.invalid"),
15363            ("commit.gpgsign", "false"),
15364        ] {
15365            run_fixture_command(
15366                StdCommand::new("git")
15367                    .current_dir(&primary)
15368                    .args(["config", key, value]),
15369            )?;
15370        }
15371        fs::write(primary.join("README.md"), "# fixture\n")?;
15372        run_fixture_command(
15373            StdCommand::new("git")
15374                .current_dir(&primary)
15375                .args(["add", "."]),
15376        )?;
15377        run_fixture_command(
15378            StdCommand::new("git")
15379                .current_dir(&primary)
15380                .args(["commit", "-m", "fixture"]),
15381        )?;
15382        run_fixture_command(
15383            StdCommand::new("git")
15384                .current_dir(&primary)
15385                .args(["worktree", "add", "-b", "sync-failure"])
15386                .arg(&worktree),
15387        )?;
15388
15389        let control_db = primary.join(".projectatlas").join("projectatlas.db");
15390        let target_db = worktree.join(".projectatlas").join("projectatlas.db");
15391        fs::create_dir_all(
15392            control_db
15393                .parent()
15394                .ok_or_else(|| io::Error::other("control database has no parent"))?,
15395        )?;
15396        fs::create_dir_all(
15397            target_db
15398                .parent()
15399                .ok_or_else(|| io::Error::other("target database has no parent"))?,
15400        )?;
15401        let target = AtlasStore::open_for_project(&target_db, &worktree)?;
15402        target.record_usage(&usage_from_text(
15403            "worktree-sync",
15404            "atlas_overview",
15405            None,
15406            None,
15407            "pub fn source() {}",
15408            "repository overview",
15409        ))?;
15410        let target_project = target
15411            .project_instance_id()?
15412            .ok_or_else(|| io::Error::other("target project identity is missing"))?;
15413        drop(target);
15414
15415        let RepositoryStructure::Git(repository) = discover_repository_structure(&primary)? else {
15416            return Err(io::Error::other("Git repository was not discovered").into());
15417        };
15418        let canonical_worktree = worktree.canonicalize()?;
15419        let entry = repository
15420            .worktrees
15421            .iter()
15422            .find(|entry| {
15423                ProjectAtlasMcpServer::active_worktree_root(entry)
15424                    == Some(canonical_worktree.as_path())
15425            })
15426            .ok_or_else(|| io::Error::other("worktree was not discovered"))?;
15427        let alias = WorktreeAlias::parse("sync-failure")?;
15428        let foreign_project = if target_project == ProjectInstanceId::from_bytes([0x7f; 16])? {
15429            ProjectInstanceId::from_bytes([0x7e; 16])?
15430        } else {
15431            ProjectInstanceId::from_bytes([0x7f; 16])?
15432        };
15433        let control = AtlasStore::open_for_project(&control_db, &primary)?;
15434        control.register_worktree(
15435            &alias,
15436            &repository.common_directory,
15437            &entry.administrative_directory,
15438            &git_administrative_identity(&entry.administrative_directory)?,
15439            &canonical_worktree,
15440            Some(foreign_project),
15441            1,
15442        )?;
15443        drop(control);
15444
15445        require(
15446            matches!(
15447                synchronize_registered_worktree_usage(&control_db, &primary, None),
15448                Err(CliError::Db(
15449                    DbError::WorktreeTelemetryProjectMismatch { .. }
15450                ))
15451            ),
15452            "aggregate synchronization hid the project identity failure behind stale success",
15453        )?;
15454
15455        let git_directory = primary.join(".git");
15456        let unavailable_git_directory = temp.path().join("unavailable-control-git");
15457        fs::rename(&git_directory, &unavailable_git_directory)?;
15458        require(
15459            matches!(
15460                synchronize_registered_worktree_usage(&control_db, &primary, None),
15461                Err(CliError::InvalidInput(message))
15462                    if message.contains("requires Git control evidence")
15463            ),
15464            "aggregate synchronization treated missing control Git evidence as success",
15465        )?;
15466        fs::rename(&unavailable_git_directory, &git_directory)?;
15467
15468        let control_head = git_directory.join("HEAD");
15469        let valid_control_head = temp.path().join("valid-control-head");
15470        fs::rename(&control_head, &valid_control_head)?;
15471        fs::create_dir(&control_head)?;
15472        require(
15473            matches!(
15474                synchronize_registered_worktree_usage(&control_db, &primary, None),
15475                Err(CliError::InvalidInput(message))
15476                    if message.contains("invalid Git evidence")
15477            ),
15478            "aggregate synchronization treated invalid control Git evidence as success",
15479        )?;
15480        fs::remove_dir(&control_head)?;
15481        fs::rename(&valid_control_head, &control_head)?;
15482
15483        let corrupt = rusqlite::Connection::open(&target_db)?;
15484        corrupt.pragma_update(None, "ignore_check_constraints", true)?;
15485        corrupt.execute("UPDATE usage_aggregate_revisions SET revision = -1", [])?;
15486        drop(corrupt);
15487        require(
15488            matches!(
15489                synchronize_registered_worktree_usage(&control_db, &primary, None),
15490                Err(CliError::Db(DbError::TelemetryIntegerOverflow {
15491                    field: "usage_aggregate_revisions.revision"
15492                }))
15493            ),
15494            "aggregate synchronization hid the local snapshot export failure",
15495        )?;
15496
15497        fs::remove_file(&target_db)?;
15498        require(
15499            matches!(
15500                synchronize_registered_worktree_usage(&control_db, &primary, None),
15501                Err(CliError::InvalidInput(message))
15502                    if message.contains("aggregate token totals cannot be synchronized")
15503            ),
15504            "aggregate synchronization reported stale success for a bound missing atlas",
15505        )?;
15506        fs::create_dir(&target_db)?;
15507        require(
15508            synchronize_registered_worktree_usage(&control_db, &primary, None).is_err(),
15509            "aggregate synchronization hid the existing local atlas open failure",
15510        )
15511    }
15512
15513    #[test]
15514    fn registered_worktree_init_falls_back_from_incomplete_control_and_preserves_existing_atlas()
15515    -> Result<(), Box<dyn std::error::Error>> {
15516        let temp = tempfile::tempdir()?;
15517        let primary = temp.path().join("control");
15518        let linked = temp.path().join("external").join("linked");
15519        fs::create_dir_all(&primary)?;
15520        fs::create_dir_all(
15521            linked
15522                .parent()
15523                .ok_or_else(|| io::Error::other("linked worktree has no parent"))?,
15524        )?;
15525        run_fixture_command(StdCommand::new("git").current_dir(&primary).arg("init"))?;
15526        for (key, value) in [
15527            ("user.name", "ProjectAtlas Test"),
15528            ("user.email", "projectatlas@example.invalid"),
15529            ("commit.gpgsign", "false"),
15530        ] {
15531            run_fixture_command(
15532                StdCommand::new("git")
15533                    .current_dir(&primary)
15534                    .args(["config", key, value]),
15535            )?;
15536        }
15537        fs::write(primary.join("lib.rs"), "pub fn control() {}\n")?;
15538        run_fixture_command(
15539            StdCommand::new("git")
15540                .current_dir(&primary)
15541                .args(["add", "."]),
15542        )?;
15543        run_fixture_command(
15544            StdCommand::new("git")
15545                .current_dir(&primary)
15546                .args(["commit", "-m", "fixture"]),
15547        )?;
15548        run_fixture_command(
15549            StdCommand::new("git")
15550                .current_dir(&primary)
15551                .args(["worktree", "add", "-b", "fallback"])
15552                .arg(&linked),
15553        )?;
15554        let git_before = run_fixture_command(StdCommand::new("git").current_dir(&primary).args([
15555            "worktree",
15556            "list",
15557            "--porcelain",
15558        ]))?;
15559
15560        let control_db = primary.join(".projectatlas").join("projectatlas.db");
15561        fs::create_dir_all(
15562            control_db
15563                .parent()
15564                .ok_or_else(|| io::Error::other("control DB has no parent"))?,
15565        )?;
15566        drop(AtlasStore::open_for_project(&control_db, &primary)?);
15567        let server = ProjectAtlasMcpServer::new(
15568            control_db.clone(),
15569            None,
15570            "worktree-fallback".to_string(),
15571            false,
15572        );
15573        let repository = server.control_git_repository()?;
15574        let canonical_linked = linked.canonicalize()?;
15575        let entry = repository
15576            .worktrees
15577            .iter()
15578            .find(|entry| {
15579                ProjectAtlasMcpServer::active_worktree_root(entry)
15580                    == Some(canonical_linked.as_path())
15581            })
15582            .ok_or_else(|| io::Error::other("linked worktree was not discovered"))?;
15583        let added = server.atlas_worktree_add(Parameters(AtlasWorktreeAddParams {
15584            worktree: ProjectAtlasMcpServer::worktree_candidate_selector(entry),
15585            alias: Some("fallback".to_string()),
15586        }));
15587        require(
15588            added.contains("status: registered"),
15589            "fallback fixture registration failed",
15590        )?;
15591
15592        let initialized = server.atlas_init(Parameters(AtlasInitParams {
15593            project_path: None,
15594            worktree: Some("fallback".to_string()),
15595            no_scan: Some(false),
15596            force_rescan: Some(false),
15597            text_index_max_bytes: None,
15598        }));
15599        require(
15600            initialized.contains("status: fallback")
15601                && initialized.contains("fallback_reason:")
15602                && initialized.contains("repository graph is unavailable"),
15603            &format!(
15604                "incomplete control atlas did not produce visible ordinary fallback: {initialized}"
15605            ),
15606        )?;
15607        let target_db = linked.join(".projectatlas").join("projectatlas.db");
15608        let target = open_atlas_store_read_only_for_project(&target_db, &linked)?;
15609        let identity = target
15610            .project_instance_id()?
15611            .ok_or_else(|| io::Error::other("fallback target identity is missing"))?;
15612        require(
15613            target.index_publication()?.is_some_and(|publication| {
15614                publication.state == projectatlas_db::IndexPublicationState::Complete
15615            }),
15616            "ordinary fallback did not publish a complete target index",
15617        )?;
15618        drop(target);
15619
15620        let repeated = server.atlas_init(Parameters(AtlasInitParams {
15621            project_path: None,
15622            worktree: Some("fallback".to_string()),
15623            no_scan: Some(true),
15624            force_rescan: Some(false),
15625            text_index_max_bytes: None,
15626        }));
15627        require(
15628            repeated.contains("status: existing"),
15629            &format!("repeat init did not preserve the valid target atlas: {repeated}"),
15630        )?;
15631        let preserved = open_atlas_store_read_only_for_project(&target_db, &linked)?;
15632        require(
15633            preserved.project_instance_id()? == Some(identity),
15634            "repeat init replaced the valid target atlas identity",
15635        )?;
15636        let control = open_atlas_store_read_only_for_project(&control_db, &primary)?;
15637        require(
15638            control
15639                .worktree_registration(&WorktreeAlias::parse("fallback")?)?
15640                .project_instance_id
15641                == Some(identity),
15642            "fallback init did not bind the exact target identity",
15643        )?;
15644        let git_after = run_fixture_command(StdCommand::new("git").current_dir(&primary).args([
15645            "worktree",
15646            "list",
15647            "--porcelain",
15648        ]))?;
15649        require(
15650            git_after == git_before,
15651            "fallback or repeat init changed Git lifecycle state",
15652        )?;
15653
15654        let config_path = init_config_path(&linked, None);
15655        let config_before = fs::read(&config_path)?;
15656        drop(preserved);
15657        drop(control);
15658        fs::remove_file(&target_db)?;
15659        let refused = server.atlas_init(Parameters(AtlasInitParams {
15660            project_path: None,
15661            worktree: Some("fallback".to_string()),
15662            no_scan: Some(false),
15663            force_rescan: Some(false),
15664            text_index_max_bytes: None,
15665        }));
15666        require(
15667            refused.contains(MCP_ERROR_BOUND_WORKTREE_ATLAS_MISSING),
15668            &format!("bound missing atlas did not fail before initialization: {refused}"),
15669        )?;
15670        require(
15671            !target_db.exists() && fs::read(&config_path)? == config_before,
15672            "bound missing atlas refusal changed target ProjectAtlas state",
15673        )
15674    }
15675
15676    #[test]
15677    fn bare_startup_and_root_set_preserve_worktree_required_without_state()
15678    -> Result<(), Box<dyn std::error::Error>> {
15679        let temp = tempfile::tempdir()?;
15680        let bare = temp.path().join("repository.git");
15681        let output = StdCommand::new("git")
15682            .args(["init", "--bare"])
15683            .arg(&bare)
15684            .output()?;
15685        require(output.status.success(), "git init --bare failed")?;
15686        let db_path = bare.join(".projectatlas").join("projectatlas.db");
15687        let server =
15688            ProjectAtlasMcpServer::new(db_path, None, "mcp-bare-root-test".to_string(), false);
15689
15690        let Err(error) = server.active_project_state() else {
15691            return Err(io::Error::other("bare MCP startup state was exposed as active").into());
15692        };
15693        if !matches!(error, CliError::WorktreeRequired(_)) {
15694            return Err(io::Error::other(format!(
15695                "bare MCP startup did not preserve typed worktree_required state: {error:?}"
15696            ))
15697            .into());
15698        }
15699
15700        let response = server.atlas_root_set(Parameters(AtlasRootSetParams {
15701            root: bare.to_string_lossy().to_string(),
15702            transition: None,
15703            nearest_project: None,
15704        }));
15705        require(
15706            response.contains("worktree_required"),
15707            "atlas_root_set did not reject a bare Git control root",
15708        )?;
15709        require(
15710            !bare.join(".projectatlas").exists(),
15711            "bare MCP startup or root-set refusal created project state",
15712        )
15713    }
15714
15715    #[test]
15716    fn selected_project_config_cannot_redirect_root() -> Result<(), Box<dyn std::error::Error>> {
15717        let temp = tempfile::tempdir()?;
15718        let repo_a = temp.path().join("repo-a");
15719        let repo_b = temp.path().join("repo-b");
15720        fs::create_dir(&repo_a)?;
15721        fs::create_dir(&repo_b)?;
15722        fs::create_dir(repo_b.join(".projectatlas"))?;
15723        let escaped_repo_a = repo_a.to_string_lossy().replace('\\', "/");
15724        fs::write(
15725            repo_b.join(".projectatlas").join("config.toml"),
15726            format!("[project]\nroot = \"{escaped_repo_a}\"\n"),
15727        )?;
15728
15729        let Err(error) = ProjectAtlasMcpServer::project_state_from_root(&repo_b) else {
15730            return Err(io::Error::other("stale selected-project config was accepted").into());
15731        };
15732        require(
15733            error.to_string().contains("outside selected project root"),
15734            "stale selected-project config error was not root-scoped",
15735        )?;
15736
15737        Ok(())
15738    }
15739
15740    #[test]
15741    fn startup_config_mismatch_cannot_bind_one_root_to_another_db()
15742    -> Result<(), Box<dyn std::error::Error>> {
15743        let temp = tempfile::tempdir()?;
15744        let repo_a = temp.path().join("repo-a");
15745        let repo_b = temp.path().join("repo-b");
15746        fs::create_dir(&repo_a)?;
15747        fs::create_dir(&repo_b)?;
15748        fs::create_dir(repo_a.join(".projectatlas"))?;
15749        let escaped_repo_a = repo_a.to_string_lossy().replace('\\', "/");
15750        let config_a = repo_a.join(".projectatlas").join("config.toml");
15751        fs::write(
15752            &config_a,
15753            format!("[project]\nroot = \"{escaped_repo_a}\"\n"),
15754        )?;
15755
15756        let db_b = repo_b.join(".projectatlas").join("projectatlas.db");
15757        let server =
15758            ProjectAtlasMcpServer::new(db_b.clone(), Some(config_a), "mcp-test".to_string(), false);
15759        let state = server.active_project_state()?;
15760
15761        require(
15762            state.root == canonical_project_root(&repo_b)?,
15763            "startup state did not fall back to the DB project root",
15764        )?;
15765        require(
15766            state.db_path == db_b,
15767            "startup state changed the selected DB path",
15768        )?;
15769        require(
15770            state.config_path.is_none(),
15771            "startup state retained a config from another project root",
15772        )?;
15773
15774        Ok(())
15775    }
15776
15777    #[test]
15778    fn read_only_store_does_not_create_missing_index() -> Result<(), Box<dyn std::error::Error>> {
15779        let temp = tempfile::tempdir()?;
15780        let repo = temp.path().join("repo-a");
15781        fs::create_dir(&repo)?;
15782        let state = ProjectAtlasMcpServer::project_state_from_root(&repo)?;
15783
15784        let Err(error) = ProjectAtlasMcpServer::open_read_store(&state) else {
15785            return Err(io::Error::other("missing index opened unexpectedly").into());
15786        };
15787        require(
15788            matches!(error, CliError::InitRequired(_)),
15789            "missing index did not return typed init_required state",
15790        )?;
15791        let payload = ProjectAtlasMcpServer::encode_error_payload(&error);
15792        require(
15793            payload.contains("kind: init_required")
15794                && payload.contains("init_required:")
15795                && payload.contains("tool: atlas_init")
15796                && payload.contains(&normalize_native_path_display(&repo)),
15797            "missing index payload did not contain the exact atlas_init recovery call",
15798        )?;
15799        require(
15800            !repo.join(".projectatlas").exists(),
15801            "read-only store created .projectatlas",
15802        )?;
15803
15804        Ok(())
15805    }
15806
15807    #[test]
15808    fn atlas_init_explicit_project_path_bootstraps_without_switching_active_project()
15809    -> Result<(), Box<dyn std::error::Error>> {
15810        let temp = tempfile::tempdir()?;
15811        let repo_a = temp.path().join("repo-a");
15812        let repo_b = temp.path().join("repo-b");
15813        fs::create_dir(&repo_a)?;
15814        fs::create_dir(&repo_b)?;
15815        let db_a = repo_a.join(".projectatlas").join("projectatlas.db");
15816        let server = ProjectAtlasMcpServer::new(db_a, None, "mcp-test".to_string(), false);
15817        let active_before = server.active_project_state()?;
15818
15819        let text = server.atlas_init(Parameters(AtlasInitParams {
15820            project_path: Some(repo_b.to_string_lossy().to_string()),
15821            worktree: None,
15822            no_scan: Some(true),
15823            force_rescan: Some(false),
15824            text_index_max_bytes: None,
15825        }));
15826
15827        let expected_b = normalize_native_path_display(canonical_project_root(&repo_b)?);
15828        require(
15829            text.contains("init:"),
15830            "atlas_init did not return named init payload",
15831        )?;
15832        require(
15833            text.contains(&expected_b),
15834            "atlas_init did not report the explicit project path",
15835        )?;
15836        require(
15837            text.contains("status: skipped"),
15838            "atlas_init --no-scan did not report skipped scan",
15839        )?;
15840        require(
15841            text.contains("purpose_handoff:")
15842                && text.contains("execution_owner: agent_host")
15843                && text.contains(&format!(
15844                    "recommended_subagent_reasoning: {PURPOSE_CURATOR_RECOMMENDED_REASONING}"
15845                ))
15846                && text.contains("main_agent_fallback: true")
15847                && text.contains("server_started_curator: false")
15848                && text.contains("silent_on_success: true")
15849                && text.contains("curation_scope: low"),
15850            "atlas_init did not expose the host-owned low-scope curator handoff",
15851        )?;
15852        require(
15853            repo_b
15854                .join(".projectatlas")
15855                .join("projectatlas.db")
15856                .is_file(),
15857            "atlas_init did not create the explicit project's DB",
15858        )?;
15859        require(
15860            repo_b
15861                .join(".projectatlas")
15862                .join("projectatlas.mcp.json")
15863                .is_file()
15864                && repo_b
15865                    .join(".projectatlas")
15866                    .join("projectatlas.claude.mcp.json")
15867                    .is_file()
15868                && repo_b
15869                    .join(".projectatlas")
15870                    .join("projectatlas.opencode.json")
15871                    .is_file(),
15872            "atlas_init did not generate host MCP configs",
15873        )?;
15874
15875        let active_after = server.active_project_state()?;
15876        require(
15877            active_after.root == active_before.root,
15878            "atlas_init with explicit project_path changed the active default root",
15879        )?;
15880        require(
15881            !repo_a.join(".projectatlas").exists(),
15882            "explicit atlas_init mutated the active project",
15883        )?;
15884
15885        Ok(())
15886    }
15887
15888    #[test]
15889    fn session_brief_missing_index_stays_read_only() -> Result<(), Box<dyn std::error::Error>> {
15890        let temp = tempfile::tempdir()?;
15891        let repo = temp.path().join("repo-a");
15892        fs::create_dir(&repo)?;
15893        let db_path = repo.join(".projectatlas").join("projectatlas.db");
15894        let server = ProjectAtlasMcpServer::new(db_path, None, "mcp-test".to_string(), false);
15895
15896        let brief = server.build_session_brief(
15897            AtlasSessionBriefParams {
15898                project_path: None,
15899                worktree: None,
15900                query: Some("startup".to_string()),
15901                purpose_task: None,
15902                compact: None,
15903                folder_limit: None,
15904                file_limit: None,
15905                blocker_limit: None,
15906                purpose_limit: None,
15907            },
15908            None,
15909        )?;
15910
15911        require(
15912            brief.project.index_status == McpIndexStatus::Missing,
15913            "missing index was not represented as typed state",
15914        )?;
15915        require(
15916            brief.overview.is_none(),
15917            "missing-index brief unexpectedly included overview",
15918        )?;
15919        require(
15920            brief.recommendations.iter().any(|recommendation| {
15921                matches!(recommendation.kind, McpBriefRecommendationKind::Init)
15922                    && recommendation.target == MCP_TOOL_ATLAS_INIT
15923                    && recommendation.arguments.get(MCP_BRIEF_ARG_PROJECT_PATH)
15924                        == Some(&serde_json::json!(normalize_native_path_display(&repo)))
15925            }),
15926            "missing-index brief did not recommend atlas_init for the exact selected root",
15927        )?;
15928        require(
15929            !repo.join(".projectatlas").exists(),
15930            "session brief created .projectatlas for a missing index",
15931        )?;
15932
15933        Ok(())
15934    }
15935
15936    #[test]
15937    fn session_brief_recommendations_preserve_per_call_project_path()
15938    -> Result<(), Box<dyn std::error::Error>> {
15939        let project_path = "F:/example/repo-b".to_string();
15940        let recommendations = ProjectAtlasMcpServer::indexed_project_recommendations(
15941            "startup",
15942            Some(NavigationNextCall {
15943                capability: NavigationNextCapability::Summary,
15944                path: "src/lib.rs".to_string(),
15945            }),
15946            1,
15947            7,
15948            Some(project_path.clone()),
15949            None,
15950        );
15951
15952        require(
15953            recommendations.iter().all(|recommendation| {
15954                recommendation.arguments.get(MCP_BRIEF_ARG_PROJECT_PATH)
15955                    == Some(&serde_json::Value::String(project_path.clone()))
15956            }),
15957            "indexed brief recommendations did not preserve project_path",
15958        )?;
15959        require(
15960            recommendations.iter().any(|recommendation| {
15961                matches!(recommendation.kind, McpBriefRecommendationKind::Summary)
15962                    && recommendation.target == MCP_TOOL_ATLAS_FILE_SUMMARY
15963                    && recommendation.arguments.get(MCP_BRIEF_ARG_FILE)
15964                        == Some(&serde_json::Value::String("src/lib.rs".to_string()))
15965            }),
15966            "summary recommendation did not preserve the ranked file selector",
15967        )?;
15968        require(
15969            recommendations.iter().all(|recommendation| {
15970                recommendation.target != MCP_TOOL_ATLAS_FOLDERS
15971                    && recommendation.target != MCP_TOOL_ATLAS_FILES
15972            }),
15973            "indexed brief recommended rerunning folder or file ranking",
15974        )?;
15975        require(
15976            recommendations.iter().any(|recommendation| {
15977                matches!(recommendation.kind, McpBriefRecommendationKind::Health)
15978                    && recommendation.arguments.get(MCP_BRIEF_ARG_LIMIT)
15979                        == Some(&serde_json::json!(7))
15980            }),
15981            "health recommendation did not preserve limit",
15982        )?;
15983
15984        let relation_recommendations = ProjectAtlasMcpServer::indexed_project_recommendations(
15985            "startup",
15986            Some(NavigationNextCall {
15987                capability: NavigationNextCapability::Relations,
15988                path: "src/graph.rs".to_string(),
15989            }),
15990            0,
15991            7,
15992            Some(project_path),
15993            None,
15994        );
15995        require(
15996            relation_recommendations.iter().any(|recommendation| {
15997                matches!(recommendation.kind, McpBriefRecommendationKind::Relations)
15998                    && recommendation.target == MCP_TOOL_ATLAS_SYMBOL_RELATIONS
15999                    && recommendation.arguments.get(MCP_BRIEF_ARG_FILE)
16000                        == Some(&serde_json::Value::String("src/graph.rs".to_string()))
16001                    && recommendation.arguments.get(MCP_BRIEF_ARG_VIEW)
16002                        == Some(&serde_json::Value::String("detailed".to_string()))
16003            }),
16004            "relation recommendation did not preserve the ranked file and detailed view",
16005        )?;
16006
16007        let worktree_recommendations = ProjectAtlasMcpServer::indexed_project_recommendations(
16008            "startup",
16009            None,
16010            1,
16011            7,
16012            None,
16013            Some("issue-430".to_string()),
16014        );
16015        require(
16016            worktree_recommendations.iter().all(|recommendation| {
16017                recommendation.arguments.get(MCP_BRIEF_ARG_WORKTREE)
16018                    == Some(&serde_json::Value::String("issue-430".to_string()))
16019                    && recommendation
16020                        .arguments
16021                        .get(MCP_BRIEF_ARG_PROJECT_PATH)
16022                        .is_none()
16023            }),
16024            "indexed brief recommendations did not preserve the mutually exclusive worktree alias",
16025        )?;
16026
16027        Ok(())
16028    }
16029
16030    #[test]
16031    fn session_brief_file_candidates_ignore_indexed_text_fallback()
16032    -> Result<(), Box<dyn std::error::Error>> {
16033        let temp = tempfile::tempdir()?;
16034        let repo = temp.path().join("repo-a");
16035        fs::create_dir_all(repo.join("src"))?;
16036        fs::write(
16037            repo.join("src").join("owner.rs"),
16038            "const ROUTE: &str = \"hiddenNeedle\";\n",
16039        )?;
16040        let db_path = repo.join(".projectatlas").join("projectatlas.db");
16041        let plan = ScanRuntimePlan::for_path(None, &repo, None)?;
16042        let mut store = open_atlas_store_for_project(&db_path, &plan.root)?;
16043        let symbol_options = SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, Some(1), Some(30));
16044        run_scan_pipeline(&mut store, &plan, &symbol_options)?;
16045        drop(store);
16046
16047        let server = ProjectAtlasMcpServer::new(db_path, None, "mcp-test".to_string(), false);
16048        let brief = server.build_session_brief(
16049            AtlasSessionBriefParams {
16050                project_path: None,
16051                worktree: None,
16052                query: Some("hiddenNeedle".to_string()),
16053                purpose_task: None,
16054                compact: None,
16055                folder_limit: Some(5),
16056                file_limit: Some(5),
16057                blocker_limit: Some(5),
16058                purpose_limit: Some(5),
16059            },
16060            None,
16061        )?;
16062
16063        require(
16064            brief.files.is_empty(),
16065            "session brief returned a content-only indexed-text hit",
16066        )?;
16067        require(
16068            brief.recommendations.iter().any(|recommendation| {
16069                matches!(recommendation.kind, McpBriefRecommendationKind::Search)
16070                    && recommendation.target == MCP_TOOL_ATLAS_SEARCH
16071                    && recommendation.arguments.get(MCP_BRIEF_ARG_PATTERN)
16072                        == Some(&serde_json::Value::String("hiddenNeedle".to_string()))
16073            }),
16074            "session brief did not route a content-only query directly to indexed search",
16075        )?;
16076
16077        let navigable = server.build_session_brief(
16078            AtlasSessionBriefParams {
16079                project_path: None,
16080                worktree: None,
16081                query: Some("owner".to_string()),
16082                purpose_task: None,
16083                compact: None,
16084                folder_limit: Some(5),
16085                file_limit: Some(5),
16086                blocker_limit: Some(5),
16087                purpose_limit: Some(5),
16088            },
16089            None,
16090        )?;
16091        let candidate = navigable
16092            .files
16093            .first()
16094            .ok_or_else(|| std::io::Error::other("navigable brief file is missing"))?;
16095        require(
16096            candidate.reason_codes.contains(&RankedReasonCode::Path)
16097                && candidate.next_call.capability == NavigationNextCapability::Summary
16098                && !candidate.purpose_agent_reviewed,
16099            "session brief dropped ranked navigation evidence",
16100        )?;
16101        require(
16102            navigable.recommendations.iter().any(|recommendation| {
16103                matches!(recommendation.kind, McpBriefRecommendationKind::Summary)
16104                    && recommendation.target == MCP_TOOL_ATLAS_FILE_SUMMARY
16105                    && recommendation.arguments.get(MCP_BRIEF_ARG_FILE)
16106                        == Some(&serde_json::Value::String(candidate.path.clone()))
16107            }) && navigable.recommendations.iter().all(|recommendation| {
16108                recommendation.target != MCP_TOOL_ATLAS_FOLDERS
16109                    && recommendation.target != MCP_TOOL_ATLAS_FILES
16110            }),
16111            "session brief recommendation did not follow its returned ranked file directly",
16112        )?;
16113
16114        Ok(())
16115    }
16116
16117    #[test]
16118    fn mcp_navigation_and_session_brief_propagate_typed_graph_evidence()
16119    -> Result<(), Box<dyn std::error::Error>> {
16120        let temp = tempfile::tempdir()?;
16121        let repo = temp.path().join("repo-a");
16122        fs::create_dir_all(repo.join("src"))?;
16123        fs::create_dir_all(repo.join("tests"))?;
16124        fs::write(
16125            repo.join("Cargo.toml"),
16126            "[package]\nname = \"adapter-navigation\"\nversion = \"0.1.0\"\n",
16127        )?;
16128        for path in [
16129            "src/navigation_owner.rs",
16130            "src/navigation_local.rs",
16131            "src/navigation_unresolved.rs",
16132            "tests/navigation_owner.rs",
16133        ] {
16134            fs::write(repo.join(path), "pub fn navigation_fixture() {}\n")?;
16135        }
16136        let db_path = repo.join(".projectatlas").join("projectatlas.db");
16137        let plan = ScanRuntimePlan::for_path(None, &repo, None)?;
16138        let mut store = open_atlas_store_for_project(&db_path, &plan.root)?;
16139        run_scan_pipeline(
16140            &mut store,
16141            &plan,
16142            &SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, Some(1), Some(30)),
16143        )?;
16144        publish_mcp_navigation_graph(&mut store)?;
16145        drop(store);
16146
16147        let server =
16148            ProjectAtlasMcpServer::new(db_path, None, "mcp-navigation-test".to_string(), false);
16149        let folders_text = server.atlas_folders_response(
16150            AtlasQueryParams {
16151                project_path: None,
16152                worktree: None,
16153                query: Some("navigation".to_string()),
16154                limit: Some(10),
16155            },
16156            None,
16157        );
16158        let files_text = server.atlas_files_response(
16159            AtlasFilesParams {
16160                project_path: None,
16161                worktree: None,
16162                query: Some("navigation".to_string()),
16163                folder: None,
16164                nearest_project: Some(false),
16165                file_pattern: None,
16166                include_content: Some(false),
16167                content_selection: None,
16168                limit: Some(10),
16169            },
16170            None,
16171        );
16172        for (surface, text) in [
16173            ("atlas_folders", &folders_text),
16174            ("atlas_files", &files_text),
16175        ] {
16176            require(
16177                text.contains("connection_counts")
16178                    && text.contains("connections")
16179                    && text.contains("direction:")
16180                    && text.contains("target:")
16181                    && text.contains("connections_truncated: true"),
16182                &format!("{surface} dropped nonempty typed graph evidence: {text}"),
16183            )?;
16184        }
16185
16186        let brief = server.build_session_brief(
16187            AtlasSessionBriefParams {
16188                project_path: None,
16189                worktree: None,
16190                query: Some("navigation".to_string()),
16191                purpose_task: None,
16192                compact: None,
16193                folder_limit: Some(10),
16194                file_limit: Some(10),
16195                blocker_limit: Some(10),
16196                purpose_limit: Some(10),
16197            },
16198            None,
16199        )?;
16200        let folder = brief
16201            .folders
16202            .iter()
16203            .find(|candidate| candidate.path == "src")
16204            .ok_or_else(|| io::Error::other("graph-enriched MCP folder is missing"))?;
16205        require(
16206            folder.connection_counts.len() == 7
16207                && folder.connections.len() == 3
16208                && folder.connections_truncated,
16209            "MCP folder lost count, sample, or global truncation evidence",
16210        )?;
16211        let owner = brief
16212            .files
16213            .iter()
16214            .find(|candidate| candidate.path == "src/navigation_owner.rs")
16215            .ok_or_else(|| io::Error::other("graph-enriched MCP owner file is missing"))?;
16216        require(
16217            owner.connection_counts.len() == 7
16218                && owner.connections.len() == 3
16219                && owner.connections_truncated
16220                && owner.next_call.capability
16221                    == projectatlas_core::NavigationNextCapability::Relations,
16222            "MCP file or session brief lost graph truncation or relations navigation",
16223        )?;
16224        let compact_relations = server.atlas_symbol_relations_response(
16225            &AtlasSymbolRelationsParams {
16226                project_path: None,
16227                file: Some("src/navigation_owner.rs".to_string()),
16228                nearest_project: Some(false),
16229                view: Some("detailed".to_string()),
16230                compact: Some(true),
16231                direction: Some("outbound".to_string()),
16232                include_occurrences: Some(true),
16233                limit: Some(10),
16234                output_bytes: Some(64 * 1_024),
16235                ..AtlasSymbolRelationsParams::default()
16236            },
16237            None,
16238        );
16239        require(
16240            compact_relations.contains("status: resolved")
16241                && compact_relations.contains("status: ambiguous")
16242                && compact_relations.contains("status: external")
16243                && compact_relations.contains("status: unresolved")
16244                && compact_relations.contains("reference: \"navigation-ambiguous\"")
16245                && compact_relations.contains("candidates: 2")
16246                && compact_relations.contains("next_call:"),
16247            &format!(
16248                "compact detailed relations dropped a resolution state or reusable next call: {compact_relations}"
16249            ),
16250        )?;
16251
16252        let compact = server.build_compact_session_brief(
16253            AtlasSessionBriefParams {
16254                project_path: None,
16255                worktree: None,
16256                query: Some("navigation_owner".to_string()),
16257                purpose_task: None,
16258                compact: Some(true),
16259                folder_limit: None,
16260                file_limit: None,
16261                blocker_limit: None,
16262                purpose_limit: None,
16263            },
16264            None,
16265        )?;
16266        let compact_owner = compact
16267            .files
16268            .iter()
16269            .find(|candidate| candidate.path == "src/navigation_owner.rs")
16270            .ok_or_else(|| io::Error::other("compact graph owner file is missing"))?;
16271        require(
16272            compact_owner.connections.len() == 1
16273                && compact_owner.connections.iter().all(|connection| {
16274                    connection.kind != RankedConnectionKind::Import
16275                        && !matches!(
16276                            &connection.target,
16277                            RankedConnectionTarget::Unresolved { .. }
16278                        )
16279                })
16280                && compact_owner.next_call.capability == NavigationNextCapability::Summary
16281                && compact_owner.purpose_agent_reviewed,
16282            "compact session brief did not retain one crisp edge and summary-first routing",
16283        )?;
16284        let expanded_text =
16285            ProjectAtlasMcpServer::encode_named_payload(MCP_PAYLOAD_SESSION_BRIEF, &brief)?;
16286        require(
16287            expanded_text.contains("missing_purposes:")
16288                && expanded_text.contains("stale_purposes:")
16289                && expanded_text.contains("approved_purposes:")
16290                && expanded_text.contains("suggested_purposes:"),
16291            "compatibility session brief lost purpose lifecycle counts",
16292        )?;
16293        let compact_text =
16294            ProjectAtlasMcpServer::encode_named_payload(MCP_PAYLOAD_SESSION_BRIEF, &compact)?;
16295        require(
16296            compact.purpose_handoff.as_ref().is_some_and(|handoff| {
16297                handoff.recommended_subagent_reasoning == PURPOSE_CURATOR_RECOMMENDED_REASONING
16298                    && handoff.instructions.len() == 1
16299                    && handoff.instructions.first()
16300                        == brief
16301                            .purpose_handoff
16302                            .as_ref()
16303                            .and_then(|expanded| expanded.instructions.first())
16304            }) && compact_text.contains(&format!(
16305                "recommended_subagent_reasoning: {PURPOSE_CURATOR_RECOMMENDED_REASONING}"
16306            )) && compact_text
16307                .contains("lowest reliable reasoning and cost tier the host supports"),
16308            &format!(
16309                "compact actionable handoff lost its reliable-tier instruction: {compact_text}"
16310            ),
16311        )?;
16312        require(
16313            compact
16314                .blockers
16315                .as_ref()
16316                .is_some_and(|blockers| blockers.total > 0)
16317                && !compact_text.contains("\n    db:")
16318                && !compact_text.contains("\n    config:")
16319                && !compact_text.contains("\n  policy:")
16320                && !compact_text.contains("\n    items:")
16321                && !compact_text.contains("reason_codes")
16322                && !compact_text.contains("connection_counts")
16323                && !compact_text.contains("agent_harness_expected")
16324                && !compact_text.contains("server_started_curator")
16325                && !compact_text.contains("missing_purposes")
16326                && compact_text.len() <= 4_096,
16327            &format!("compact session brief retained default-only chatter: {compact_text}"),
16328        )?;
16329
16330        let families = brief
16331            .files
16332            .iter()
16333            .flat_map(|candidate| candidate.connection_counts.iter().map(|count| count.kind))
16334            .collect::<BTreeSet<_>>();
16335        require(
16336            families
16337                == BTreeSet::from([
16338                    RankedConnectionKind::Package,
16339                    RankedConnectionKind::Import,
16340                    RankedConnectionKind::Call,
16341                    RankedConnectionKind::Reference,
16342                    RankedConnectionKind::Test,
16343                    RankedConnectionKind::Route,
16344                    RankedConnectionKind::Config,
16345                ]),
16346            &format!("MCP graph families were not propagated: {families:?}"),
16347        )?;
16348        let connections = brief
16349            .files
16350            .iter()
16351            .flat_map(|candidate| candidate.connections.iter())
16352            .collect::<Vec<_>>();
16353        require(
16354            connections
16355                .iter()
16356                .any(|connection| connection.direction == RankedConnectionDirection::Outbound)
16357                && connections
16358                    .iter()
16359                    .any(|connection| connection.direction == RankedConnectionDirection::Inbound),
16360            "MCP graph samples did not preserve both directions",
16361        )?;
16362        for (name, present) in [
16363            (
16364                "local",
16365                connections.iter().any(|connection| {
16366                    matches!(connection.target, RankedConnectionTarget::Local { .. })
16367                }),
16368            ),
16369            (
16370                "package",
16371                connections.iter().any(|connection| {
16372                    matches!(connection.target, RankedConnectionTarget::Package { .. })
16373                }),
16374            ),
16375            (
16376                "external",
16377                connections.iter().any(|connection| {
16378                    matches!(connection.target, RankedConnectionTarget::External { .. })
16379                }),
16380            ),
16381            (
16382                "unresolved",
16383                connections.iter().any(|connection| {
16384                    matches!(connection.target, RankedConnectionTarget::Unresolved { .. })
16385                }),
16386            ),
16387        ] {
16388            require(
16389                present,
16390                &format!("MCP graph samples omitted {name} targets"),
16391            )?;
16392        }
16393        Ok(())
16394    }
16395
16396    fn publish_mcp_navigation_graph(
16397        store: &mut AtlasStore,
16398    ) -> Result<(), Box<dyn std::error::Error>> {
16399        let project = store
16400            .project_instance_id()?
16401            .ok_or_else(|| io::Error::other("MCP navigation project identity is missing"))?;
16402        let current_publication = store
16403            .index_publication()?
16404            .ok_or_else(|| io::Error::other("MCP navigation publication is missing"))?;
16405        let fingerprint = current_publication
16406            .contract_fingerprint
16407            .clone()
16408            .ok_or_else(|| io::Error::other("MCP navigation fingerprint is missing"))?;
16409        let generation = current_publication
16410            .generation
16411            .checked_next()
16412            .ok_or_else(|| io::Error::other("MCP navigation generation overflow"))?;
16413        let file_entity = |path: &str| {
16414            GraphEntity::new(
16415                project,
16416                EntitySelector::File {
16417                    path: RepositoryFilePath::new(Path::new(path))?,
16418                },
16419                generation,
16420            )
16421        };
16422        let owner = file_entity("src/navigation_owner.rs")?;
16423        let local = file_entity("src/navigation_local.rs")?;
16424        let unresolved = file_entity("src/navigation_unresolved.rs")?;
16425        let test = file_entity("tests/navigation_owner.rs")?;
16426        let package = GraphEntity::new(
16427            project,
16428            EntitySelector::Package {
16429                package: PackageSelector {
16430                    manager: GraphIdentityText::new("cargo")?,
16431                    name: GraphIdentityText::new("adapter-navigation")?,
16432                    manifest: RepositoryFilePath::new(Path::new("Cargo.toml"))?,
16433                },
16434            },
16435            generation,
16436        )?;
16437        let external = GraphEntity::new(
16438            project,
16439            EntitySelector::External {
16440                external: ExternalSelector {
16441                    system: GraphIdentityText::new("crates.io")?,
16442                    identity: GraphIdentityText::new("serde@1")?,
16443                },
16444            },
16445            generation,
16446        )?;
16447        let resolved = |source: &GraphEntity, kind, target: &GraphEntity| {
16448            Ok::<_, Box<dyn std::error::Error>>(LogicalRelation::new(
16449                source,
16450                kind,
16451                RelationResolution::resolved(target)?,
16452                ConfidenceClass::Exact,
16453                Completeness::Complete,
16454                generation,
16455            )?)
16456        };
16457        let unresolved_relation = |source: &GraphEntity, kind, reference: &str| {
16458            Ok::<_, Box<dyn std::error::Error>>(LogicalRelation::new(
16459                source,
16460                kind,
16461                RelationResolution::Unresolved {
16462                    reference: GraphIdentityText::new(reference)?,
16463                },
16464                ConfidenceClass::High,
16465                Completeness::Partial,
16466                generation,
16467            )?)
16468        };
16469        let relations = vec![
16470            resolved(
16471                &package,
16472                GraphRelationKind::Legacy(RelationKind::DependsOn),
16473                &owner,
16474            )?,
16475            LogicalRelation::new(
16476                &owner,
16477                GraphRelationKind::Legacy(RelationKind::Imports),
16478                RelationResolution::external(&external)?,
16479                ConfidenceClass::Exact,
16480                Completeness::Complete,
16481                generation,
16482            )?,
16483            resolved(
16484                &owner,
16485                GraphRelationKind::Legacy(RelationKind::Calls),
16486                &local,
16487            )?,
16488            unresolved_relation(
16489                &unresolved,
16490                GraphRelationKind::Extended(ExtendedRelationKind::References),
16491                "navigation-reference",
16492            )?,
16493            resolved(
16494                &test,
16495                GraphRelationKind::Extended(ExtendedRelationKind::Tests),
16496                &owner,
16497            )?,
16498            resolved(
16499                &owner,
16500                GraphRelationKind::Extended(ExtendedRelationKind::RoutesTo),
16501                &local,
16502            )?,
16503            LogicalRelation::new(
16504                &owner,
16505                GraphRelationKind::Extended(ExtendedRelationKind::References),
16506                RelationResolution::Ambiguous {
16507                    reference: GraphIdentityText::new("navigation-ambiguous")?,
16508                    candidates: std::num::NonZeroU32::new(2)
16509                        .ok_or_else(|| io::Error::other("ambiguous fixture count is zero"))?,
16510                },
16511                ConfidenceClass::High,
16512                Completeness::Partial,
16513                generation,
16514            )?,
16515            unresolved_relation(
16516                &owner,
16517                GraphRelationKind::Extended(ExtendedRelationKind::Configures),
16518                "NAVIGATION_MODE",
16519            )?,
16520        ];
16521        let nodes = store
16522            .load_nodes()?
16523            .into_iter()
16524            .map(|node| node.node)
16525            .collect::<Vec<_>>();
16526        {
16527            let mut publication = store.begin_index_publication(&fingerprint)?;
16528            publication.begin_scan_replacement()?;
16529            publication.upsert_scan_node_batch(&nodes)?;
16530            publication.finish_scan_replacement()?;
16531            publication.replace_repository_graph(
16532                project,
16533                &[owner, local, unresolved, test, package, external],
16534                &relations,
16535                &[],
16536                &[],
16537            )?;
16538            publication.complete()?;
16539        }
16540        store.set_purpose("src", "Navigation graph folder", PurposeSource::Agent)?;
16541        store.set_purpose(
16542            "src/navigation_owner.rs",
16543            "Navigation graph owner",
16544            PurposeSource::Agent,
16545        )?;
16546        store.set_purpose(
16547            "src/navigation_unresolved.rs",
16548            "Navigation unresolved graph owner",
16549            PurposeSource::Agent,
16550        )?;
16551        Ok(())
16552    }
16553
16554    #[test]
16555    fn session_brief_exposes_host_owned_purpose_curator_handoff()
16556    -> Result<(), Box<dyn std::error::Error>> {
16557        let temp = tempfile::tempdir()?;
16558        let repo = temp.path().join("repo-a");
16559        fs::create_dir_all(repo.join("src"))?;
16560        fs::write(repo.join("src").join("main.rs"), "fn main() {}\n")?;
16561        let db_path = repo.join(".projectatlas").join("projectatlas.db");
16562        let plan = ScanRuntimePlan::for_path(None, &repo, None)?;
16563        let mut store = open_atlas_store_for_project(&db_path, &plan.root)?;
16564        let symbol_options = SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, Some(1), Some(30));
16565        run_scan_pipeline(&mut store, &plan, &symbol_options)?;
16566        drop(store);
16567
16568        let server = ProjectAtlasMcpServer::new(db_path, None, "mcp-test".to_string(), false);
16569        let brief = server.build_session_brief(
16570            AtlasSessionBriefParams {
16571                project_path: None,
16572                worktree: None,
16573                query: Some("startup".to_string()),
16574                purpose_task: Some("startup-task".to_string()),
16575                compact: None,
16576                folder_limit: Some(5),
16577                file_limit: Some(5),
16578                blocker_limit: Some(5),
16579                purpose_limit: Some(1),
16580            },
16581            None,
16582        )?;
16583        let handoff = brief
16584            .purpose_handoff
16585            .as_ref()
16586            .ok_or_else(|| std::io::Error::other("actionable purpose handoff missing"))?;
16587        require(
16588            handoff.execution_owner == "agent_host",
16589            "session handoff was not host-owned",
16590        )?;
16591        require(
16592            handoff.recommended_subagent_reasoning == PURPOSE_CURATOR_RECOMMENDED_REASONING,
16593            "session handoff did not request the lowest reliable host-supported reasoning",
16594        )?;
16595        require(
16596            handoff.main_agent_fallback && !handoff.server_started_curator,
16597            "session handoff misrepresented curator execution ownership",
16598        )?;
16599        require(
16600            handoff.silent_on_success,
16601            "session handoff was not quiet on successful maintenance",
16602        )?;
16603        require(
16604            handoff.queue.task == "startup-task"
16605                && handoff.queue.curation_scope == "low"
16606                && handoff.queue.actionable
16607                && handoff.queue.returned == 1
16608                && handoff.queue.limit == 1
16609                && handoff.queue.truncated,
16610            "compatibility session handoff lost its bounded purpose queue metadata",
16611        )?;
16612        require(
16613            handoff.queue.items.iter().all(|item| {
16614                item.work_key.len() == 64
16615                    && item.state_token.len() == 64
16616                    && !item.purpose_agent_reviewed
16617            }),
16618            "session handoff item tokens or lifecycle state were incomplete",
16619        )?;
16620        let compact = server.build_compact_session_brief(
16621            AtlasSessionBriefParams {
16622                project_path: None,
16623                worktree: None,
16624                query: Some("startup".to_string()),
16625                purpose_task: Some("startup-task".to_string()),
16626                compact: Some(true),
16627                folder_limit: Some(5),
16628                file_limit: Some(5),
16629                blocker_limit: Some(5),
16630                purpose_limit: Some(1),
16631            },
16632            None,
16633        )?;
16634        let compact_handoff = compact
16635            .purpose_handoff
16636            .as_ref()
16637            .ok_or_else(|| std::io::Error::other("compact purpose handoff missing"))?;
16638        require(
16639            matches!(
16640                compact_handoff.next_call.kind,
16641                McpBriefRecommendationKind::PurposeQueue
16642            ) && compact_handoff.next_call.target == MCP_TOOL_ATLAS_PURPOSE_QUEUE
16643                && compact_handoff.next_call.arguments.get(MCP_BRIEF_ARG_TASK)
16644                    == Some(&serde_json::json!("startup-task"))
16645                && compact_handoff.next_call.arguments.get(MCP_BRIEF_ARG_LIMIT)
16646                    == Some(&serde_json::json!(1)),
16647            "compact handoff did not preserve the exact bounded purpose-queue call",
16648        )?;
16649        require(
16650            brief.limits.purpose_limit == 1 && brief.limits.purposes_truncated,
16651            "session brief purpose limits were not reported",
16652        )?;
16653        Ok(())
16654    }
16655
16656    #[test]
16657    fn settings_capabilities_report_nearest_policy() -> Result<(), Box<dyn std::error::Error>> {
16658        let temp = tempfile::tempdir()?;
16659        let repo = temp.path().join("repo-a");
16660        fs::create_dir(&repo)?;
16661        let db_path = repo.join(".projectatlas").join("projectatlas.db");
16662        let disabled =
16663            ProjectAtlasMcpServer::new(db_path.clone(), None, "mcp-test".to_string(), false);
16664        let enabled = ProjectAtlasMcpServer::new(db_path, None, "mcp-test".to_string(), true);
16665        let disabled_state = disabled.active_project_state()?;
16666        let enabled_state = enabled.active_project_state()?;
16667
16668        let disabled_text = disabled.render_settings_with_capabilities(&disabled_state)?;
16669        require(
16670            disabled_text.contains("mcp_session:"),
16671            "settings did not include mcp_session capabilities",
16672        )?;
16673        require(
16674            disabled_text.contains("path_scope: selected_project"),
16675            "disabled nearest-project policy was not typed",
16676        )?;
16677        require(
16678            disabled_text.contains("language_registry:")
16679                && disabled_text.contains("accepted_set_digest:")
16680                && disabled_text.contains("semantic_provider_digest:")
16681                && disabled_text.contains("semantic_relation_contract_digest:")
16682                && disabled_text.contains("relation_family_inventory:")
16683                && disabled_text.contains("optional_disabled_families:")
16684                && disabled_text.contains("benchmarked:")
16685                && !disabled_text.contains("accepted_minimum:")
16686                && !disabled_text.contains("provenance_source:")
16687                && disabled_text.contains("optional_catalog:")
16688                && disabled_text.contains("database:")
16689                && disabled_text.contains("compile_options:")
16690                && disabled_text.contains("search:")
16691                && disabled_text.contains("default_mode: lexical")
16692                && disabled_text.contains("optional_parser_pack:"),
16693            "settings did not project compact shared language registry truth",
16694        )?;
16695        require(
16696            disabled_text.len() <= MCP_SETTINGS_RESPONSE_MAX_BYTES,
16697            "settings exceeded its agent-facing output bound",
16698        )?;
16699        let enabled_text = enabled.render_settings_with_capabilities(&enabled_state)?;
16700        require(
16701            enabled_text.contains("path_scope: nearest_indexed_project"),
16702            "enabled nearest-project policy was not typed",
16703        )?;
16704        require(
16705            !enabled_text.contains("GITHUB_TOKEN") && !enabled_text.contains("GH_TOKEN"),
16706            "settings capabilities leaked token environment names",
16707        )?;
16708
16709        Ok(())
16710    }
16711
16712    #[cfg(windows)]
16713    #[test]
16714    fn mcp_settings_reports_native_root_equivalence() -> Result<(), Box<dyn std::error::Error>> {
16715        let temp = tempfile::tempdir()?;
16716        let base = temp
16717            .path()
16718            .to_str()
16719            .ok_or("temporary directory was not UTF-8")?;
16720        let long_component = "a".repeat(220);
16721        let verbatim_root = PathBuf::from(format!(r"\\?\{base}\{long_component}"));
16722        fs::create_dir(&verbatim_root)?;
16723        let verbatim_database = verbatim_root.join(".projectatlas/projectatlas.db");
16724        fs::create_dir_all(
16725            verbatim_database
16726                .parent()
16727                .ok_or("verbatim MCP database has no parent")?,
16728        )?;
16729        drop(AtlasStore::open_for_project(
16730            &verbatim_database,
16731            &verbatim_root,
16732        )?);
16733        let verbatim_config = temp.path().join("mcp-verbatim-config.toml");
16734        let verbatim_text = verbatim_root
16735            .to_str()
16736            .ok_or("verbatim MCP root was not UTF-8")?;
16737        fs::write(
16738            &verbatim_config,
16739            format!(
16740                "[project]\nroot = {}\n",
16741                serde_json::to_string(verbatim_text)?
16742            ),
16743        )?;
16744        let verbatim_expected =
16745            CanonicalProjectRoot::from_path(&verbatim_root)?.display_string()?;
16746        let verbatim_server = ProjectAtlasMcpServer::new(
16747            verbatim_database.clone(),
16748            None,
16749            "mcp-verbatim-settings-test".to_string(),
16750            false,
16751        );
16752        let verbatim_state = McpProjectState {
16753            root: verbatim_root,
16754            db_path: verbatim_database,
16755            config_path: Some(verbatim_config),
16756            worktree: None,
16757        };
16758        let verbatim: serde_json::Value = toon_format::decode_default(
16759            &verbatim_server.render_settings_with_capabilities(&verbatim_state)?,
16760        )?;
16761        require(
16762            verbatim.pointer("/settings/repo_root") == Some(&serde_json::json!(verbatim_expected)),
16763            "MCP settings lost the verbatim native root projection",
16764        )?;
16765
16766        let original = temp.path().join("McpCaseRoot");
16767        let staging = temp.path().join("McpCaseRootStaging");
16768        let renamed = temp.path().join("mcpcaseroot");
16769        fs::create_dir(&original)?;
16770        let database = original.join(".projectatlas/projectatlas.db");
16771        fs::create_dir_all(
16772            database
16773                .parent()
16774                .ok_or_else(|| io::Error::other("MCP case-only database has no parent"))?,
16775        )?;
16776        drop(AtlasStore::open_for_project(&database, &original)?);
16777        fs::rename(&original, &staging)?;
16778        fs::rename(&staging, &renamed)?;
16779        let renamed_database = renamed.join(".projectatlas/projectatlas.db");
16780        let positive_config = temp.path().join("mcp-case-only-config.toml");
16781        fs::write(
16782            &positive_config,
16783            format!(
16784                "[project]\nroot = {}\n",
16785                serde_json::to_string(&renamed.to_string_lossy())?
16786            ),
16787        )?;
16788        let positive_server = ProjectAtlasMcpServer::new(
16789            renamed_database.clone(),
16790            None,
16791            "mcp-settings-test".to_string(),
16792            false,
16793        );
16794        let positive_state = McpProjectState {
16795            root: renamed,
16796            db_path: renamed_database,
16797            config_path: Some(positive_config),
16798            worktree: None,
16799        };
16800        let positive: serde_json::Value = toon_format::decode_default(
16801            &positive_server.render_settings_with_capabilities(&positive_state)?,
16802        )?;
16803        require(
16804            positive.pointer("/settings/root_verified") == Some(&serde_json::json!(true)),
16805            "MCP settings rejected case-only root rename",
16806        )?;
16807
16808        let case_sensitive_parent = temp.path().join("mcp-case-sensitive-parent");
16809        fs::create_dir(&case_sensitive_parent)?;
16810        let enabled = StdCommand::new("fsutil")
16811            .args(["file", "SetCaseSensitiveInfo"])
16812            .arg(&case_sensitive_parent)
16813            .arg("enable")
16814            .status()
16815            .is_ok_and(|status| status.success());
16816        if !enabled {
16817            return Ok(());
16818        }
16819        let stored_root = case_sensitive_parent.join("Repo");
16820        let selected_root = case_sensitive_parent.join("repo");
16821        fs::create_dir(&stored_root)?;
16822        fs::create_dir(&selected_root)?;
16823        let database = stored_root.join(".projectatlas/projectatlas.db");
16824        fs::create_dir_all(
16825            database
16826                .parent()
16827                .ok_or_else(|| io::Error::other("MCP case-sensitive database has no parent"))?,
16828        )?;
16829        drop(AtlasStore::open_for_project(&database, &stored_root)?);
16830        let negative_config = temp.path().join("mcp-case-sensitive-config.toml");
16831        fs::write(
16832            &negative_config,
16833            format!(
16834                "[project]\nroot = {}\n",
16835                serde_json::to_string(&selected_root.to_string_lossy())?
16836            ),
16837        )?;
16838        let negative_server = ProjectAtlasMcpServer::new(
16839            database.clone(),
16840            None,
16841            "mcp-settings-test".to_string(),
16842            false,
16843        );
16844        let negative_state = McpProjectState {
16845            root: selected_root,
16846            db_path: database,
16847            config_path: Some(negative_config),
16848            worktree: None,
16849        };
16850        let negative: serde_json::Value = toon_format::decode_default(
16851            &negative_server.render_settings_with_capabilities(&negative_state)?,
16852        )?;
16853        require(
16854            negative.pointer("/settings/root_verified") == Some(&serde_json::json!(false)),
16855            "MCP settings accepted a case-sensitive sibling",
16856        )?;
16857        Ok(())
16858    }
16859
16860    #[test]
16861    fn task_progress_status_and_cancel_are_typed() -> Result<(), Box<dyn std::error::Error>> {
16862        let temp = tempfile::tempdir()?;
16863        let repo = temp.path().join("repo-a");
16864        fs::create_dir(&repo)?;
16865        let db_path = repo.join(".projectatlas").join("projectatlas.db");
16866        let server = ProjectAtlasMcpServer::new(db_path, None, "mcp-test".to_string(), false);
16867
16868        let status = server.task_status(MCP_TASK_CONTRACT_ID.to_string())?;
16869        require(
16870            status.lookup == McpTaskLookupStatus::Found,
16871            "contract task missing",
16872        )?;
16873        require(
16874            status
16875                .task
16876                .as_ref()
16877                .is_some_and(|task| task.state == McpTaskState::Complete),
16878            "contract task was not complete",
16879        )?;
16880        require(
16881            status.states.contains(&McpTaskState::Pending)
16882                && status.states.contains(&McpTaskState::Canceled),
16883            "status response did not expose task state contract",
16884        )?;
16885        let cancel = server.task_cancel(MCP_TASK_CONTRACT_ID.to_string())?;
16886        require(
16887            cancel.result == McpTaskCancelResult::AlreadyFinished,
16888            "completed contract task did not return already_finished",
16889        )?;
16890        let missing = server.task_status("missing-task".to_string())?;
16891        require(
16892            missing.lookup == McpTaskLookupStatus::NotFound,
16893            "unknown task did not return not_found",
16894        )?;
16895
16896        Ok(())
16897    }
16898
16899    #[test]
16900    fn background_task_envelope_and_cancellation_reach_owned_control()
16901    -> Result<(), Box<dyn std::error::Error>> {
16902        let temp = tempfile::tempdir()?;
16903        let repo = temp.path().join("repo-a");
16904        fs::create_dir(&repo)?;
16905        let db_path = repo.join(".projectatlas").join("projectatlas.db");
16906        let mut server = ProjectAtlasMcpServer::new(db_path, None, "mcp-test".to_string(), false);
16907        let host_envelope = server.background_resources;
16908        let host_workers = thread::available_parallelism().map_or(1, usize::from);
16909        let representative_envelope = McpBackgroundResourceEnvelope::from_available_workers(8);
16910        require(
16911            host_envelope.task_limit <= MCP_BACKGROUND_TASK_SAFE_CEILING
16912                && host_envelope.workers_per_task > 0
16913                && host_envelope.total_worker_limit
16914                    <= host_workers.clamp(1, INDEX_WORKER_SAFE_CEILING)
16915                && host_envelope.workers_per_task * host_envelope.task_limit
16916                    <= host_envelope.total_worker_limit,
16917            "background resource envelope exceeded its host or process worker budget",
16918        )?;
16919        require(
16920            representative_envelope
16921                == (McpBackgroundResourceEnvelope {
16922                    task_limit: 4,
16923                    workers_per_task: 2,
16924                    total_worker_limit: 8,
16925                })
16926                && McpBackgroundResourceEnvelope::from_available_workers(0).total_worker_limit == 1
16927                && McpBackgroundResourceEnvelope::from_available_workers(usize::MAX)
16928                    .total_worker_limit
16929                    == INDEX_WORKER_SAFE_CEILING,
16930            "background resource envelope did not partition representative host capacities",
16931        )?;
16932
16933        server.background_resources = McpBackgroundResourceEnvelope::from_available_workers(4);
16934        let envelope = server.background_resources;
16935        let started = Arc::new(std::sync::Barrier::new(envelope.task_limit + 1));
16936        let databases_ready = Arc::new(std::sync::Barrier::new(envelope.task_limit + 1));
16937        let release = Arc::new(std::sync::Barrier::new(envelope.task_limit + 1));
16938        let observed_option_workers = Arc::new(AtomicU64::new(0));
16939        let observed_control_workers = Arc::new(AtomicU64::new(0));
16940        let mut concurrent_tasks = Vec::new();
16941        for task_index in 0..envelope.task_limit {
16942            let isolated_root = temp.path().join(format!("repo-{task_index}"));
16943            let isolated_db = isolated_root.join(".projectatlas").join("projectatlas.db");
16944            fs::create_dir_all(isolated_root.join(".projectatlas"))?;
16945            let worker_started = Arc::clone(&started);
16946            let worker_databases_ready = Arc::clone(&databases_ready);
16947            let worker_release = Arc::clone(&release);
16948            let worker_option_workers = Arc::clone(&observed_option_workers);
16949            let worker_control_workers = Arc::clone(&observed_control_workers);
16950            concurrent_tasks.push(server.start_index_task(
16951                McpTaskOperation::Scan,
16952                SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, None, None),
16953                MCP_TOOL_ATLAS_OVERVIEW,
16954                move |control, options| {
16955                    worker_option_workers
16956                        .fetch_add(options.reported_workers() as u64, Ordering::Relaxed);
16957                    worker_control_workers.fetch_add(
16958                        control.worker_ceiling().unwrap_or_default() as u64,
16959                        Ordering::Relaxed,
16960                    );
16961                    worker_started.wait();
16962                    let store = open_atlas_store_for_project(&isolated_db, &isolated_root);
16963                    worker_databases_ready.wait();
16964                    worker_release.wait();
16965                    let _store = store?;
16966                    Ok(())
16967                },
16968            )?);
16969        }
16970        started.wait();
16971        let admitted_workers = envelope.workers_per_task * envelope.task_limit;
16972        require(
16973            observed_option_workers.load(Ordering::Relaxed) == admitted_workers as u64
16974                && observed_control_workers.load(Ordering::Relaxed) == admitted_workers as u64
16975                && admitted_workers <= envelope.total_worker_limit,
16976            "concurrent background tasks did not share the aggregate worker envelope",
16977        )?;
16978        let overflow = server.start_index_task(
16979            McpTaskOperation::Scan,
16980            SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, None, None),
16981            MCP_TOOL_ATLAS_OVERVIEW,
16982            |_control, _options| Ok(()),
16983        );
16984        require(
16985            matches!(overflow, Err(CliError::Mcp(message)) if message.starts_with(MCP_INDEX_TASK_LIMIT_PREFIX)),
16986            "background task admission exceeded the server task limit",
16987        )?;
16988        require(
16989            server.task_status(MCP_TASK_CONTRACT_ID.to_string())?.lookup
16990                == McpTaskLookupStatus::Found,
16991            "task status became unresponsive while concurrent work was active",
16992        )?;
16993        databases_ready.wait();
16994        release.wait();
16995        for task in concurrent_tasks {
16996            require(
16997                wait_for_background_task(&server, &task.task_id)?.state == McpTaskState::Complete,
16998                "concurrent background task did not finish successfully",
16999            )?;
17000        }
17001
17002        let cancel_started = Arc::new(std::sync::Barrier::new(2));
17003        let worker_cancel_started = Arc::clone(&cancel_started);
17004        let canceled_task = server.start_index_task(
17005            McpTaskOperation::Scan,
17006            SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, None, None),
17007            MCP_TOOL_ATLAS_OVERVIEW,
17008            move |control, options| {
17009                if options.reported_workers() != control.worker_ceiling().unwrap_or_default() {
17010                    return Err(CliError::Mcp(
17011                        "background parser and operation worker ceilings diverged".to_string(),
17012                    ));
17013                }
17014                worker_cancel_started.wait();
17015                loop {
17016                    control
17017                        .check(projectatlas_core::IndexWorkStage::RepositoryTraversal)
17018                        .map_err(|failure| {
17019                            CliError::Fs(projectatlas_fs::FsError::IndexWork(failure))
17020                        })?;
17021                    thread::yield_now();
17022                }
17023            },
17024        )?;
17025        cancel_started.wait();
17026
17027        let running = server.task_status(canceled_task.task_id.clone())?;
17028        require(
17029            running
17030                .task
17031                .as_ref()
17032                .is_some_and(|record| record.state == McpTaskState::Running),
17033            "background task did not become running",
17034        )?;
17035        let cancel = server.task_cancel(canceled_task.task_id.clone())?;
17036        require(
17037            cancel.result == McpTaskCancelResult::CancellationRequested,
17038            "task cancellation did not reach the active work control",
17039        )?;
17040
17041        require(
17042            wait_for_background_task(&server, &canceled_task.task_id)?.state
17043                == McpTaskState::Canceled,
17044            "background task did not finish canceled after consuming the signal",
17045        )?;
17046        require(
17047            run_successful_background_task(&server)?.state == McpTaskState::Complete,
17048            "successful task was not admitted after cancellation",
17049        )?;
17050
17051        let failed_task = server.start_index_task(
17052            McpTaskOperation::Scan,
17053            SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, None, None),
17054            MCP_TOOL_ATLAS_OVERVIEW,
17055            |_control, _options| Err(CliError::Mcp("expected task failure".to_string())),
17056        )?;
17057        require(
17058            wait_for_background_task(&server, &failed_task.task_id)?.state == McpTaskState::Failed,
17059            "background task error did not become a terminal failure",
17060        )?;
17061        require(
17062            run_successful_background_task(&server)?.state == McpTaskState::Complete,
17063            "successful task was not admitted after failure",
17064        )?;
17065
17066        let panicked_task = server.start_index_task(
17067            McpTaskOperation::Scan,
17068            SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, None, None),
17069            MCP_TOOL_ATLAS_OVERVIEW,
17070            |_control, _options| -> Result<(), CliError> {
17071                std::panic::resume_unwind(Box::new("expected background task panic"));
17072            },
17073        )?;
17074        let panicked = wait_for_background_task(&server, &panicked_task.task_id)?;
17075        require(
17076            panicked.state == McpTaskState::Failed
17077                && panicked.error.as_deref() == Some(MCP_INDEX_WORKER_PANIC_ERROR),
17078            "background task panic did not remain bounded and terminal",
17079        )?;
17080        require(
17081            run_successful_background_task(&server)?.state == McpTaskState::Complete,
17082            "terminal task lifecycle did not release background admission capacity",
17083        )?;
17084
17085        Ok(())
17086    }
17087
17088    #[test]
17089    fn background_scan_defers_config_validation_and_rejects_root_redirection()
17090    -> Result<(), Box<dyn std::error::Error>> {
17091        let temp = tempfile::tempdir()?;
17092        let repo = temp.path().join("repo-a");
17093        let redirected = temp.path().join("repo-b");
17094        let atlas_dir = repo.join(".projectatlas");
17095        fs::create_dir_all(&atlas_dir)?;
17096        fs::create_dir_all(&redirected)?;
17097        let config_path = atlas_dir.join("config.toml");
17098        fs::write(&config_path, "[project]\nroot = \"../../repo-b\"\n")?;
17099        let db_path = atlas_dir.join("projectatlas.db");
17100        let server =
17101            ProjectAtlasMcpServer::new(db_path, Some(config_path), "mcp-test".to_string(), false);
17102
17103        let response = server.atlas_scan(Parameters(AtlasScanParams {
17104            project_path: Some(repo.to_string_lossy().into_owned()),
17105            worktree: None,
17106            path: None,
17107            nearest_project: Some(false),
17108            max_bytes: None,
17109            max_workers: Some(1),
17110            timeout_seconds: None,
17111            text_index_max_bytes: None,
17112            background: Some(true),
17113        }));
17114        require(
17115            response.contains(MCP_PAYLOAD_TASK_START),
17116            "explicit background project validated redirecting config before task admission",
17117        )?;
17118        let admitted_task_id = server
17119            .task_registry
17120            .read()
17121            .map_err(|_poisoned| io::Error::other("task registry lock poisoned"))?
17122            .latest_task_id(&McpTaskOperation::Scan)
17123            .ok_or_else(|| io::Error::other("admitted background scan task missing"))?;
17124        let mut admitted_terminal = None;
17125        for _attempt in 0..1_000 {
17126            let status = server.task_status(admitted_task_id.clone())?;
17127            if status
17128                .task
17129                .as_ref()
17130                .is_some_and(McpTaskRecord::is_terminal_state)
17131            {
17132                admitted_terminal = status.task;
17133                break;
17134            }
17135            thread::sleep(Duration::from_millis(1));
17136        }
17137        require(
17138            admitted_terminal
17139                .as_ref()
17140                .is_some_and(|record| record.state == McpTaskState::Failed),
17141            "redirecting background config did not fail inside the admitted task",
17142        )?;
17143        require(
17144            admitted_terminal
17145                .as_ref()
17146                .and_then(|record| record.error.as_deref())
17147                .is_some_and(|error| error.contains("outside selected project root")),
17148            "controlled plan loading did not preserve root-redirection refusal",
17149        )?;
17150
17151        Ok(())
17152    }
17153
17154    #[test]
17155    fn index_adapters_publish_scan_symbol_and_watch_effects()
17156    -> Result<(), Box<dyn std::error::Error>> {
17157        let temp = tempfile::tempdir()?;
17158        let repo = temp.path().join("repo");
17159        let source_dir = repo.join("src");
17160        fs::create_dir_all(&source_dir)?;
17161        let source_path = source_dir.join("lib.rs");
17162        fs::write(
17163            &source_path,
17164            "pub fn first() { second(); }\nfn second() {}\n",
17165        )?;
17166        let config_path = repo.join(".projectatlas").join("config.toml");
17167        init_project_with_config(&repo, Some(&config_path))?;
17168        let db_path = repo.join(".projectatlas").join("projectatlas.db");
17169        let server =
17170            ProjectAtlasMcpServer::new(db_path.clone(), None, "mcp-test".to_string(), false);
17171        let project_path = repo.to_string_lossy().into_owned();
17172
17173        for operation in [
17174            McpTaskOperation::Scan,
17175            McpTaskOperation::SymbolsBuild,
17176            McpTaskOperation::WatchOnce,
17177        ] {
17178            match operation {
17179                McpTaskOperation::SymbolsBuild => {
17180                    let store = open_atlas_store_for_project(&db_path, &repo)?;
17181                    store.clear_symbol_graph_for_path("src/lib.rs")?;
17182                    require(
17183                        store.symbol_count_for_path("src/lib.rs")? == 0,
17184                        "symbol fixture was not cleared before background rebuild",
17185                    )?;
17186                }
17187                McpTaskOperation::WatchOnce => {
17188                    fs::write(
17189                        &source_path,
17190                        "pub fn first() { second(); third(); }\nfn second() {}\nfn third() {}\n",
17191                    )?;
17192                }
17193                McpTaskOperation::Scan | McpTaskOperation::Contract | McpTaskOperation::Search => {}
17194            }
17195
17196            let response = match operation {
17197                McpTaskOperation::Scan | McpTaskOperation::SymbolsBuild => {
17198                    let params = AtlasScanParams {
17199                        project_path: Some(project_path.clone()),
17200                        worktree: None,
17201                        path: None,
17202                        nearest_project: Some(false),
17203                        max_bytes: None,
17204                        max_workers: Some(1),
17205                        timeout_seconds: None,
17206                        text_index_max_bytes: None,
17207                        background: Some(true),
17208                    };
17209                    if operation == McpTaskOperation::Scan {
17210                        server.atlas_scan(Parameters(params))
17211                    } else {
17212                        server.atlas_symbols_build(Parameters(params))
17213                    }
17214                }
17215                McpTaskOperation::WatchOnce => {
17216                    server.atlas_watch_once(Parameters(AtlasWatchOnceParams {
17217                        project_path: Some(project_path.clone()),
17218                        worktree: None,
17219                        path: None,
17220                        nearest_project: Some(false),
17221                        max_workers: Some(1),
17222                        timeout_seconds: None,
17223                        text_index_max_bytes: None,
17224                        background: Some(true),
17225                    }))
17226                }
17227                McpTaskOperation::Contract | McpTaskOperation::Search => unreachable!(),
17228            };
17229            require(
17230                response.contains(MCP_PAYLOAD_TASK_START),
17231                "production background adapter did not admit its task",
17232            )?;
17233            let terminal = wait_for_background_operation(&server, &operation)?;
17234            require(
17235                terminal.state == McpTaskState::Complete,
17236                terminal
17237                    .error
17238                    .as_deref()
17239                    .unwrap_or("background task failed"),
17240            )?;
17241
17242            let expected_symbol = match operation {
17243                McpTaskOperation::Scan | McpTaskOperation::SymbolsBuild => "second",
17244                McpTaskOperation::WatchOnce => "third",
17245                McpTaskOperation::Contract | McpTaskOperation::Search => unreachable!(),
17246            };
17247            let store = open_atlas_store_for_project(&db_path, &repo)?;
17248            store.set_purpose(
17249                "src/lib.rs",
17250                "Own café λ relation navigation",
17251                PurposeSource::Agent,
17252            )?;
17253            drop(store);
17254            require_agent_index_reads(&server, &project_path, expected_symbol)?;
17255        }
17256
17257        let store = open_atlas_store_for_project(&db_path, &repo)?;
17258        store.clear_symbol_graph_for_path("src/lib.rs")?;
17259        drop(store);
17260        let synchronous_symbols = server.atlas_symbols_build(Parameters(AtlasScanParams {
17261            project_path: Some(project_path.clone()),
17262            worktree: None,
17263            path: None,
17264            nearest_project: Some(false),
17265            max_bytes: None,
17266            max_workers: Some(1),
17267            timeout_seconds: None,
17268            text_index_max_bytes: None,
17269            background: Some(false),
17270        }));
17271        require(
17272            synchronous_symbols.contains(MCP_PAYLOAD_SYMBOLS_BUILD),
17273            "synchronous symbol adapter did not return its completed report",
17274        )?;
17275        require_agent_index_reads(&server, &project_path, "second")?;
17276
17277        fs::write(
17278            &source_path,
17279            "pub fn first() { second(); fourth(); }\nfn second() {}\nfn fourth() {}\n",
17280        )?;
17281        let synchronous_watch = server.atlas_watch_once(Parameters(AtlasWatchOnceParams {
17282            project_path: Some(project_path.clone()),
17283            worktree: None,
17284            path: None,
17285            nearest_project: Some(false),
17286            max_workers: Some(1),
17287            timeout_seconds: None,
17288            text_index_max_bytes: None,
17289            background: Some(false),
17290        }));
17291        require(
17292            synchronous_watch.contains(MCP_PAYLOAD_WATCH),
17293            "synchronous watch adapter did not return its completed report",
17294        )?;
17295        require_agent_index_reads(&server, &project_path, "fourth")?;
17296        Ok(())
17297    }
17298
17299    #[test]
17300    fn long_lived_mcp_query_families_reuse_one_verified_epoch()
17301    -> Result<(), Box<dyn std::error::Error>> {
17302        const LARGE_UNRELATED_SOURCE_FILES: usize = 256;
17303        const MAX_MEASURED_QUERY_OUTPUT_BYTES: usize = 64 * 1_024;
17304        const MAX_MEASURED_QUERY_ELAPSED: Duration = Duration::from_secs(30);
17305
17306        let measure = |unrelated_source_files: usize| {
17307            let temp = tempfile::tempdir()?;
17308            let repo = temp.path().join("repo");
17309            let source_dir = repo.join("src");
17310            fs::create_dir_all(&source_dir)?;
17311            fs::write(
17312                source_dir.join("lib.rs"),
17313                "pub fn first() { second(); }\nfn second() {}\n",
17314            )?;
17315            for index in 0..unrelated_source_files {
17316                fs::write(
17317                    source_dir.join(format!("unrelated_{index:03}.rs")),
17318                    format!("pub fn unrelated_{index:03}() {{}}\n"),
17319                )?;
17320            }
17321            let config_path = repo.join(".projectatlas").join("config.toml");
17322            init_project_with_config(&repo, Some(&config_path))?;
17323            let db_path = repo.join(".projectatlas").join("projectatlas.db");
17324            let mut writer = open_atlas_store_for_project(&db_path, &repo)?;
17325            let plan = ScanRuntimePlan::for_path(None, &repo, None)?;
17326            run_scan_pipeline(
17327                &mut writer,
17328                &plan,
17329                &SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, Some(1), None),
17330            )?;
17331            drop(writer);
17332
17333            let server = ProjectAtlasMcpServer::new(
17334                db_path.clone(),
17335                None,
17336                "verified-epoch-test".to_string(),
17337                false,
17338            );
17339            let state = McpProjectState {
17340                root: repo,
17341                db_path,
17342                config_path: None,
17343                worktree: None,
17344            };
17345            let first = server.with_fresh_store(&state, |store, _stamp| Ok(store.overview()?))?;
17346            require(
17347                first.work.exact_verifications >= 1,
17348                "first long-lived MCP read did not establish exact source truth",
17349            )?;
17350            require(
17351                first.work.filesystem_entries > u64::try_from(unrelated_source_files)?,
17352                "scale fixture did not exercise its complete repository source set",
17353            )?;
17354            let expected_stamp = first.stamp;
17355
17356            let folder = server.with_fresh_store(&state, |store, _stamp| {
17357                Ok(render_ranked_nodes(
17358                    NODE_LABEL_FOLDERS,
17359                    &ranked_folder_nodes_with_reasons(store, "", 4)?,
17360                ))
17361            })?;
17362            let folder_output_bytes = folder.value.len();
17363            let folder = folder.with_output_bytes(folder_output_bytes);
17364            let files = server.with_fresh_store(&state, |store, _stamp| {
17365                Ok(render_ranked_nodes(
17366                    NODE_LABEL_FILES,
17367                    &ranked_file_nodes_with_reasons(store, "", Some("src"), None, 4, false)?,
17368                ))
17369            })?;
17370            let files_output_bytes = files.value.len();
17371            let files = files.with_output_bytes(files_output_bytes);
17372            let summary = server.with_fresh_store(&state, |store, _stamp| {
17373                let content = read_indexed_file_content(store, "src/lib.rs")?;
17374                let report = build_file_summary_from_source(
17375                    store,
17376                    Path::new("src/lib.rs"),
17377                    DEFAULT_FILE_SUMMARY_LIMIT,
17378                    &content,
17379                )?;
17380                Ok(render_file_summary(&report))
17381            })?;
17382            let summary_output_bytes = summary.value.len();
17383            let summary = summary.with_output_bytes(summary_output_bytes);
17384            let relations = server.with_fresh_store(&state, |store, _stamp| {
17385                Ok(render_symbol_relations(&store.load_symbol_relations(
17386                    Some("src/lib.rs"),
17387                    None,
17388                    8,
17389                )?))
17390            })?;
17391            let relation_output_bytes = relations.value.len();
17392            let relations = relations.with_output_bytes(relation_output_bytes);
17393
17394            let mut measurements = Vec::new();
17395            for (family, outcome) in [
17396                ("folder", folder),
17397                ("file", files),
17398                ("summary", summary),
17399                ("relation", relations),
17400            ] {
17401                require(
17402                    outcome.stamp == expected_stamp,
17403                    &format!("{family} call did not remain bound to the verified epoch"),
17404                )?;
17405                require(
17406                    outcome.work.exact_verifications == 0
17407                        && outcome.work.filesystem_entries == 0
17408                        && outcome.work.filesystem_bytes == 0
17409                        && outcome.work.decoded_nodes == 0,
17410                    &format!("{family} call repeated repository-sized freshness work"),
17411                )?;
17412                require(
17413                    outcome.work.sqlite_read_statements == 1,
17414                    &format!("{family} freshness check used unexpected SQLite work"),
17415                )?;
17416                require(
17417                    outcome.work.output_bytes == u64::try_from(outcome.value.len())?,
17418                    &format!("{family} call did not record its accepted rendered bytes"),
17419                )?;
17420                require(
17421                    outcome.value.len() <= MAX_MEASURED_QUERY_OUTPUT_BYTES,
17422                    &format!("{family} call exceeded the focused output bound"),
17423                )?;
17424                require(
17425                    !outcome.work.elapsed.is_zero()
17426                        && outcome.work.elapsed <= MAX_MEASURED_QUERY_ELAPSED,
17427                    &format!("{family} call did not retain a bounded elapsed measurement"),
17428                )?;
17429                measurements.push((family, outcome.work));
17430            }
17431            Ok::<_, Box<dyn std::error::Error>>(measurements)
17432        };
17433
17434        let small = measure(0)?;
17435        let large = measure(LARGE_UNRELATED_SOURCE_FILES)?;
17436        for ((small_family, small_work), (large_family, large_work)) in small.into_iter().zip(large)
17437        {
17438            require(
17439                small_family == large_family,
17440                "small and large query measurements used different families",
17441            )?;
17442            require(
17443                small_work.exact_verifications == large_work.exact_verifications
17444                    && small_work.filesystem_entries == large_work.filesystem_entries
17445                    && small_work.filesystem_bytes == large_work.filesystem_bytes
17446                    && small_work.sqlite_read_statements == large_work.sqlite_read_statements
17447                    && small_work.decoded_nodes == large_work.decoded_nodes,
17448                &format!("{small_family} warm freshness work changed with repository scale"),
17449            )?;
17450        }
17451        Ok(())
17452    }
17453
17454    #[test]
17455    fn selected_root_absolute_path_keys_stay_inside_selected_project()
17456    -> Result<(), Box<dyn std::error::Error>> {
17457        let temp = tempfile::tempdir()?;
17458        let repo = temp.path().join("repo");
17459        let outside = temp.path().join("outside");
17460        fs::create_dir_all(repo.join("src"))?;
17461        fs::create_dir_all(outside.join("src"))?;
17462        let inside_file = repo.join("src").join("lib.rs");
17463        let outside_file = outside.join("src").join("lib.rs");
17464        fs::write(&inside_file, "pub fn inside() {}\n")?;
17465        fs::write(&outside_file, "pub fn outside() {}\n")?;
17466        let state = McpProjectState {
17467            root: canonical_project_root(&repo)?,
17468            db_path: repo.join(".projectatlas").join("projectatlas.db"),
17469            config_path: None,
17470            worktree: None,
17471        };
17472
17473        let inside_key =
17474            ProjectAtlasMcpServer::absolute_path_key_in_selected_project(&state, &inside_file)?
17475                .ok_or_else(|| io::Error::other("inside selected root did not produce key"))?;
17476        require(
17477            inside_key == "src/lib.rs",
17478            "inside selected root produced wrong repo key",
17479        )?;
17480        require(
17481            ProjectAtlasMcpServer::absolute_path_key_in_selected_project(&state, &outside_file)?
17482                .is_none(),
17483            "outside selected root produced a repo key",
17484        )?;
17485
17486        Ok(())
17487    }
17488
17489    #[test]
17490    fn indexed_root_candidate_requires_matching_project_root()
17491    -> Result<(), Box<dyn std::error::Error>> {
17492        let temp = tempfile::tempdir()?;
17493        let repo = temp.path().join("repo");
17494        let other = temp.path().join("other");
17495        fs::create_dir_all(repo.join(".projectatlas"))?;
17496        fs::create_dir_all(&other)?;
17497
17498        require(
17499            ProjectAtlasMcpServer::indexed_root_from_candidate(&repo).is_none(),
17500            "candidate without DB was treated as indexed",
17501        )?;
17502
17503        let db_path = repo.join(".projectatlas").join("projectatlas.db");
17504        {
17505            let _store = open_atlas_store_for_project(&db_path, &other)?;
17506        }
17507        require(
17508            ProjectAtlasMcpServer::indexed_root_from_candidate(&repo).is_none(),
17509            "candidate with mismatched DB root was treated as indexed",
17510        )?;
17511
17512        reset_index_files(&db_path, true, false, false)?;
17513        {
17514            let _store = open_atlas_store_for_project(&db_path, &repo)?;
17515        }
17516        let indexed = ProjectAtlasMcpServer::indexed_root_from_candidate(&repo)
17517            .ok_or_else(|| io::Error::other("matching DB root was not accepted"))?;
17518        require(
17519            indexed.root == canonical_project_root(&repo)?,
17520            "indexed root did not preserve canonical candidate root",
17521        )?;
17522        let expected_db_path =
17523            ProjectAtlasMcpServer::projectatlas_db_path(&canonical_project_root(&repo)?);
17524        require(
17525            indexed.db_path == expected_db_path,
17526            "indexed root changed DB path",
17527        )?;
17528
17529        Ok(())
17530    }
17531
17532    #[cfg(windows)]
17533    #[test]
17534    fn indexed_root_predecessor_candidate_supports_nearest_routing()
17535    -> Result<(), Box<dyn std::error::Error>> {
17536        let temp = tempfile::tempdir()?;
17537        let root = temp.path().join("predecessor");
17538        let source = root.join("src").join("lib.rs");
17539        let database = root.join(".projectatlas").join("projectatlas.db");
17540        fs::create_dir_all(
17541            source
17542                .parent()
17543                .ok_or_else(|| io::Error::other("predecessor source path has no parent"))?,
17544        )?;
17545        fs::create_dir_all(
17546            database
17547                .parent()
17548                .ok_or_else(|| io::Error::other("predecessor database path has no parent"))?,
17549        )?;
17550        fs::write(&source, "pub fn predecessor() {}\n")?;
17551        let store = open_atlas_store_for_project(&database, &root)?;
17552        drop(store);
17553        let predecessor = rusqlite::Connection::open(&database)?;
17554        drop_native_worktree_identity_schema(&predecessor)?;
17555        predecessor.execute_batch(
17556            "DROP TABLE project_root_identity;
17557             DROP TABLE IF EXISTS graph_identity_rejections;
17558             UPDATE metadata SET value = '19' WHERE key = 'schema_version';",
17559        )?;
17560        drop(predecessor);
17561
17562        let expected_root = canonical_project_root(&root)?;
17563        let expected_database = ProjectAtlasMcpServer::projectatlas_db_path(&expected_root);
17564        let canonical = ProjectAtlasMcpServer::project_state_from_nearest_indexed_path(&source)?
17565            .ok_or_else(|| io::Error::other("canonical nearest predecessor was not found"))?;
17566        let lexical =
17567            ProjectAtlasMcpServer::project_state_from_nearest_lexical_indexed_path(&source)?
17568                .ok_or_else(|| io::Error::other("lexical nearest predecessor was not found"))?;
17569        for state in [canonical, lexical] {
17570            require(
17571                state.root == expected_root && state.db_path == expected_database,
17572                "nearest predecessor routing changed the canonical root or database path",
17573            )?;
17574        }
17575        Ok(())
17576    }
17577
17578    #[cfg(windows)]
17579    #[test]
17580    fn nearest_routing_recanonicalizes_case_only_root_rename()
17581    -> Result<(), Box<dyn std::error::Error>> {
17582        let temp = tempfile::tempdir()?;
17583        let original = temp.path().join("NearestCaseRoot");
17584        let staging = temp.path().join("NearestCaseRootStaging");
17585        let renamed = temp.path().join("nearestcaseroot");
17586        let source = original.join("src").join("lib.rs");
17587        fs::create_dir_all(
17588            source
17589                .parent()
17590                .ok_or_else(|| io::Error::other("case-only nearest source has no parent"))?,
17591        )?;
17592        let database = original.join(".projectatlas").join("projectatlas.db");
17593        fs::create_dir_all(
17594            database
17595                .parent()
17596                .ok_or_else(|| io::Error::other("case-only nearest database has no parent"))?,
17597        )?;
17598        fs::write(&source, "pub fn nearest_case_only() {}\n")?;
17599        drop(open_atlas_store_for_project(&database, &original)?);
17600
17601        fs::rename(&original, &staging)?;
17602        fs::rename(&staging, &renamed)?;
17603        let renamed_source = renamed.join("src").join("lib.rs");
17604        let expected_root = canonical_project_root(&renamed)?;
17605        let expected_database = ProjectAtlasMcpServer::projectatlas_db_path(&expected_root);
17606        let canonical =
17607            ProjectAtlasMcpServer::project_state_from_nearest_indexed_path(&renamed_source)?
17608                .ok_or_else(|| {
17609                    io::Error::other("canonical nearest case-only root was not found")
17610                })?;
17611        let lexical = ProjectAtlasMcpServer::project_state_from_nearest_lexical_indexed_path(
17612            &renamed_source,
17613        )?
17614        .ok_or_else(|| io::Error::other("lexical nearest case-only root was not found"))?;
17615        for state in [canonical, lexical] {
17616            require(
17617                state.root == expected_root && state.db_path == expected_database,
17618                "nearest routing rejected a case-only root rename",
17619            )?;
17620        }
17621        Ok(())
17622    }
17623
17624    #[cfg(windows)]
17625    #[test]
17626    fn nearest_routing_rejects_case_sensitive_sibling() -> Result<(), Box<dyn std::error::Error>> {
17627        let temp = tempfile::tempdir()?;
17628        let parent = temp.path().join("nearest-case-sensitive-parent");
17629        fs::create_dir(&parent)?;
17630        let enabled = StdCommand::new("fsutil")
17631            .args(["file", "SetCaseSensitiveInfo"])
17632            .arg(&parent)
17633            .arg("enable")
17634            .status()
17635            .is_ok_and(|status| status.success());
17636        if !enabled {
17637            return Ok(());
17638        }
17639
17640        let stored_root = parent.join("Repo");
17641        let selected_root = parent.join("repo");
17642        fs::create_dir(&stored_root)?;
17643        fs::create_dir(&selected_root)?;
17644        let stored_database = stored_root.join(".projectatlas").join("projectatlas.db");
17645        let selected_database = selected_root.join(".projectatlas").join("projectatlas.db");
17646        fs::create_dir_all(
17647            stored_database
17648                .parent()
17649                .ok_or_else(|| io::Error::other("stored nearest database has no parent"))?,
17650        )?;
17651        fs::create_dir_all(
17652            selected_database
17653                .parent()
17654                .ok_or_else(|| io::Error::other("selected nearest database has no parent"))?,
17655        )?;
17656        drop(open_atlas_store_for_project(
17657            &stored_database,
17658            &stored_root,
17659        )?);
17660        fs::copy(&stored_database, &selected_database)?;
17661
17662        require(
17663            ProjectAtlasMcpServer::indexed_root_from_candidate(&selected_root).is_none()
17664                && ProjectAtlasMcpServer::indexed_root_from_lexical_candidate(&selected_root)
17665                    .is_none(),
17666            "nearest routing accepted a distinct case-sensitive sibling",
17667        )?;
17668        Ok(())
17669    }
17670
17671    #[cfg(unix)]
17672    #[test]
17673    fn mcp_init_rejects_ambiguous_predecessor_before_project_writes()
17674    -> Result<(), Box<dyn std::error::Error>> {
17675        use std::ffi::OsString;
17676        use std::os::unix::ffi::OsStringExt;
17677
17678        let temp = tempfile::tempdir()?;
17679        let raw_root = temp
17680            .path()
17681            .join(OsString::from_vec(b"mcp-raw-root-\x80".to_vec()));
17682        let replacement_root = PathBuf::from(raw_root.to_string_lossy().into_owned());
17683        let database = raw_root
17684            .join(PROJECTATLAS_DIR_NAME)
17685            .join(PROJECTATLAS_DB_FILE_NAME);
17686        fs::create_dir_all(&raw_root)?;
17687        fs::create_dir_all(
17688            database
17689                .parent()
17690                .ok_or_else(|| io::Error::other("predecessor database has no parent"))?,
17691        )?;
17692        let store = AtlasStore::open_for_project(&database, &raw_root)?;
17693        drop(store);
17694        let predecessor = rusqlite::Connection::open(&database)?;
17695        drop_native_worktree_identity_schema(&predecessor)?;
17696        predecessor.execute_batch(
17697            "DROP TABLE project_root_identity;
17698             DROP TABLE IF EXISTS graph_identity_rejections;
17699             UPDATE metadata SET value = '19' WHERE key = 'schema_version';",
17700        )?;
17701        predecessor.execute(
17702            "INSERT INTO metadata(key, value) VALUES('project_root', ?1)
17703             ON CONFLICT(key) DO UPDATE SET value = excluded.value",
17704            [&replacement_root.to_string_lossy().into_owned()],
17705        )?;
17706        drop(predecessor);
17707        fs::create_dir_all(&replacement_root)?;
17708
17709        // The startup root resolver opens the predecessor read-only. SQLite
17710        // may materialize WAL sidecars for that read path, so warm the exact
17711        // recovery route before capturing the no-mutation baseline. An
17712        // ambiguous legacy root must be refused rather than selected.
17713        require(
17714            default_mcp_project_root(&database, None).is_err(),
17715            "ambiguous predecessor was selected during startup discovery",
17716        )?;
17717        let database_before = fs::read(&database)?;
17718        let sidecars_before = ["wal", "shm", "journal"]
17719            .map(|suffix| fs::read(db_sidecar_path(&database, suffix)).ok());
17720        let raw_project_dir = raw_root.join(PROJECTATLAS_DIR_NAME);
17721        let raw_config = raw_project_dir.join(PROJECTATLAS_CONFIG_FILE_NAME);
17722        let raw_nonsource = raw_project_dir.join(MCP_NONSOURCE_FILE_NAME);
17723        let replacement_project_dir = replacement_root.join(PROJECTATLAS_DIR_NAME);
17724        let replacement_config = replacement_project_dir.join(PROJECTATLAS_CONFIG_FILE_NAME);
17725        let replacement_nonsource = replacement_project_dir.join(MCP_NONSOURCE_FILE_NAME);
17726        let server = ProjectAtlasMcpServer::new(
17727            database.clone(),
17728            None,
17729            "ambiguous-predecessor".to_string(),
17730            false,
17731        );
17732        let startup = ProjectAtlasMcpServer::startup_project_state(database.clone(), None);
17733        require(
17734            startup.root == canonical_project_root(&raw_root)?,
17735            "MCP startup did not select the raw native predecessor root",
17736        )?;
17737        require(
17738            startup.root != canonical_project_root(&replacement_root)?,
17739            "MCP startup selected the replacement-character candidate",
17740        )?;
17741        let result = server.atlas_init(Parameters(AtlasInitParams {
17742            project_path: None,
17743            worktree: None,
17744            no_scan: Some(true),
17745            force_rescan: Some(false),
17746            text_index_max_bytes: None,
17747        }));
17748        require(
17749            result.contains("project-root identity"),
17750            &format!("ambiguous predecessor init returned an unexpected result: {result}"),
17751        )?;
17752        require(
17753            fs::read(&database)? == database_before
17754                && ["wal", "shm", "journal"]
17755                    .map(|suffix| fs::read(db_sidecar_path(&database, suffix)).ok())
17756                    == sidecars_before,
17757            "ambiguous predecessor init changed database or sidecar state",
17758        )?;
17759        require(
17760            !replacement_project_dir.exists()
17761                && !replacement_config.exists()
17762                && !replacement_nonsource.exists(),
17763            "ambiguous predecessor init wrote replacement-root project state",
17764        )?;
17765        require(
17766            !raw_config.exists() && !raw_nonsource.exists(),
17767            "ambiguous predecessor init wrote raw-root project state",
17768        )?;
17769        Ok(())
17770    }
17771
17772    #[test]
17773    fn mcp_init_rejects_current_wrong_root_before_project_writes()
17774    -> Result<(), Box<dyn std::error::Error>> {
17775        let temp = tempfile::tempdir()?;
17776        let selected_root = temp.path().join("selected-root");
17777        let bound_root = temp.path().join("bound-root");
17778        let database = temp.path().join("external-projectatlas.db");
17779        let config_path = selected_root.join("external-config/config.toml");
17780        fs::create_dir_all(&selected_root)?;
17781        fs::create_dir_all(&bound_root)?;
17782        let persisted_identity = {
17783            let store = AtlasStore::open_for_project(&database, &bound_root)?;
17784            store.project_root_identity()?
17785        };
17786        drop(AtlasStore::open_read_only_for_project(
17787            &database,
17788            &bound_root,
17789        )?);
17790        let database_before = fs::read(&database)?;
17791        let sidecars_before = ["wal", "shm", "journal"]
17792            .map(|suffix| fs::read(crate::runtime::db_sidecar_path(&database, suffix)).ok());
17793        let selected_project_dir = selected_root.join(PROJECTATLAS_DIR_NAME);
17794        let config_parent = config_path
17795            .parent()
17796            .ok_or_else(|| io::Error::other("selected config has no parent"))?;
17797        let server = ProjectAtlasMcpServer::new(
17798            database.clone(),
17799            Some(config_path.clone()),
17800            "current-wrong-root".to_string(),
17801            false,
17802        );
17803        *server
17804            .project_state
17805            .write()
17806            .map_err(|_poisoned| io::Error::other("MCP project state lock poisoned"))? =
17807            McpProjectState {
17808                root: selected_root,
17809                db_path: database.clone(),
17810                config_path: Some(config_path.clone()),
17811                worktree: None,
17812            };
17813
17814        let result = server.atlas_init(Parameters(AtlasInitParams {
17815            project_path: None,
17816            worktree: None,
17817            no_scan: Some(true),
17818            force_rescan: Some(false),
17819            text_index_max_bytes: None,
17820        }));
17821        require(
17822            result.contains("does not match selected root"),
17823            &format!("current wrong-root MCP init returned an unexpected result: {result}"),
17824        )?;
17825        require(
17826            !selected_project_dir.exists() && !config_parent.exists() && !config_path.exists(),
17827            "current wrong-root MCP init created selected project or config state",
17828        )?;
17829        require(
17830            fs::read(&database)? == database_before
17831                && ["wal", "shm", "journal"].map(|suffix| {
17832                    fs::read(crate::runtime::db_sidecar_path(&database, suffix)).ok()
17833                }) == sidecars_before,
17834            "current wrong-root MCP init changed database or sidecar state",
17835        )?;
17836        let reopened = AtlasStore::open_read_only_for_project(&database, &bound_root)?;
17837        require(
17838            reopened.project_root_identity()? == persisted_identity,
17839            "current binding changed after wrong-root MCP init",
17840        )?;
17841        Ok(())
17842    }
17843
17844    #[cfg(windows)]
17845    #[test]
17846    fn mcp_init_rejects_predecessor_wrong_root_before_project_writes()
17847    -> Result<(), Box<dyn std::error::Error>> {
17848        let temp = tempfile::tempdir()?;
17849        let selected_root = temp.path().join("selected-predecessor-root");
17850        let bound_root = temp.path().join("bound-predecessor-root");
17851        let database = temp.path().join("external-predecessor.db");
17852        let config_path = selected_root.join("external-config/config.toml");
17853        fs::create_dir_all(&selected_root)?;
17854        fs::create_dir_all(&bound_root)?;
17855        drop(AtlasStore::open_for_project(&database, &bound_root)?);
17856        {
17857            let connection = rusqlite::Connection::open(&database)?;
17858            drop_native_worktree_identity_schema(&connection)?;
17859            connection.execute_batch(
17860                "DROP TABLE project_root_identity;
17861                 DROP TABLE IF EXISTS graph_identity_rejections;
17862                 UPDATE metadata SET value = '19' WHERE key = 'schema_version';",
17863            )?;
17864        }
17865        read_legacy_project_root_candidate_read_only(&database)?;
17866        let database_before = fs::read(&database)?;
17867        let sidecars_before = ["wal", "shm", "journal"]
17868            .map(|suffix| fs::read(crate::runtime::db_sidecar_path(&database, suffix)).ok());
17869        let selected_project_dir = selected_root.join(PROJECTATLAS_DIR_NAME);
17870        let config_parent = config_path
17871            .parent()
17872            .ok_or_else(|| io::Error::other("selected predecessor config has no parent"))?;
17873        let server = ProjectAtlasMcpServer::new(
17874            database.clone(),
17875            Some(config_path.clone()),
17876            "predecessor-wrong-root".to_string(),
17877            false,
17878        );
17879        *server
17880            .project_state
17881            .write()
17882            .map_err(|_poisoned| io::Error::other("MCP project state lock poisoned"))? =
17883            McpProjectState {
17884                root: selected_root,
17885                db_path: database.clone(),
17886                config_path: Some(config_path.clone()),
17887                worktree: None,
17888            };
17889
17890        let result = server.atlas_init(Parameters(AtlasInitParams {
17891            project_path: None,
17892            worktree: None,
17893            no_scan: Some(true),
17894            force_rescan: Some(false),
17895            text_index_max_bytes: None,
17896        }));
17897        require(
17898            result.contains("does not match selected root"),
17899            &format!("predecessor wrong-root MCP init returned an unexpected result: {result}"),
17900        )?;
17901        require(
17902            !selected_project_dir.exists() && !config_parent.exists() && !config_path.exists(),
17903            "predecessor wrong-root MCP init created selected project or config state",
17904        )?;
17905        require(
17906            fs::read(&database)? == database_before
17907                && ["wal", "shm", "journal"].map(|suffix| {
17908                    fs::read(crate::runtime::db_sidecar_path(&database, suffix)).ok()
17909                }) == sidecars_before,
17910            "predecessor wrong-root MCP init changed database or sidecar state",
17911        )?;
17912        Ok(())
17913    }
17914
17915    #[cfg(unix)]
17916    #[test]
17917    fn nearest_root_rejects_ambiguous_predecessor_without_mutation()
17918    -> Result<(), Box<dyn std::error::Error>> {
17919        fn sidecar_bytes(database: &Path) -> [Option<Vec<u8>>; 3] {
17920            ["wal", "shm", "journal"].map(|suffix| fs::read(db_sidecar_path(database, suffix)).ok())
17921        }
17922
17923        fn directory_inventory(path: &Path) -> Result<Vec<String>, Box<dyn std::error::Error>> {
17924            let mut names = fs::read_dir(path)?
17925                .map(|entry| entry.map(|entry| entry.file_name().to_string_lossy().into_owned()))
17926                .collect::<Result<Vec<_>, _>>()?;
17927            names.sort();
17928            Ok(names)
17929        }
17930
17931        fn snapshot(
17932            database: &Path,
17933        ) -> Result<(Vec<u8>, [Option<Vec<u8>>; 3], Vec<String>), Box<dyn std::error::Error>>
17934        {
17935            let parent = database
17936                .parent()
17937                .ok_or_else(|| io::Error::other("predecessor database has no parent"))?;
17938            Ok((
17939                fs::read(database)?,
17940                sidecar_bytes(database),
17941                directory_inventory(parent)?,
17942            ))
17943        }
17944
17945        let temp = tempfile::tempdir()?;
17946        let raw_root = temp
17947            .path()
17948            .join(std::ffi::OsString::from_vec(b"nearest-raw-\x80".to_vec()));
17949        let replacement_root = temp.path().join("nearest-raw-�");
17950        let raw_database = raw_root.join(".projectatlas").join("projectatlas.db");
17951        let replacement_database = replacement_root
17952            .join(".projectatlas")
17953            .join("projectatlas.db");
17954        fs::create_dir_all(
17955            raw_database
17956                .parent()
17957                .ok_or_else(|| io::Error::other("raw predecessor database has no parent"))?,
17958        )?;
17959        fs::create_dir_all(
17960            replacement_database.parent().ok_or_else(|| {
17961                io::Error::other("replacement predecessor database has no parent")
17962            })?,
17963        )?;
17964        let store = open_atlas_store_for_project(&raw_database, &raw_root)?;
17965        drop(store);
17966        let predecessor = rusqlite::Connection::open(&raw_database)?;
17967        drop_native_worktree_identity_schema(&predecessor)?;
17968        predecessor.execute_batch(
17969            "DROP TABLE project_root_identity;
17970             DROP TABLE IF EXISTS graph_identity_rejections;
17971             UPDATE metadata SET value = '19' WHERE key = 'schema_version';",
17972        )?;
17973        drop(predecessor);
17974        fs::copy(&raw_database, &replacement_database)?;
17975
17976        // A read-only WAL opener may materialize its sidecars. Warm the exact
17977        // recovery path before capturing the no-mutation baseline.
17978        for database in [&raw_database, &replacement_database] {
17979            require(
17980                matches!(
17981                    read_legacy_project_root_candidate_read_only(database),
17982                    Err(DbError::ProjectRootIdentityMissing)
17983                ),
17984                "ambiguous predecessor candidate was exposed during warm-up",
17985            )?;
17986        }
17987
17988        for (root, database) in [
17989            (&raw_root, &raw_database),
17990            (&replacement_root, &replacement_database),
17991        ] {
17992            let before = snapshot(database)?;
17993            require(
17994                ProjectAtlasMcpServer::indexed_root_from_candidate(root).is_none()
17995                    && ProjectAtlasMcpServer::indexed_root_from_lexical_candidate(root).is_none(),
17996                "ambiguous predecessor was admitted by nearest routing",
17997            )?;
17998            require(
17999                snapshot(database)? == before,
18000                "ambiguous predecessor detection changed database or sidecar state",
18001            )?;
18002        }
18003
18004        let collision_raw_root = temp
18005            .path()
18006            .join(std::ffi::OsString::from_vec(b"nearest-repo\\name".to_vec()));
18007        let collision_slash_root = temp.path().join("nearest-repo").join("name");
18008        let collision_raw_database = collision_raw_root
18009            .join(PROJECTATLAS_DIR_NAME)
18010            .join(PROJECTATLAS_DB_FILE_NAME);
18011        let collision_slash_database = collision_slash_root
18012            .join(PROJECTATLAS_DIR_NAME)
18013            .join(PROJECTATLAS_DB_FILE_NAME);
18014        fs::create_dir_all(&collision_raw_root)?;
18015        fs::create_dir_all(
18016            collision_slash_database
18017                .parent()
18018                .ok_or_else(|| io::Error::other("slash collision database has no parent"))?,
18019        )?;
18020        drop(open_atlas_store_for_project(
18021            &collision_raw_database,
18022            &collision_raw_root,
18023        )?);
18024        let collision_predecessor = rusqlite::Connection::open(&collision_raw_database)?;
18025        collision_predecessor.execute(
18026            "UPDATE metadata SET value = ?1 WHERE key = 'project_root'",
18027            [collision_slash_root.to_string_lossy().into_owned()],
18028        )?;
18029        drop_native_worktree_identity_schema(&collision_predecessor)?;
18030        collision_predecessor.execute_batch(
18031            "DROP TABLE project_root_identity;
18032             DROP TABLE IF EXISTS graph_identity_rejections;
18033             UPDATE metadata SET value = '19' WHERE key = 'schema_version';",
18034        )?;
18035        drop(collision_predecessor);
18036        fs::copy(&collision_raw_database, &collision_slash_database)?;
18037        for database in [&collision_raw_database, &collision_slash_database] {
18038            require(
18039                matches!(
18040                    read_legacy_project_root_candidate_read_only(database),
18041                    Err(DbError::ProjectRootIdentityMissing)
18042                ),
18043                "slash-colliding predecessor candidate was exposed during warm-up",
18044            )?;
18045        }
18046        for (root, database) in [
18047            (&collision_raw_root, &collision_raw_database),
18048            (&collision_slash_root, &collision_slash_database),
18049        ] {
18050            let before = snapshot(database)?;
18051            require(
18052                ProjectAtlasMcpServer::indexed_root_from_candidate(root).is_none()
18053                    && ProjectAtlasMcpServer::indexed_root_from_lexical_candidate(root).is_none(),
18054                "slash-colliding predecessor was admitted by nearest routing",
18055            )?;
18056            require(
18057                snapshot(database)? == before,
18058                "slash-colliding predecessor detection changed database or sidecar state",
18059            )?;
18060        }
18061        Ok(())
18062    }
18063}