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, MAX_MAP_RESPONSE_BYTES, add_ignore_entry,
12    effective_config_report, enforce_map_response_limit, init_gitignore, init_project_with_config,
13    list_ignore_entries, load_atlas_config, load_atlas_config_for_root, remove_ignore_entry,
14    render_map, write_json_map,
15};
16use crate::runtime::{
17    DEFAULT_HEALTH_LIMIT, INDEX_WORKER_SAFE_CEILING, IndexInitRequired, IndexProjectMismatch,
18    IndexRefreshRequired, IndexVerificationIncomplete, InitBootstrapOptions, InitHydrationPhase,
19    InitHydrationStatus, InitPhaseStatus, InitScanPhase, InitSetupReport, MAX_HEALTH_LIMIT,
20    MAX_SYMBOL_FILE_BYTES, ProjectWorktreeRequired, PurposeCuratorHandoff, PurposeLintLevel,
21    PurposeReviewRequest, ResetIndexReport, ScanReport, ScanRuntimePlan,
22    SettingsClassifiedNavigationReport, SourceObservationRegistry, SourceReadAdmission,
23    SymbolBuildOptions, UsageRuntimeInstance, VerifiedReadOutcome, VerifiedReadStamp, WatchReport,
24    build_settings_report, byte_count_to_tokens, canonical_project_root,
25    canonical_source_project_root, classified_navigation_capabilities,
26    classified_ranked_file_nodes_with_reasons, config_root_mismatch_error,
27    default_mcp_project_root, estimated_source_tokens_for_indexed_files,
28    estimated_source_tokens_for_paths, federated_worktree_error, index_init_required,
29    index_work_control, init_config_path, init_next_steps, lint_project,
30    load_synchronized_repository_token_report, lossless_native_path_display,
31    lossless_project_root_display, next_step_report_payload, next_step_report_with_selection,
32    normalized_folder_filter, open_atlas_store_for_project, open_atlas_store_read_only_for_project,
33    open_existing_atlas_store_for_project, open_federated_atlas_stores_for_project,
34    preflight_existing_project_binding, publication_contract_matches, purpose_curation_page,
35    purpose_curator_handoff, ranked_file_nodes_with_reasons, ranked_folder_nodes_with_reasons,
36    read_indexed_file_content, reconcile_hydrated_index_controlled,
37    record_directory_walk_usage_estimate, record_usage_estimate, record_usage_text,
38    refresh_index_from_assessment_controlled, render_classified_ranked_file_rows,
39    render_classified_symbol_rows, render_health_page, render_purpose_curation_page,
40    render_purpose_review_report, require_current_worktree_usage_snapshot,
41    require_registered_worktree_lifecycle, reset_index_files, reset_index_files_with_revalidation,
42    review_purposes, run_init_bootstrap, run_scan_pipeline_controlled,
43    run_single_watch_refresh_controlled, run_symbol_build_pipeline_controlled,
44    strip_legacy_purpose, telemetry_disabled, validate_purpose_review_admission,
45    validated_indexed_file_key, watcher_status_report,
46};
47#[cfg(all(test, unix))]
48use crate::runtime::{IndexReadStatus, IndexRefreshReason, IndexRefreshScope};
49#[cfg(test)]
50use crate::runtime::{
51    PURPOSE_CURATOR_RECOMMENDED_REASONING, db_sidecar_path, mcp_config_path_for_db,
52    run_scan_pipeline, synchronize_registered_worktree_usage,
53};
54use crate::token_tui::{
55    TokenDashboardTheme, render_token_dashboard_plain_with_theme,
56    render_token_trend_dashboard_plain_with_theme,
57};
58use crate::{
59    AgentErrorKind, CliError, DEFAULT_FILE_SUMMARY_LIMIT, DatabaseFilesystemErrorPayload,
60    HarnessConfig, OutputFormat, RootTransition, RuntimeInfoReport, SchemaMigrationRequiredPayload,
61    SchemaVersionMismatchPayload, SearchRetrievalModeArg, build_harness_mcp_config_report,
62    build_parity_report, build_repository_control_report, build_root_report, build_runtime_info,
63    controlled_named_output, database_filesystem_error_payload, finalize_coverage_output,
64    render_code_slice, render_file_summary, render_parity_report, render_repository_control_report,
65    render_root_report, render_runtime_info, render_search_report, render_watch_status,
66    schema_migration_required_payload, schema_version_mismatch_payload,
67};
68use projectatlas_core::graph::{
69    Completeness, ConfidenceClass, CoverageRecord, DocumentTargetUnresolvedReason, EntitySelector,
70    ExternalSelector, GraphIdentityText, GraphLimitKind, GraphLimits, GraphRelationKind,
71    ProjectInstanceId, RelationOccurrence, RelationResolution, RepositoryFilePath,
72    ReusableTargetSelector, SourceSpan,
73};
74use projectatlas_core::health::Severity;
75use projectatlas_core::language::{ContentClassification, ContentSelection};
76use projectatlas_core::outline::build_outline;
77use projectatlas_core::symbols::ParserKind;
78use projectatlas_core::telemetry::{
79    TOKEN_BASELINE_DIRECTORY_WALK, TOKEN_BASELINE_SELECTED_CANDIDATES,
80    TOKEN_BUCKET_NAVIGATION_AVOIDANCE, TOKEN_CONFIDENCE_INFERRED, TOKEN_CONFIDENCE_POLICY_ESTIMATE,
81    TokenTrendWindow, UsageInstanceOwner, usage_from_estimates_with_context, usage_from_text,
82};
83use projectatlas_core::toon::{
84    encode_agent_payload, render_outline, render_overview, render_ranked_nodes,
85    render_symbol_relations, render_token_overview, render_token_trends,
86};
87use projectatlas_core::{
88    CanonicalProjectRoot, IndexGeneration, IndexWorkControl, IndexWorkFailure, IndexWorkStage,
89    MAX_GIT_WORKTREE_REGISTRATIONS, NavigationNextCall, NavigationNextCapability, Overview,
90    PurposeSource, PurposeStatus, RankedConnection, RankedConnectionCount, RankedConnectionKind,
91    RankedConnectionTarget, RankedNode, RankedReasonCode, normalize_native_path_display,
92    normalize_native_path_display_str, normalize_repo_path, normalize_repo_path_prefix,
93    validated_repo_file_key, validated_repo_node_key,
94};
95use projectatlas_db::{
96    ActiveWorktreeRegistrationGuard, AtlasStore, DbError, HealthQuery, HealthResolution,
97    HealthScope, PreparedWorktreeHydrationCandidate, RepositoryCoverageQuery, WorktreeAlias,
98    WorktreeHydrationActivation, WorktreeRegistration, WorktreeRegistrationState,
99    WorktreeUsageSnapshot, WorktreeUsageSyncState, read_legacy_project_root_candidate_read_only,
100    read_project_root_identity_read_only, verify_project_database,
101};
102use projectatlas_fs::worktree::{
103    GitRepositoryStructure, GitWorktreeEntry, GitWorktreeRole, GitWorktreeState,
104    RepositoryStructure, discover_repository_structure, git_administrative_identity,
105};
106#[cfg(test)]
107use projectatlas_service::build_file_summary_from_source;
108use projectatlas_service::{
109    COVERAGE_PAGE_MAX_LIMIT, CodeSliceBudget, CoverageDigest, CoverageTrustState,
110    DetailedRelationBudget, DetailedRelationNode, DetailedRelationQuery, DetailedRelationReport,
111    DetailedRelationRow, DetailedRelationWork, EntrypointProfile, FederatedDetailedRelationReport,
112    FederatedParticipant, FederatedRelationWork, FederatedRendezvous, FederatedStore,
113    FileCallSummary, FileSummaryReport, FileSymbolSummary, GitImpactSelection,
114    RelationAnalysisMode, RelationAnalysisQuery, RelationAnchor, RelationDirection,
115    RelationNextCall, RelationPurpose, RelationTotalState, SearchQuery, ServiceError,
116    SymbolSliceSelector, TokenReport, TokenReportRequest,
117    build_file_summary_from_source_with_selection, load_coverage_discovery_controlled,
118    load_detailed_relation_page, load_federated_detailed_relations,
119    load_federated_relation_analysis, load_relation_analysis, load_token_report,
120    parse_coverage_parser, parse_coverage_relation, parse_coverage_state,
121    parse_relation_confidence, parse_relation_direction, parse_relation_resolution,
122    parse_symbol_kind, read_indexed_code_slice_from_source_bounded_with_selection,
123    read_symbol_slice_from_source_bounded_with_selection, search_indexed_files_with_control,
124    validate_federated_root_count,
125};
126use rmcp::handler::server::{
127    router::tool::ToolRouter, tool::IntoCallToolResult, wrapper::Parameters,
128};
129use rmcp::model::{CallToolResponse, Implementation, ServerCapabilities, ServerConfig};
130use rmcp::schemars;
131use rmcp::service::RequestContext;
132use rmcp::{RoleServer, ServerHandler, ServiceExt, tool, tool_handler, tool_router};
133use serde::{Deserialize, Serialize};
134use std::collections::{HashSet, VecDeque};
135use std::fs;
136use std::path::{Component, Path, PathBuf};
137use std::sync::{
138    Arc, Mutex, RwLock,
139    atomic::{AtomicU64, Ordering},
140};
141use std::thread;
142use std::time::{Duration, SystemTime, UNIX_EPOCH};
143
144/// Text response routed through `rmcp`'s native tool-success/tool-error conversion.
145#[derive(Debug, PartialEq, Eq)]
146struct McpToolTextResult(Result<String, String>);
147
148impl std::ops::Deref for McpToolTextResult {
149    type Target = str;
150
151    fn deref(&self) -> &Self::Target {
152        match &self.0 {
153            Ok(text) | Err(text) => text,
154        }
155    }
156}
157
158impl std::fmt::Display for McpToolTextResult {
159    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160        formatter.write_str(self)
161    }
162}
163
164impl IntoCallToolResult for McpToolTextResult {
165    fn into_call_tool_result(self) -> Result<CallToolResponse, rmcp::ErrorData> {
166        self.0.into_call_tool_result()
167    }
168}
169
170/// MCP tools required for the agent-first repository-intelligence surface.
171pub(crate) const REQUIRED_MCP_TOOL_NAMES: &[&str] = &[
172    MCP_TOOL_ATLAS_SET_PROJECT_PATH,
173    MCP_TOOL_ATLAS_WORKTREE_LIST,
174    MCP_TOOL_ATLAS_WORKTREE_ADD,
175    MCP_TOOL_ATLAS_WORKTREE_REMOVE,
176    MCP_TOOL_ATLAS_INIT,
177    MCP_TOOL_ATLAS_MAP,
178    MCP_TOOL_ATLAS_ROOT,
179    MCP_TOOL_ATLAS_ROOT_SET,
180    MCP_TOOL_ATLAS_CONFIG,
181    MCP_TOOL_ATLAS_IGNORE_LIST,
182    MCP_TOOL_ATLAS_IGNORE_INIT_GITIGNORE,
183    MCP_TOOL_ATLAS_IGNORE_ADD,
184    MCP_TOOL_ATLAS_IGNORE_REMOVE,
185    MCP_TOOL_ATLAS_SCAN,
186    MCP_TOOL_ATLAS_OVERVIEW,
187    MCP_TOOL_ATLAS_FOLDERS,
188    MCP_TOOL_ATLAS_FILES,
189    MCP_TOOL_ATLAS_NEXT,
190    MCP_TOOL_ATLAS_OUTLINE,
191    MCP_TOOL_ATLAS_FILE_SUMMARY,
192    MCP_TOOL_ATLAS_SEARCH,
193    MCP_TOOL_ATLAS_SLICE,
194    MCP_TOOL_ATLAS_SYMBOLS_BUILD,
195    MCP_TOOL_ATLAS_SYMBOLS,
196    MCP_TOOL_ATLAS_SYMBOL_RELATIONS,
197    MCP_TOOL_ATLAS_HEALTH,
198    MCP_TOOL_ATLAS_HEALTH_RESOLVE,
199    MCP_TOOL_ATLAS_LINT,
200    MCP_TOOL_ATLAS_TOKEN_REPORT,
201    MCP_TOOL_ATLAS_PARITY_REPORT,
202    MCP_TOOL_ATLAS_SETTINGS,
203    MCP_TOOL_ATLAS_WATCH_STATUS,
204    MCP_TOOL_ATLAS_WATCH_ONCE,
205    MCP_TOOL_ATLAS_STRIP_LEGACY_PURPOSE,
206    MCP_TOOL_ATLAS_RESET_INDEX,
207    MCP_TOOL_ATLAS_MCP_CONFIG,
208    MCP_TOOL_ATLAS_RUNTIME_INFO,
209    MCP_TOOL_ATLAS_SESSION_BRIEF,
210    MCP_TOOL_ATLAS_TASK_STATUS,
211    MCP_TOOL_ATLAS_TASK_CANCEL,
212    MCP_TOOL_ATLAS_PURPOSE_QUEUE,
213    MCP_TOOL_ATLAS_PURPOSE_SET,
214    MCP_TOOL_ATLAS_PURPOSE_REVIEW,
215];
216
217/// MCP tool name for active project selection.
218const MCP_TOOL_ATLAS_SET_PROJECT_PATH: &str = "atlas_set_project_path";
219/// MCP tool name for bounded structural worktree inventory.
220const MCP_TOOL_ATLAS_WORKTREE_LIST: &str = "atlas_worktree_list";
221/// MCP tool name for control-atlas worktree registration.
222const MCP_TOOL_ATLAS_WORKTREE_ADD: &str = "atlas_worktree_add";
223/// MCP tool name for control-atlas worktree retirement.
224const MCP_TOOL_ATLAS_WORKTREE_REMOVE: &str = "atlas_worktree_remove";
225/// MCP tool name for project initialization.
226const MCP_TOOL_ATLAS_INIT: &str = "atlas_init";
227/// MCP tool name for compatibility map exports.
228const MCP_TOOL_ATLAS_MAP: &str = "atlas_map";
229/// MCP tool name for root diagnostics.
230const MCP_TOOL_ATLAS_ROOT: &str = "atlas_root";
231/// MCP tool name for binding a project root.
232const MCP_TOOL_ATLAS_ROOT_SET: &str = "atlas_root_set";
233/// MCP tool name for effective config reports.
234const MCP_TOOL_ATLAS_CONFIG: &str = "atlas_config";
235/// MCP tool name for ignore policy reports.
236const MCP_TOOL_ATLAS_IGNORE_LIST: &str = "atlas_ignore_list";
237/// MCP tool name for project `.gitignore` initialization.
238const MCP_TOOL_ATLAS_IGNORE_INIT_GITIGNORE: &str = "atlas_ignore_init_gitignore";
239/// MCP tool name for adding manual ignore entries.
240const MCP_TOOL_ATLAS_IGNORE_ADD: &str = "atlas_ignore_add";
241/// MCP tool name for removing manual ignore entries.
242const MCP_TOOL_ATLAS_IGNORE_REMOVE: &str = "atlas_ignore_remove";
243/// MCP tool name for repository scans.
244const MCP_TOOL_ATLAS_SCAN: &str = "atlas_scan";
245/// MCP tool name for repository overviews.
246const MCP_TOOL_ATLAS_OVERVIEW: &str = "atlas_overview";
247/// MCP tool name for folder ranking.
248const MCP_TOOL_ATLAS_FOLDERS: &str = "atlas_folders";
249/// MCP tool name for file ranking.
250const MCP_TOOL_ATLAS_FILES: &str = "atlas_files";
251/// MCP tool name for next-step recommendations.
252const MCP_TOOL_ATLAS_NEXT: &str = "atlas_next";
253/// MCP tool name for file outlines.
254const MCP_TOOL_ATLAS_OUTLINE: &str = "atlas_outline";
255/// MCP tool name for file summaries.
256const MCP_TOOL_ATLAS_FILE_SUMMARY: &str = "atlas_file_summary";
257/// MCP tool name for indexed search.
258const MCP_TOOL_ATLAS_SEARCH: &str = "atlas_search";
259/// MCP tool name for source slices.
260const MCP_TOOL_ATLAS_SLICE: &str = "atlas_slice";
261/// MCP tool name for symbol builds.
262const MCP_TOOL_ATLAS_SYMBOLS_BUILD: &str = "atlas_symbols_build";
263/// MCP tool name for symbol lookup.
264const MCP_TOOL_ATLAS_SYMBOLS: &str = "atlas_symbols";
265/// MCP tool name for symbol relation lookup.
266const MCP_TOOL_ATLAS_SYMBOL_RELATIONS: &str = "atlas_symbol_relations";
267/// MCP tool name for health pages.
268const MCP_TOOL_ATLAS_HEALTH: &str = "atlas_health";
269/// MCP tool name for health resolutions.
270const MCP_TOOL_ATLAS_HEALTH_RESOLVE: &str = "atlas_health_resolve";
271/// MCP tool name for lint reports.
272const MCP_TOOL_ATLAS_LINT: &str = "atlas_lint";
273/// MCP tool name for token reports.
274const MCP_TOOL_ATLAS_TOKEN_REPORT: &str = "atlas_token_report";
275/// MCP tool name for parity reports.
276const MCP_TOOL_ATLAS_PARITY_REPORT: &str = "atlas_parity_report";
277/// MCP tool name for settings reports.
278const MCP_TOOL_ATLAS_SETTINGS: &str = "atlas_settings";
279/// MCP tool name for watcher status.
280const MCP_TOOL_ATLAS_WATCH_STATUS: &str = "atlas_watch_status";
281/// MCP tool name for one-shot watcher refreshes.
282const MCP_TOOL_ATLAS_WATCH_ONCE: &str = "atlas_watch_once";
283/// MCP tool name for legacy purpose cleanup.
284const MCP_TOOL_ATLAS_STRIP_LEGACY_PURPOSE: &str = "atlas_strip_legacy_purpose";
285/// MCP tool name for runtime index reset.
286const MCP_TOOL_ATLAS_RESET_INDEX: &str = "atlas_reset_index";
287/// MCP tool name for generated MCP config documents.
288const MCP_TOOL_ATLAS_MCP_CONFIG: &str = "atlas_mcp_config";
289/// MCP tool name for runtime identity reports.
290const MCP_TOOL_ATLAS_RUNTIME_INFO: &str = "atlas_runtime_info";
291/// MCP tool name for compact agent startup briefs.
292const MCP_TOOL_ATLAS_SESSION_BRIEF: &str = "atlas_session_brief";
293/// MCP tool name for task-progress status lookup.
294const MCP_TOOL_ATLAS_TASK_STATUS: &str = "atlas_task_status";
295/// MCP tool name for task-progress cancellation.
296const MCP_TOOL_ATLAS_TASK_CANCEL: &str = "atlas_task_cancel";
297/// MCP tool name for purpose queue lookup.
298const MCP_TOOL_ATLAS_PURPOSE_QUEUE: &str = "atlas_purpose_queue";
299/// MCP tool name for purpose updates.
300const MCP_TOOL_ATLAS_PURPOSE_SET: &str = "atlas_purpose_set";
301/// MCP tool name for purpose reviews.
302const MCP_TOOL_ATLAS_PURPOSE_REVIEW: &str = "atlas_purpose_review";
303/// `ProjectAtlas` local state directory name.
304const PROJECTATLAS_DIR_NAME: &str = ".projectatlas";
305/// `ProjectAtlas` `SQLite` database filename.
306const PROJECTATLAS_DB_FILE_NAME: &str = "projectatlas.db";
307/// Project-local nested config filename.
308const PROJECTATLAS_CONFIG_FILE_NAME: &str = "config.toml";
309/// Project-local flat config filename.
310const PROJECTATLAS_FLAT_CONFIG_FILE_NAME: &str = "projectatlas.toml";
311/// Maximum distinct project bindings retained before inactive telemetry rotation.
312const MCP_TELEMETRY_PROJECT_BINDING_LIMIT: usize = 64;
313/// Hard ceiling for the default agent-facing settings diagnostic.
314const MCP_SETTINGS_RESPONSE_MAX_BYTES: usize = 64_000;
315/// Prefix for an oversized settings-response diagnostic.
316const MCP_SETTINGS_RESPONSE_LIMIT_PREFIX: &str = "settings response requires ";
317/// Separator for the observed and permitted settings-response byte counts.
318const MCP_SETTINGS_RESPONSE_LIMIT_SEPARATOR: &str = " bytes, exceeding the ";
319/// Suffix for an oversized settings-response diagnostic.
320const MCP_SETTINGS_RESPONSE_LIMIT_SUFFIX: &str = "-byte diagnostic limit";
321/// Maximum generation/filter token baselines retained by one MCP process.
322const MCP_SOURCE_TOKEN_BASELINE_LIMIT: usize = 128;
323/// Default MCP config server key.
324const MCP_DEFAULT_CONFIG_SERVER_NAME: &str = "projectatlas";
325/// Recovery guidance when a path names a subfolder rather than another selected root.
326const 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";
327/// Recovery guidance when a path escapes the selected `ProjectAtlas` root.
328const 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";
329/// Current-directory root alias.
330const CURRENT_DIR_ALIAS: &str = ".";
331/// `ProjectAtlas` MCP server display name.
332const MCP_SERVER_NAME: &str = "ProjectAtlas";
333/// Request-local MCP cancellation monitor thread name.
334const MCP_CANCELLATION_MONITOR_THREAD_NAME: &str = "projectatlas-mcp-cancel";
335/// Prefix for cancellation-monitor startup failures.
336const MCP_CANCELLATION_MONITOR_START_ERROR_PREFIX: &str =
337    "MCP request cancellation monitor could not start: ";
338/// Prefix for invalid registered worktree roots.
339const MCP_ERROR_REGISTERED_WORKTREE_ROOT_INVALID_PREFIX: &str =
340    "registered worktree root is invalid: ";
341/// MCP error lock-poison message.
342const MCP_PROJECT_STATE_LOCK_POISONED: &str = "MCP project state lock poisoned";
343/// Prefix used only if structured MCP error serialization fails.
344const MCP_ERROR_SERIALIZATION_FALLBACK_PREFIX: &str = "error: ";
345/// MCP payload key for scan reports.
346const MCP_PAYLOAD_SCAN: &str = "scan";
347/// MCP payload key for project initialization reports.
348const MCP_PAYLOAD_INIT: &str = "init";
349/// MCP payload key for structural worktree inventory.
350const MCP_PAYLOAD_WORKTREES: &str = "worktrees";
351/// MCP payload key for one worktree registration transition.
352const MCP_PAYLOAD_WORKTREE: &str = "worktree";
353/// MCP payload key for compatibility map reports.
354const MCP_PAYLOAD_MAP: &str = "map";
355/// MCP payload key for effective config reports.
356const MCP_PAYLOAD_CONFIG: &str = "config";
357/// MCP payload key for ignore policy reports.
358const MCP_PAYLOAD_IGNORE: &str = "ignore";
359/// MCP payload key for `.gitignore` initialization reports.
360const MCP_PAYLOAD_GITIGNORE: &str = "gitignore";
361/// MCP payload key for lint reports.
362const MCP_PAYLOAD_LINT: &str = "lint";
363/// MCP payload key for symbol-build reports.
364const MCP_PAYLOAD_SYMBOLS_BUILD: &str = "symbols_build";
365/// MCP payload key for detailed symbol-relation reports.
366const MCP_PAYLOAD_SYMBOL_RELATIONS: &str = "symbol_relations";
367/// MCP payload key for health-resolution reports.
368const MCP_PAYLOAD_HEALTH_RESOLUTION: &str = "health_resolution";
369/// MCP payload key for token trend reports.
370const MCP_PAYLOAD_TOKEN_TRENDS: &str = "token_trends";
371/// MCP payload key for token savings reports.
372const MCP_PAYLOAD_TOKEN_SAVINGS: &str = "token_savings";
373/// MCP payload key for optional chart strings.
374const MCP_PAYLOAD_CHART: &str = "chart";
375/// MCP payload key for watcher reports.
376const MCP_PAYLOAD_WATCH: &str = "watch";
377/// MCP payload key for legacy-purpose migration reports.
378const MCP_PAYLOAD_LEGACY_PURPOSE_MIGRATION: &str = "legacy_purpose_migration";
379/// MCP payload key for reset-index reports.
380const MCP_PAYLOAD_RESET_INDEX: &str = "reset_index";
381/// MCP payload key for generated MCP config reports.
382const MCP_PAYLOAD_MCP_CONFIG: &str = "mcp_config";
383/// MCP payload key for next-step recommendation reports.
384const MCP_PAYLOAD_NEXT: &str = "next";
385/// MCP payload key for selected-project audit metadata on routed reads.
386const MCP_PAYLOAD_SELECTED_PROJECT: &str = "selected_project";
387/// MCP payload key for settings reports.
388const MCP_PAYLOAD_SETTINGS: &str = "settings";
389/// MCP payload key for agent startup briefs.
390const MCP_PAYLOAD_SESSION_BRIEF: &str = "session_brief";
391/// Default source status omitted from compact file-summary payloads.
392const MCP_FILE_SOURCE_STATUS_LIVE: &str = "live-source";
393/// MCP payload key for accepted background tasks.
394const MCP_PAYLOAD_TASK_START: &str = "task_start";
395/// MCP payload key for task status lookups.
396const MCP_PAYLOAD_TASK_STATUS: &str = "task_status";
397/// MCP payload key for task cancellation responses.
398const MCP_PAYLOAD_TASK_CANCEL: &str = "task_cancel";
399/// MCP session capability payload key.
400const MCP_PAYLOAD_SESSION_CAPABILITIES: &str = "mcp_session";
401/// Session-brief argument key for per-call project roots.
402const MCP_BRIEF_ARG_PROJECT_PATH: &str = "project_path";
403/// Session-brief argument key for registered worktree aliases.
404const MCP_BRIEF_ARG_WORKTREE: &str = "worktree";
405/// Session-brief argument key for exact repository-relative files.
406const MCP_BRIEF_ARG_FILE: &str = "file";
407/// Session-brief argument key for indexed search patterns.
408const MCP_BRIEF_ARG_PATTERN: &str = "pattern";
409/// Session-brief argument key for relation view selection.
410const MCP_BRIEF_ARG_VIEW: &str = "view";
411/// Session-brief argument key for row limits.
412const MCP_BRIEF_ARG_LIMIT: &str = "limit";
413/// Session-brief argument key for host-owned purpose-curation tasks.
414const MCP_BRIEF_ARG_TASK: &str = "task";
415/// Compact-response argument key used by typed startup recommendations.
416const MCP_BRIEF_ARG_COMPACT: &str = "compact";
417/// Session-brief recommendation target for normal filesystem reads.
418const MCP_BRIEF_TARGET_FILESYSTEM_TOOLS: &str = "filesystem_tools";
419/// Session-brief reason for missing selected indexes.
420const MCP_BRIEF_REASON_SELECTED_INDEX_MISSING: &str = "selected_index_missing";
421/// Session-brief reason for filesystem fallback before an index exists.
422const MCP_BRIEF_REASON_FILESYSTEM_UNTIL_INDEX: &str =
423    "use_filesystem_until_projectatlas_index_exists";
424/// Session-brief reason for following the selected file into its summary.
425const MCP_BRIEF_REASON_RANKED_FILE_SUMMARY: &str = "ranked_file_ready_for_summary";
426/// Session-brief reason for following truncated graph evidence into detailed relations.
427const MCP_BRIEF_REASON_RANKED_FILE_RELATIONS: &str = "ranked_file_ready_for_relations";
428/// Session-brief reason for searching when ranking found no directly navigable file.
429const MCP_BRIEF_REASON_SEARCH_FALLBACK: &str = "no_ranked_file_candidate_search_index";
430/// Session-brief reason for normal filesystem orientation in an indexed empty project.
431const MCP_BRIEF_REASON_NO_FILE_CANDIDATE: &str = "no_ranked_file_candidate";
432/// Session-brief reason for health follow-up.
433const MCP_BRIEF_REASON_HEALTH_BLOCKERS: &str = "unresolved_health_blockers_present";
434/// Session-brief reason for following an actionable purpose-curator handoff.
435const MCP_BRIEF_REASON_PURPOSE_QUEUE: &str = "purpose_queue_ready";
436/// Built-in task-progress contract message.
437const MCP_TASK_PROGRESS_CONTRACT_MESSAGE: &str = "task progress contract available";
438/// MCP telemetry event for overview calls.
439const MCP_EVENT_ATLAS_OVERVIEW: &str = "mcp.atlas_overview";
440/// MCP telemetry event for folder calls.
441const MCP_EVENT_ATLAS_FOLDERS: &str = "mcp.atlas_folders";
442/// MCP telemetry event for file calls.
443const MCP_EVENT_ATLAS_FILES: &str = "mcp.atlas_files";
444/// MCP telemetry event for next-step recommendation calls.
445const MCP_EVENT_ATLAS_NEXT: &str = "mcp.atlas_next";
446/// MCP telemetry event for outline calls.
447const MCP_EVENT_ATLAS_OUTLINE: &str = "mcp.atlas_outline";
448/// MCP telemetry event for file-summary calls.
449const MCP_EVENT_ATLAS_FILE_SUMMARY: &str = "mcp.atlas_file_summary";
450/// MCP telemetry event for search calls.
451const MCP_EVENT_ATLAS_SEARCH: &str = "mcp.atlas_search";
452/// MCP telemetry event for slice calls.
453const MCP_EVENT_ATLAS_SLICE: &str = "mcp.atlas_slice";
454/// MCP telemetry event for symbol calls.
455const MCP_EVENT_ATLAS_SYMBOLS: &str = "mcp.atlas_symbols";
456/// MCP telemetry event for symbol-relation calls.
457const MCP_EVENT_ATLAS_SYMBOL_RELATIONS: &str = "mcp.atlas_symbol_relations";
458/// Compatible MCP symbol-relation view name.
459const MCP_SYMBOL_RELATION_VIEW_LEGACY: &str = "legacy";
460/// Additive detailed MCP symbol-relation view name.
461const MCP_SYMBOL_RELATION_VIEW_DETAILED: &str = "detailed";
462/// Additive closed analysis view on the existing relation route.
463const MCP_SYMBOL_RELATION_VIEW_ANALYSIS: &str = "analysis";
464/// Default closed relation-analysis mode.
465const MCP_RELATION_ANALYSIS_MODE_ARCHITECTURE: &str = "architecture";
466/// VCS impact relation-analysis mode.
467const MCP_RELATION_ANALYSIS_MODE_IMPACT: &str = "impact";
468/// Static trace relation-analysis mode.
469const MCP_RELATION_ANALYSIS_MODE_TRACE: &str = "trace";
470/// Explicit entrypoint-profile reachability mode.
471const MCP_RELATION_ANALYSIS_MODE_ENTRYPOINT: &str = "entrypoint";
472/// Default entrypoint profile name.
473const MCP_ENTRYPOINT_PROFILE_DEFAULT_NAME: &str = "entrypoint-profile";
474/// MCP validation error when entrypoint analysis receives federated stores.
475const MCP_ERROR_ENTRYPOINT_FEDERATED: &str = "entrypoint profiles require one project root";
476/// Prefix for invalid entrypoint anchor JSON.
477const MCP_ERROR_ENTRYPOINT_ANCHORS_PREFIX: &str =
478    "entrypoints must contain exact RelationAnchor JSON objects: ";
479/// MCP validation error for entrypoint controls on another analysis mode.
480const MCP_ERROR_ENTRYPOINT_CONTROLS_MODE: &str =
481    "entrypoint profile controls require analysis_mode=entrypoint";
482/// MCP validation error for contradictory entrypoint and symbol selectors.
483const MCP_ERROR_ENTRYPOINT_SYMBOL_SELECTOR: &str =
484    "entrypoint anchors cannot be combined with detailed symbol selectors";
485/// Default working-tree VCS impact selection.
486const MCP_RELATION_ANALYSIS_VCS_WORKING_TREE: &str = "working_tree";
487/// Staged-index VCS impact selection.
488const MCP_RELATION_ANALYSIS_VCS_INDEX: &str = "index";
489/// Explicit revision-range VCS impact selection.
490const MCP_RELATION_ANALYSIS_VCS_REVISION_RANGE: &str = "revision_range";
491/// Default direction for detailed MCP symbol-relation requests.
492const MCP_SYMBOL_RELATION_DIRECTION_DEFAULT: &str = "outbound";
493/// Default minimum confidence for detailed MCP symbol-relation requests.
494const MCP_SYMBOL_RELATION_CONFIDENCE_DEFAULT: &str = "low";
495/// Default resolution filter for detailed MCP symbol-relation requests.
496const MCP_SYMBOL_RELATION_RESOLUTION_DEFAULT: &str = "any";
497/// MCP validation error for an unsupported relation view.
498const MCP_ERROR_SYMBOL_RELATION_VIEW: &str = "unsupported symbol relation view";
499/// MCP validation error for legacy query fields in detailed requests.
500const MCP_ERROR_DETAILED_RELATION_QUERY: &str =
501    "detailed symbol relations use exact symbol selectors, not query";
502/// Unsupported graph controls on the complete compatibility-array route.
503const MCP_ERROR_LEGACY_RELATION_CONTROLS: &str = "graph selectors, federation, traversal and continuation controls require view detailed or analysis";
504/// Missing compatibility admission after view selection.
505const MCP_ERROR_LEGACY_RELATION_BUDGET: &str = "legacy relation budget was not admitted";
506/// Exact-file narrowing guidance for detailed relation requests.
507const MCP_ERROR_DETAILED_RELATION_FILE: &str = "detailed relations require an exact file anchor; discover candidates with atlas_files (limit 3), then call atlas_symbol_relations with file and optional symbol";
508/// MCP validation error for an empty detailed symbol anchor.
509const MCP_ERROR_DETAILED_RELATION_SYMBOL: &str = "detailed relation symbol must not be empty";
510/// MCP validation error for symbol disambiguators without a symbol.
511const MCP_ERROR_DETAILED_RELATION_DISAMBIGUATOR: &str = "symbol disambiguators require symbol";
512/// MCP validation error for a relation limit outside the service range.
513const MCP_ERROR_DETAILED_RELATION_LIMIT: &str = "detailed relation limit exceeds the u32 range";
514/// MCP validation error for compact projection on a non-detailed relation view.
515const MCP_ERROR_COMPACT_DETAILED_RELATION_VIEW: &str =
516    "compact symbol relations require view=detailed";
517/// MCP validation error for classified traversal on the legacy relation view.
518const MCP_ERROR_CONTENT_SELECTION_RELATION_VIEW: &str =
519    "content_selection requires view=detailed or view=analysis";
520/// MCP validation error for analysis controls on another relation view.
521const MCP_ERROR_ANALYSIS_VIEW_REQUIRED: &str = "analysis controls require view=analysis";
522/// MCP validation error for federation on the legacy relation view.
523const MCP_ERROR_FEDERATED_RELATION_VIEW: &str =
524    "roots or worktrees require the detailed or analysis relation view";
525/// MCP validation error for two federation selector families in one request.
526const MCP_ERROR_FEDERATED_SELECTOR_CONFLICT: &str =
527    "roots and worktrees are mutually exclusive federation selectors";
528/// MCP validation error for combining alias federation with a legacy root path.
529const MCP_ERROR_FEDERATED_PROJECT_PATH_CONFLICT: &str =
530    "worktrees federation cannot be combined with project_path";
531/// MCP validation error for a mismatched explicit primary alias.
532const MCP_ERROR_FEDERATED_PRIMARY_CONFLICT: &str =
533    "worktree must match the first ordered worktrees alias";
534/// MCP validation error for a symbol trace without a symbol-kind selector.
535const MCP_ERROR_TRACE_TARGET_KIND_REQUIRED: &str = "symbol trace targets require trace_target_kind";
536/// MCP validation error for a symbol trace without a signature selector.
537const MCP_ERROR_TRACE_TARGET_SIGNATURE_REQUIRED: &str =
538    "symbol trace targets require trace_target_signature";
539/// MCP validation error for symbol disambiguators without a trace symbol.
540const MCP_ERROR_TRACE_TARGET_REQUIRED: &str =
541    "trace target symbol disambiguators require trace_target";
542/// MCP validation error for a trace target without its owning file.
543const MCP_ERROR_TRACE_TARGET_FILE_REQUIRED: &str = "trace_target requires trace_target_file";
544/// MCP validation error for revision fields outside revision-range selection.
545const MCP_ERROR_VCS_REVISION_FIELDS: &str = "vcs_base and vcs_head require vcs=revision_range";
546/// MCP validation error for a revision-range selection without its base.
547const MCP_ERROR_VCS_BASE_REQUIRED: &str = "vcs=revision_range requires vcs_base";
548/// MCP validation error for a revision-range selection without its head.
549const MCP_ERROR_VCS_HEAD_REQUIRED: &str = "vcs=revision_range requires vcs_head";
550/// MCP validation error for an unsupported VCS impact selector.
551const MCP_ERROR_UNSUPPORTED_ANALYSIS_VCS: &str = "unsupported analysis VCS selection";
552/// MCP validation error for an unsupported closed relation-analysis mode.
553const MCP_ERROR_UNSUPPORTED_ANALYSIS_MODE: &str = "unsupported relation analysis mode";
554/// MCP telemetry event for health calls.
555const MCP_EVENT_ATLAS_HEALTH: &str = "mcp.atlas_health";
556/// MCP telemetry event for purpose-queue calls.
557const MCP_EVENT_ATLAS_PURPOSE_QUEUE: &str = "mcp.atlas_purpose_queue";
558/// MCP ignore kind token for directory names.
559const MCP_IGNORE_KIND_DIR_NAME: &str = "dir-name";
560/// MCP alternate ignore kind token for directory names.
561const MCP_IGNORE_KIND_DIR_NAME_ALIAS: &str = "dir_name";
562/// MCP ignore kind token for path prefixes.
563const MCP_IGNORE_KIND_PATH_PREFIX: &str = "path-prefix";
564/// MCP alternate ignore kind token for path prefixes.
565const MCP_IGNORE_KIND_PATH_PREFIX_ALIAS: &str = "path_prefix";
566/// MCP purpose lint level token for low strictness.
567const MCP_PURPOSE_LEVEL_LOW: &str = "low";
568/// MCP purpose lint level token for medium strictness.
569const MCP_PURPOSE_LEVEL_MEDIUM: &str = "medium";
570/// MCP purpose lint level token for strict mode.
571const MCP_PURPOSE_LEVEL_STRICT: &str = "strict";
572/// Default purpose-curation task used by session startup briefs.
573const MCP_PURPOSE_TASK_SESSION_STARTUP: &str = "session-startup";
574/// Default purpose-curation task used by direct queue requests.
575const MCP_PURPOSE_TASK_QUEUE: &str = "purpose-curation";
576/// MCP harness token for standard MCP JSON config.
577const MCP_HARNESS_MCP_JSON: &str = "mcp-json";
578/// MCP alternate harness token for standard MCP JSON config.
579const MCP_HARNESS_MCP_JSON_ALIAS: &str = "mcp_json";
580/// MCP harness token for Codex config.
581const MCP_HARNESS_CODEX: &str = "codex";
582/// MCP harness token for Claude Code config.
583const MCP_HARNESS_CLAUDE_CODE: &str = "claude-code";
584/// MCP alternate harness token for Claude Code config.
585const MCP_HARNESS_CLAUDE_CODE_ALIAS: &str = "claude_code";
586/// MCP harness token for `OpenCode` config.
587const MCP_HARNESS_OPENCODE: &str = "opencode";
588/// Required ignore kind diagnostic.
589const MCP_ERROR_IGNORE_KIND_REQUIRED: &str =
590    "ignore kind is required; expected dir-name or path-prefix";
591/// Required ignore kind diagnostic for mutation tools.
592const MCP_ERROR_IGNORE_KIND_REQUIRED_FOR_ADD: &str = "ignore kind is required for atlas_ignore_add";
593/// Error for mutually exclusive exact-root and structural-control selection.
594const MCP_ERROR_ROOT_CONTROL_CONFLICT: &str =
595    "control_root cannot be combined with project_path or verify";
596/// Invalid coverage start-index diagnostic prefix.
597const MCP_ERROR_COVERAGE_START_INDEX_TOO_LARGE_PREFIX: &str = "coverage start index is too large: ";
598/// Invalid coverage limit diagnostic prefix.
599const MCP_ERROR_COVERAGE_LIMIT_TOO_LARGE_PREFIX: &str = "coverage limit is too large: ";
600/// Coverage-filter diagnostic for the default structural-health mode.
601const MCP_ERROR_COVERAGE_FILTERS_REQUIRE_COVERAGE: &str = "coverage filters require coverage=true";
602/// CI environment variable used by MCP map export safeguards.
603const MCP_ENV_CI: &str = "CI";
604/// GitHub Actions environment variable used by MCP map export safeguards.
605const MCP_ENV_GITHUB_ACTIONS: &str = "GITHUB_ACTIONS";
606/// Optional JSON sidecar skip reason in CI.
607const MCP_MAP_SKIPPED_IN_CI_REASON: &str =
608    "JSON sidecar skipped in CI; pass force=true to write it";
609/// Placeholder when no routed root is available for a diagnostic.
610const MCP_NO_ROOT_PLACEHOLDER: &str = "none";
611/// Invalid ignore kind diagnostic prefix.
612const MCP_ERROR_INVALID_IGNORE_KIND_PREFIX: &str = "invalid ignore kind '";
613/// Invalid ignore kind diagnostic suffix.
614const MCP_ERROR_INVALID_IGNORE_KIND_SUFFIX: &str = "'; expected dir-name or path-prefix";
615/// Invalid purpose lint level diagnostic prefix.
616const MCP_ERROR_INVALID_PURPOSE_LEVEL_PREFIX: &str = "invalid purpose_level '";
617/// Invalid purpose lint level diagnostic suffix.
618const MCP_ERROR_INVALID_PURPOSE_LEVEL_SUFFIX: &str = "'; expected low, medium, or strict";
619/// Invalid harness diagnostic prefix.
620const MCP_ERROR_INVALID_HARNESS_PREFIX: &str = "invalid harness '";
621/// Invalid harness diagnostic suffix.
622const MCP_ERROR_INVALID_HARNESS_SUFFIX: &str =
623    "'; expected mcp-json, codex, claude-code, or opencode";
624/// Ambiguous route diagnostic fragment.
625const MCP_ERROR_FOR_PATH_FRAGMENT: &str = " for '";
626/// Ambiguous route diagnostic fragment.
627const MCP_ERROR_LEXICAL_ROOT_FRAGMENT: &str = "'; lexical root: '";
628/// Ambiguous route diagnostic fragment.
629const MCP_ERROR_RESOLVED_ROOT_FRAGMENT: &str = "'; resolved root: '";
630/// Ambiguous route diagnostic fragment.
631const MCP_ERROR_GUIDANCE_FRAGMENT: &str = "'; ";
632/// Node payload label for rendered folder rows.
633const NODE_LABEL_FOLDERS: &str = "folders";
634/// Node payload label for rendered file rows.
635const NODE_LABEL_FILES: &str = "files";
636/// Stable MCP payload key for classified symbol rows.
637const NODE_LABEL_SYMBOLS: &str = "symbols";
638/// Error when a symbol disambiguator is supplied without a symbol name.
639const SYMBOL_DISAMBIGUATOR_WITHOUT_SYMBOL_ERROR: &str = "symbol disambiguators require symbol";
640/// Error when a line slice omits its start line.
641const START_LINE_REQUIRED_ERROR: &str = "start_line is required unless symbol is provided";
642/// Error when an absolute MCP file path has no valid indexed project ancestor.
643const PATH_NOT_INSIDE_INDEXED_PROJECT_ERROR: &str =
644    "path is not inside an indexed ProjectAtlas project";
645/// Error when an absolute MCP folder path has no valid indexed project ancestor.
646const FOLDER_NOT_INSIDE_INDEXED_PROJECT_ERROR: &str =
647    "folder is not inside an indexed ProjectAtlas project";
648/// Error when lexical and canonical path routing disagree.
649const AMBIGUOUS_NEAREST_PROJECT_PATH_ERROR: &str = "absolute MCP path resolves through a symlink or junction with multiple plausible ProjectAtlas roots";
650/// Separator for diagnostic lists of accepted severity names.
651const SEVERITY_EXPECTED_SEPARATOR: &str = ", ";
652/// Final separator for diagnostic lists of accepted severity names.
653const SEVERITY_EXPECTED_FINAL_SEPARATOR: &str = ", or ";
654/// Token trend validation error suffix.
655const TOKEN_TREND_WINDOW_ERROR_SUFFIX: &str = "expected day, week, month, or year";
656/// Validation error for benchmark evidence on token trend requests.
657const TOKEN_TREND_BENCHMARK_ERROR: &str =
658    "benchmark_results is only supported for token overview reports";
659/// Internal mismatch when a trend request returns the overview variant.
660const TOKEN_TRENDS_RESULT_VARIANT_MISMATCH: &str = "token trend request returned an overview";
661/// Internal mismatch when an overview request returns the trends variant.
662const TOKEN_OVERVIEW_RESULT_VARIANT_MISMATCH: &str = "token overview request returned trends";
663/// Token chart theme validation error prefix.
664const TOKEN_CHART_THEME_ERROR_PREFIX: &str = "unsupported token chart theme ";
665/// Token chart theme validation error suffix.
666const TOKEN_CHART_THEME_ERROR_SUFFIX: &str = "; expected dark or light";
667/// Watch-status recommendation when no index exists.
668const WATCH_STATUS_SCAN_RECOMMENDATION: &str =
669    " Run `atlas_scan` first when no ProjectAtlas index exists for this project.";
670/// Default number of rows in an agent startup brief section.
671const SESSION_BRIEF_DEFAULT_LIMIT: usize = 5;
672/// Default number of rows in an explicitly compact startup brief section.
673const COMPACT_SESSION_BRIEF_DEFAULT_LIMIT: usize = 3;
674/// Maximum number of rows in an agent startup brief section.
675const SESSION_BRIEF_MAX_LIMIT: usize = 8;
676/// Bounded MCP task registry capacity.
677const MCP_TASK_REGISTRY_CAPACITY: usize = 32;
678/// Maximum concise task failure text retained in the session registry.
679const MCP_TASK_ERROR_MAX_CHARS: usize = 512;
680/// Built-in task id that exposes the task-progress contract itself.
681const MCP_TASK_CONTRACT_ID: &str = "task-progress-contract";
682/// Task-registry synchronization failure diagnostic.
683const MCP_TASK_REGISTRY_LOCK_POISONED: &str = "MCP task registry lock is poisoned";
684/// Prefix for generated background index task identifiers.
685const MCP_INDEX_TASK_ID_PREFIX: &str = "index-";
686/// Prefix for named background index worker threads.
687const MCP_INDEX_WORKER_NAME_PREFIX: &str = "projectatlas-";
688/// Prefix for active background task limit diagnostics.
689const MCP_INDEX_TASK_LIMIT_PREFIX: &str = "background indexing task limit ";
690/// Suffix for active background task limit diagnostics.
691const MCP_INDEX_TASK_LIMIT_SUFFIX: &str = " is already active";
692/// Maximum background indexing tasks admitted by one MCP server session.
693const MCP_BACKGROUND_TASK_SAFE_CEILING: usize = 4;
694/// Terminal diagnostic when a background worker panics.
695const MCP_INDEX_WORKER_PANIC_ERROR: &str = "background indexing worker panicked";
696/// Prefix for background worker spawn failures.
697const MCP_INDEX_WORKER_SPAWN_ERROR_PREFIX: &str = "failed to start background indexing: ";
698/// Progress message recorded after task admission.
699const MCP_TASK_PROGRESS_ACCEPTED: &str = "accepted";
700/// Progress message recorded while task work is active.
701const MCP_TASK_PROGRESS_RUNNING: &str = "running";
702/// Progress message recorded after successful task completion.
703const MCP_TASK_PROGRESS_COMPLETE: &str = "complete";
704/// Progress message recorded after task failure.
705const MCP_TASK_PROGRESS_FAILED: &str = "failed";
706/// Progress message recorded after cooperative cancellation completes.
707const MCP_TASK_PROGRESS_CANCELED: &str = "canceled";
708/// Progress message recorded after cancellation is requested.
709const MCP_TASK_PROGRESS_CANCELLATION_REQUESTED: &str = "cancellation_requested";
710/// Agent-facing MCP server instructions.
711const 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.";
712/// Root-scoped selector conflict returned before filesystem or database access.
713const MCP_WORKTREE_PROJECT_PATH_CONFLICT: &str =
714    "worktree and project_path are mutually exclusive; choose one target selector";
715/// Reserved alias for the immutable MCP control authority.
716const MCP_MAIN_WORKTREE_ALIAS: &str = "main";
717/// Maximum combined structural and unmatched registered rows in one MCP response.
718const MCP_WORKTREE_LIST_MAX_ROWS: usize = (MAX_GIT_WORKTREE_REGISTRATIONS * 2) + 1;
719/// Stable prefix for structural worktree candidates returned to agents.
720const MCP_WORKTREE_SELECTOR_PREFIX: &str = "wt-";
721/// Hex characters retained from the administrative-path digest.
722const MCP_WORKTREE_SELECTOR_DIGEST_CHARS: usize = 16;
723/// Init-owned non-source metadata file copied or removed during hydration fallback.
724const MCP_NONSOURCE_FILE_NAME: &str = "projectatlas-nonsource-files.toon";
725/// Typed fallback when a caller explicitly suppresses the reconciliation scan.
726const MCP_HYDRATION_NO_SCAN_REASON: &str =
727    "hydration requires source reconciliation; ordinary no-scan init was requested";
728/// Worktree aliases require one structurally verified Git control repository.
729const MCP_ERROR_WORKTREE_CONTROL_REPOSITORY_REQUIRED: &str =
730    "worktree aliases require a structurally valid Git control repository";
731/// UTF-8-only display terminals cannot render some native worktree paths.
732const MCP_ERROR_WORKTREE_PATH_NON_UTF8: &str =
733    "native worktree identity is not available as UTF-8; use its stable alias or selector";
734/// Active registrations must agree with their local atlas identity.
735const MCP_ERROR_WORKTREE_IDENTITY_CONFLICT: &str =
736    "local atlas identity conflicts with its active registration";
737/// Deferred alias work must retain the exact control catalog that owns its registration ID.
738const MCP_ERROR_WORKTREE_CONTROL_IDENTITY_CONFLICT: &str =
739    "control atlas identity changed after worktree alias selection";
740/// Recovery guidance when an initialized registration loses its exact atlas.
741const 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";
742/// Resetting a bound atlas would strand its control-catalog identity.
743const 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";
744/// A reused administrative path cannot inherit an earlier registration.
745const MCP_ERROR_WORKTREE_LIFECYCLE_CHANGED: &str =
746    "registered worktree administrative lifecycle changed; unregister and register it again";
747/// Federation must retain the exact alias captured during target resolution.
748const MCP_ERROR_FEDERATED_ALIAS_MISSING: &str =
749    "federated worktree resolution lost its captured alias";
750/// Federation cannot address the same alias or root twice.
751const MCP_ERROR_FEDERATED_TARGET_DUPLICATE: &str =
752    "federated worktree aliases and roots must be unique";
753/// The immutable control checkout is addressed only through its reserved alias.
754const MCP_ERROR_CONTROL_ALIAS_REQUIRED: &str =
755    "the control checkout is selected through reserved alias main";
756/// Worktree registration requires one non-empty structural selector.
757const MCP_ERROR_WORKTREE_SELECTOR_EMPTY: &str = "worktree selector must not be empty";
758/// Structural evidence can disappear between selection and registration.
759const MCP_ERROR_WORKTREE_NO_LONGER_ACTIVE: &str = "selected worktree is no longer active";
760/// Retiring a missing registration preserves its last accepted aggregate.
761const MCP_WORKTREE_MISSING_RETENTION_REASON: &str =
762    "worktree is structurally missing; the last accepted telemetry total is retained";
763
764/// Optional active-project override accepted by MCP tools.
765#[derive(Debug, Deserialize, schemars::JsonSchema)]
766struct AtlasProjectParams {
767    /// Optional project root for this call. Defaults to the active MCP project.
768    project_path: Option<String>,
769    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
770    worktree: Option<String>,
771}
772
773/// MCP parameter payload for compact agent startup briefs.
774#[derive(Debug, Deserialize, schemars::JsonSchema)]
775struct AtlasSessionBriefParams {
776    /// Optional project root for this call. Defaults to the active MCP project.
777    project_path: Option<String>,
778    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
779    worktree: Option<String>,
780    /// Optional task query used for folder and file ranking.
781    query: Option<String>,
782    /// Optional host-owned task label for the purpose-curator handoff.
783    purpose_task: Option<String>,
784    /// Return the additive compact startup projection when true.
785    compact: Option<bool>,
786    /// Maximum folder candidates to return.
787    folder_limit: Option<usize>,
788    /// Maximum file candidates to return.
789    file_limit: Option<usize>,
790    /// Maximum health blockers to return.
791    blocker_limit: Option<usize>,
792    /// Maximum actionable low-scope purpose rows in the startup handoff.
793    purpose_limit: Option<usize>,
794}
795
796/// MCP parameter payload for task-progress tools.
797#[derive(Debug, Deserialize, schemars::JsonSchema)]
798struct AtlasTaskParams {
799    /// Opaque MCP-session-local task id.
800    task_id: String,
801}
802
803/// MCP parameter payload for selecting the active project.
804#[derive(Debug, Deserialize, schemars::JsonSchema)]
805struct AtlasSetProjectPathParams {
806    /// Project root to make active for calls that omit `project_path`.
807    project_path: String,
808}
809
810/// MCP parameter payload for bounded structural worktree inventory.
811#[derive(Debug, Deserialize, schemars::JsonSchema)]
812struct AtlasWorktreeListParams {
813    /// Include retired `ProjectAtlas` registrations after active structural rows.
814    include_retired: Option<bool>,
815}
816
817/// MCP parameter payload for registering one structurally discovered worktree.
818#[derive(Debug, Deserialize, schemars::JsonSchema)]
819struct AtlasWorktreeAddParams {
820    /// Stable or uniquely matching short selector returned by `atlas_worktree_list`.
821    worktree: String,
822    /// Optional short alias; defaults to the selected worktree directory name when valid.
823    alias: Option<String>,
824}
825
826/// MCP parameter payload for retiring one active `ProjectAtlas` worktree alias.
827#[derive(Debug, Deserialize, schemars::JsonSchema)]
828struct AtlasWorktreeRemoveParams {
829    /// Active registered alias to remove from `ProjectAtlas` selection.
830    worktree: String,
831}
832
833/// MCP parameter payload for initializing a project.
834#[derive(Debug, Deserialize, schemars::JsonSchema)]
835struct AtlasInitParams {
836    /// Optional project root for this call. Defaults to the active MCP project.
837    project_path: Option<String>,
838    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
839    worktree: Option<String>,
840    /// Create/verify the project surface without running the scan/index pipeline.
841    no_scan: Option<bool>,
842    /// Run the scan/index phase even when a future freshness check could skip it.
843    force_rescan: Option<bool>,
844    /// Maximum UTF-8 file size persisted into `SQLite` text search during the init scan.
845    text_index_max_bytes: Option<u64>,
846}
847
848/// MCP parameter payload for compatibility map exports.
849#[derive(Debug, Deserialize, schemars::JsonSchema)]
850struct AtlasMapParams {
851    /// Optional project root for this call. Defaults to the active MCP project.
852    project_path: Option<String>,
853    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
854    worktree: Option<String>,
855    /// Return JSON map content and write the adjacent JSON sidecar when true.
856    json: Option<bool>,
857    /// Write the requested JSON sidecar even in CI-like environments.
858    force: Option<bool>,
859}
860
861/// MCP parameter payload for root diagnostics.
862#[derive(Debug, Deserialize, schemars::JsonSchema)]
863struct AtlasRootParams {
864    /// Optional project root for this call. Defaults to the active MCP project.
865    project_path: Option<String>,
866    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
867    worktree: Option<String>,
868    /// Optional checkout or Git common directory for mutation-free worktree status.
869    control_root: Option<String>,
870    /// Return the same report shape with `verified` available for gating.
871    verify: Option<bool>,
872}
873
874/// MCP parameter payload for binding a root.
875#[derive(Debug, Deserialize, schemars::JsonSchema)]
876struct AtlasRootSetParams {
877    /// Project root to bind and make active for later calls.
878    root: String,
879    /// Explicit durable transition. Omitted requests retain bind behavior.
880    transition: Option<RootTransition>,
881    /// Include mcp --nearest-project in generated project-local MCP configs.
882    nearest_project: Option<bool>,
883}
884
885/// MCP parameter payload for ignore mutations.
886#[derive(Debug, Deserialize, schemars::JsonSchema)]
887struct AtlasIgnoreMutationParams {
888    /// Optional project root for this call. Defaults to the active MCP project.
889    project_path: Option<String>,
890    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
891    worktree: Option<String>,
892    /// Ignore kind: dir-name or path-prefix. Omit only for broad remove.
893    kind: Option<String>,
894    /// Directory name or repository-relative path prefix.
895    value: String,
896}
897
898/// MCP parameter payload for lint reports.
899#[derive(Debug, Deserialize, schemars::JsonSchema)]
900struct AtlasLintParams {
901    /// Optional project root for this call. Defaults to the active MCP project.
902    project_path: Option<String>,
903    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
904    worktree: Option<String>,
905    /// Deprecated compatibility flag matching the CLI.
906    strict_folders: Option<bool>,
907    /// Purpose lint strictness: low, medium, or strict.
908    purpose_level: Option<String>,
909    /// Include untracked-file report.
910    report_untracked: Option<bool>,
911    /// Make untracked-file findings fail lint.
912    strict_untracked: Option<bool>,
913}
914
915/// MCP parameter payload for generated MCP config documents.
916#[derive(Debug, Deserialize, schemars::JsonSchema)]
917struct AtlasMcpConfigParams {
918    /// Optional project root for this call. Defaults to the active MCP project.
919    project_path: Option<String>,
920    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
921    worktree: Option<String>,
922    /// MCP server name to emit. Defaults to projectatlas.
923    server_name: Option<String>,
924    /// Harness config shape: mcp-json, codex, claude-code, or opencode.
925    harness: Option<String>,
926    /// Include mcp --nearest-project in generated startup args.
927    nearest_project: Option<bool>,
928}
929
930/// Run the official RMCP stdio server.
931pub(crate) fn run_mcp_server(
932    db_path: PathBuf,
933    config_path: Option<PathBuf>,
934    session: String,
935    allow_nearest_project: bool,
936) -> Result<(), CliError> {
937    let server = ProjectAtlasMcpServer::new(db_path, config_path, session, allow_nearest_project);
938    let shutdown_server = server.clone();
939    let runtime = tokio::runtime::Builder::new_multi_thread()
940        .enable_all()
941        .build()
942        .map_err(|source| CliError::Mcp(source.to_string()))?;
943    let result = runtime.block_on(async move {
944        server
945            .serve(rmcp::transport::stdio())
946            .await
947            .map_err(|source| CliError::Mcp(source.to_string()))?
948            .waiting()
949            .await
950            .map_err(|source| CliError::Mcp(source.to_string()))
951            .map(|_| ())
952    });
953    shutdown_server.seal_usage_instances_for_projects();
954    result
955}
956
957/// Return whether the generated RMCP router contains required tool families.
958pub(crate) fn required_mcp_surface_present() -> bool {
959    REQUIRED_MCP_TOOL_NAMES
960        .iter()
961        .all(|name| mcp_tool_route_present(name))
962}
963
964/// Return whether the generated RMCP router has a concrete tool route.
965pub(crate) fn mcp_tool_route_present(name: &str) -> bool {
966    ProjectAtlasMcpServer::tool_router().has_route(name)
967}
968
969/// MCP parameter payload for scanning and symbol refresh.
970#[derive(Debug, Deserialize, schemars::JsonSchema)]
971struct AtlasScanParams {
972    /// Optional project root for this call. Defaults to the active MCP project.
973    project_path: Option<String>,
974    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
975    worktree: Option<String>,
976    /// Repository root path. Defaults to the configured or indexed project root.
977    path: Option<String>,
978    /// Opt in to nearest indexed `ProjectAtlas` project discovery for absolute paths.
979    nearest_project: Option<bool>,
980    /// Maximum file size to parse for symbols.
981    max_bytes: Option<u64>,
982    /// Maximum parser worker threads.
983    max_workers: Option<usize>,
984    /// Cooperative indexing-work budget in seconds (30-minute default and ceiling).
985    /// Larger representable budgets are clamped; unrepresentable deadlines fail closed.
986    /// Covers root/config/store setup after work-control creation, traversal, source hashing,
987    /// parsing and publication checkpoints. Earlier startup/target selection is outside this budget.
988    timeout_seconds: Option<u64>,
989    /// Maximum UTF-8 file size persisted into `SQLite` text search.
990    text_index_max_bytes: Option<u64>,
991    /// Run indexing in the bounded session task registry and return immediately.
992    background: Option<bool>,
993}
994
995/// MCP parameter payload for one-shot watcher refresh.
996#[derive(Debug, Deserialize, schemars::JsonSchema)]
997struct AtlasWatchOnceParams {
998    /// Optional project root for this call. Defaults to the active MCP project.
999    project_path: Option<String>,
1000    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
1001    worktree: Option<String>,
1002    /// Repository root path. Defaults to the configured or indexed project root.
1003    path: Option<String>,
1004    /// Opt in to nearest indexed `ProjectAtlas` project discovery for absolute paths.
1005    nearest_project: Option<bool>,
1006    /// Maximum parser worker threads.
1007    max_workers: Option<usize>,
1008    /// Cooperative indexing-work budget for this refresh in seconds (30-minute default and ceiling).
1009    /// Larger representable budgets are clamped; unrepresentable deadlines fail closed.
1010    /// Covers root/config/store setup after work-control creation, traversal, source hashing,
1011    /// parsing and publication checkpoints. Earlier startup/target selection is outside this budget.
1012    timeout_seconds: Option<u64>,
1013    /// Maximum UTF-8 file size persisted into `SQLite` text search.
1014    text_index_max_bytes: Option<u64>,
1015    /// Run indexing in the bounded session task registry and return immediately.
1016    background: Option<bool>,
1017}
1018
1019/// MCP parameter payload for ranked node lookup.
1020#[derive(Debug, Deserialize, schemars::JsonSchema)]
1021struct AtlasQueryParams {
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    /// Maximum number of rows to return.
1029    limit: Option<usize>,
1030}
1031
1032/// MCP parameter payload for classified next-step recommendations.
1033#[derive(Debug, Deserialize, schemars::JsonSchema)]
1034struct AtlasNextParams {
1035    /// Optional project root for this call. Defaults to the active MCP project.
1036    project_path: Option<String>,
1037    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
1038    worktree: Option<String>,
1039    /// Search query for path and purpose matching.
1040    query: Option<String>,
1041    /// Optional classified-content selection: source, documentation, or both.
1042    content_selection: Option<String>,
1043    /// Maximum number of rows to return.
1044    limit: Option<usize>,
1045}
1046
1047/// MCP parameter payload for ranked file lookup with optional absolute folder routing.
1048#[derive(Debug, Deserialize, schemars::JsonSchema)]
1049struct AtlasFilesParams {
1050    /// Optional project root for this call. Defaults to the active MCP project.
1051    project_path: Option<String>,
1052    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
1053    worktree: Option<String>,
1054    /// Search query for path and purpose matching.
1055    query: Option<String>,
1056    /// Folder path to constrain file lookup.
1057    folder: Option<String>,
1058    /// Opt in to nearest indexed `ProjectAtlas` project discovery for absolute folder paths.
1059    nearest_project: Option<bool>,
1060    /// Optional repository-relative glob filter.
1061    file_pattern: Option<String>,
1062    /// Include indexed file text as a bounded fallback ranking signal.
1063    include_content: Option<bool>,
1064    /// Optional classified-content selection: source, documentation, or both.
1065    content_selection: Option<String>,
1066    /// Maximum number of rows to return.
1067    limit: Option<usize>,
1068}
1069
1070/// MCP parameter payload for outlining a file.
1071#[derive(Debug, Deserialize, schemars::JsonSchema)]
1072struct AtlasOutlineParams {
1073    /// Optional project root for this call. Defaults to the active MCP project.
1074    project_path: Option<String>,
1075    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
1076    worktree: Option<String>,
1077    /// Repository-relative file path.
1078    file: String,
1079    /// Opt in to nearest indexed `ProjectAtlas` project discovery for absolute file paths.
1080    nearest_project: Option<bool>,
1081    /// Number of non-empty preview lines to include.
1082    lines: Option<usize>,
1083}
1084
1085/// MCP parameter payload for deterministic file summaries.
1086#[derive(Debug, Deserialize, schemars::JsonSchema)]
1087struct AtlasFileSummaryParams {
1088    /// Optional project root for this call. Defaults to the active MCP project.
1089    project_path: Option<String>,
1090    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
1091    worktree: Option<String>,
1092    /// Repository-relative file path.
1093    file: String,
1094    /// Opt in to nearest indexed `ProjectAtlas` project discovery for absolute file paths.
1095    nearest_project: Option<bool>,
1096    /// Return the additive compact summary projection when true.
1097    compact: Option<bool>,
1098    /// Optional classified-content selection: source, documentation, or both.
1099    content_selection: Option<String>,
1100    /// Maximum rows per functions/methods/classes/types/calls section.
1101    limit: Option<usize>,
1102}
1103
1104/// MCP parameter payload for text search.
1105#[derive(Debug, Deserialize, schemars::JsonSchema)]
1106struct AtlasSearchParams {
1107    /// Optional project root for this call. Defaults to the active MCP project.
1108    project_path: Option<String>,
1109    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
1110    worktree: Option<String>,
1111    /// Literal, regex, or fuzzy pattern to search for.
1112    pattern: String,
1113    /// Retrieval family; lexical is the default and always-available mode.
1114    retrieval_mode: Option<SearchRetrievalModeArg>,
1115    /// Treat the pattern as a regex.
1116    regex: Option<bool>,
1117    /// Treat the pattern as a fuzzy subsequence.
1118    fuzzy: Option<bool>,
1119    /// Match case-sensitively.
1120    case_sensitive: Option<bool>,
1121    /// Optional repository-relative glob filter.
1122    file_pattern: Option<String>,
1123    /// Number of context lines before and after a match.
1124    context_lines: Option<usize>,
1125    /// Pagination start index.
1126    start_index: Option<usize>,
1127    /// Optional classified-content selection: source, documentation, or both.
1128    content_selection: Option<String>,
1129    /// Maximum matches to return.
1130    limit: Option<usize>,
1131}
1132
1133/// MCP parameter payload for exact source slices.
1134#[derive(Debug, Deserialize, schemars::JsonSchema)]
1135struct AtlasSliceParams {
1136    /// Optional project root for this call. Defaults to the active MCP project.
1137    project_path: Option<String>,
1138    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
1139    worktree: Option<String>,
1140    /// Repository-relative file path.
1141    file: String,
1142    /// Opt in to nearest indexed `ProjectAtlas` project discovery for absolute file paths.
1143    nearest_project: Option<bool>,
1144    /// One-based start line when no symbol is supplied.
1145    start_line: Option<usize>,
1146    /// Optional one-based end line.
1147    end_line: Option<usize>,
1148    /// Symbol name to slice instead of line numbers.
1149    symbol: Option<String>,
1150    /// Optional parent symbol for disambiguating `symbol`.
1151    symbol_parent: Option<String>,
1152    /// Optional symbol kind for disambiguating `symbol`.
1153    symbol_kind: Option<String>,
1154    /// Optional exact signature for disambiguating `symbol`.
1155    symbol_signature: Option<String>,
1156    /// Optional source line for disambiguating `symbol`.
1157    symbol_line: Option<usize>,
1158    /// Optional classified-content selection: source, documentation, or both.
1159    content_selection: Option<String>,
1160    /// Maximum encoded bytes admitted to the slice response.
1161    output_bytes: Option<u32>,
1162}
1163
1164/// MCP parameter payload for symbol and relation lookup.
1165#[derive(Debug, Deserialize, schemars::JsonSchema)]
1166struct AtlasSymbolsParams {
1167    /// Optional project root for this call. Defaults to the active MCP project.
1168    project_path: Option<String>,
1169    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
1170    worktree: Option<String>,
1171    /// Optional repository-relative file path.
1172    file: Option<String>,
1173    /// Opt in to nearest indexed `ProjectAtlas` project discovery for absolute file paths.
1174    nearest_project: Option<bool>,
1175    /// Optional symbol, signature, relation, or path query.
1176    query: Option<String>,
1177    /// Optional classified-content selection: source, documentation, or both.
1178    content_selection: Option<String>,
1179    /// Maximum rows to return.
1180    limit: Option<usize>,
1181}
1182
1183/// MCP parameters for legacy, detailed, or closed-analysis relation navigation.
1184#[derive(Debug, Default, Deserialize, schemars::JsonSchema)]
1185struct AtlasSymbolRelationsParams {
1186    /// Optional project root for this call. Defaults to the active MCP project.
1187    project_path: Option<String>,
1188    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
1189    worktree: Option<String>,
1190    /// Optional repository-relative file path.
1191    file: Option<String>,
1192    /// Opt in to nearest indexed `ProjectAtlas` project discovery for absolute file paths.
1193    nearest_project: Option<bool>,
1194    /// Optional symbol, signature, relation, or path query.
1195    query: Option<String>,
1196    /// Anchored `detailed` by default; explicit `legacy` returns complete small arrays; `analysis` is opt-in.
1197    view: Option<String>,
1198    /// Optional classified-content selection: source, documentation, or both.
1199    content_selection: Option<String>,
1200    /// Return the opt-in compact detailed projection while preserving exact selectors and trust.
1201    compact: Option<bool>,
1202    /// Resume one exact generation- and purpose-bound detailed page.
1203    cursor: Option<String>,
1204    /// Complete ordered project-root set for one read-only federated call.
1205    roots: Option<Vec<String>>,
1206    /// Complete ordered registered worktree aliases for one read-only federated call.
1207    worktrees: Option<Vec<String>>,
1208    /// Exact symbol name used as the detailed anchor; omit for a file anchor.
1209    symbol: Option<String>,
1210    /// Optional exact parent used to disambiguate the detailed symbol anchor.
1211    symbol_parent: Option<String>,
1212    /// Optional exact kind used to disambiguate the detailed symbol anchor.
1213    symbol_kind: Option<String>,
1214    /// Optional exact signature used to disambiguate the detailed symbol anchor.
1215    symbol_signature: Option<String>,
1216    /// Detailed traversal direction: `outbound` or `inbound`.
1217    direction: Option<String>,
1218    /// Optional exact legacy or extended relation family.
1219    relation: Option<String>,
1220    /// Detailed confidence floor: `exact`, `high`, `medium`, or `low`.
1221    minimum_confidence: Option<String>,
1222    /// Detailed resolution filter.
1223    resolution: Option<String>,
1224    /// Maximum detailed traversal depth.
1225    depth: Option<u32>,
1226    /// Retain bounded exact source occurrences in detailed rows.
1227    include_occurrences: Option<bool>,
1228    /// Maximum exact occurrences retained per detailed relation.
1229    occurrence_limit: Option<u32>,
1230    /// Maximum adjacency rows inspected by one detailed page.
1231    edge_limit: Option<u32>,
1232    /// Maximum unique traversal nodes retained across continuation state.
1233    node_limit: Option<u32>,
1234    /// Maximum unique visited-node state retained across continuation state.
1235    visited_limit: Option<u32>,
1236    /// Maximum exact occurrences retained across the complete page.
1237    occurrence_total_limit: Option<u32>,
1238    /// Maximum decoded, cursor, and service-composition intermediate bytes.
1239    intermediate_bytes: Option<u64>,
1240    /// Maximum service-owned elapsed milliseconds.
1241    deadline_ms: Option<u64>,
1242    /// Encoded bytes: detailed/legacy default 32768; analysis default 262144; legacy maximum 262144.
1243    output_bytes: Option<u32>,
1244    /// Closed analysis mode: `architecture`, `impact`, `trace`, or `entrypoint`.
1245    analysis_mode: Option<String>,
1246    /// Stable name for an explicit entrypoint profile.
1247    profile_name: Option<String>,
1248    /// JSON `RelationAnchor` values; repeat for multiple entrypoints.
1249    entrypoints: Option<Vec<String>>,
1250    /// Relation families admitted by an entrypoint profile.
1251    profile_relations: Option<Vec<String>>,
1252    /// Exact target symbol name for trace mode.
1253    trace_target: Option<String>,
1254    /// Exact target file for trace mode; alone selects a file target.
1255    trace_target_file: Option<String>,
1256    /// Exact target parent for symbol trace mode.
1257    trace_target_parent: Option<String>,
1258    /// Exact target kind required by symbol trace mode.
1259    trace_target_kind: Option<String>,
1260    /// Exact target signature required by symbol trace mode.
1261    trace_target_signature: Option<String>,
1262    /// Impact VCS scope: `working_tree`, `index`, or `revision_range`.
1263    vcs: Option<String>,
1264    /// Older Git revision used by `revision_range`.
1265    vcs_base: Option<String>,
1266    /// Newer Git revision used by `revision_range`.
1267    vcs_head: Option<String>,
1268    /// Include communities with containment excluded.
1269    include_communities: Option<bool>,
1270    /// Include dependency SCC findings.
1271    include_cycles: Option<bool>,
1272    /// Include conservative dead-code candidates.
1273    include_dead_code: Option<bool>,
1274    /// Row limit: detailed defaults to 10; explicit legacy/analysis to 50; legacy maximum 200.
1275    limit: Option<usize>,
1276}
1277
1278/// Return whether any closed analysis-only control was supplied.
1279fn relation_analysis_controls_present(params: &AtlasSymbolRelationsParams) -> bool {
1280    params.analysis_mode.is_some()
1281        || params.profile_name.is_some()
1282        || params
1283            .entrypoints
1284            .as_ref()
1285            .is_some_and(|items| !items.is_empty())
1286        || params
1287            .profile_relations
1288            .as_ref()
1289            .is_some_and(|items| !items.is_empty())
1290        || params.trace_target.is_some()
1291        || params.trace_target_file.is_some()
1292        || params.trace_target_parent.is_some()
1293        || params.trace_target_kind.is_some()
1294        || params.trace_target_signature.is_some()
1295        || params.vcs.is_some()
1296        || params.vcs_base.is_some()
1297        || params.vcs_head.is_some()
1298        || params.include_communities.is_some()
1299        || params.include_cycles.is_some()
1300        || params.include_dead_code.is_some()
1301}
1302
1303/// Decode and validate the optional exact trace target.
1304fn relation_analysis_trace_target(
1305    store: &AtlasStore,
1306    params: &AtlasSymbolRelationsParams,
1307) -> Result<Option<RelationAnchor>, CliError> {
1308    match (&params.trace_target, &params.trace_target_file) {
1309        (Some(name), Some(file)) => {
1310            let file = validated_indexed_file_key(store, Path::new(file))?;
1311            let kind = params.trace_target_kind.as_deref().ok_or_else(|| {
1312                CliError::Service(ServiceError::InvalidInput(
1313                    MCP_ERROR_TRACE_TARGET_KIND_REQUIRED.to_string(),
1314                ))
1315            })?;
1316            let signature = params.trace_target_signature.clone().ok_or_else(|| {
1317                CliError::Service(ServiceError::InvalidInput(
1318                    MCP_ERROR_TRACE_TARGET_SIGNATURE_REQUIRED.to_string(),
1319                ))
1320            })?;
1321            Ok(Some(RelationAnchor::Symbol {
1322                file: RepositoryFilePath::new(Path::new(&file)).map_err(|error| {
1323                    CliError::Service(ServiceError::InvalidInput(error.to_string()))
1324                })?,
1325                name: name.clone(),
1326                symbol_kind: Some(parse_symbol_kind(kind)?),
1327                parent: params.trace_target_parent.clone(),
1328                signature: Some(signature),
1329            }))
1330        }
1331        (None, Some(file)) => {
1332            if params.trace_target_parent.is_some()
1333                || params.trace_target_kind.is_some()
1334                || params.trace_target_signature.is_some()
1335            {
1336                return Err(CliError::Service(ServiceError::InvalidInput(
1337                    MCP_ERROR_TRACE_TARGET_REQUIRED.to_string(),
1338                )));
1339            }
1340            let file = validated_indexed_file_key(store, Path::new(file))?;
1341            Ok(Some(RelationAnchor::File {
1342                file: RepositoryFilePath::new(Path::new(&file)).map_err(|error| {
1343                    CliError::Service(ServiceError::InvalidInput(error.to_string()))
1344                })?,
1345            }))
1346        }
1347        (Some(_), None) => Err(CliError::Service(ServiceError::InvalidInput(
1348            MCP_ERROR_TRACE_TARGET_FILE_REQUIRED.to_string(),
1349        ))),
1350        (None, None) => Ok(None),
1351    }
1352}
1353
1354/// Decode and validate the optional VCS impact selector.
1355fn relation_analysis_vcs(
1356    params: &AtlasSymbolRelationsParams,
1357) -> Result<GitImpactSelection, CliError> {
1358    match params
1359        .vcs
1360        .as_deref()
1361        .unwrap_or(MCP_RELATION_ANALYSIS_VCS_WORKING_TREE)
1362    {
1363        MCP_RELATION_ANALYSIS_VCS_WORKING_TREE => {
1364            if params.vcs_base.is_some() || params.vcs_head.is_some() {
1365                return Err(CliError::Service(ServiceError::InvalidInput(
1366                    MCP_ERROR_VCS_REVISION_FIELDS.to_string(),
1367                )));
1368            }
1369            Ok(GitImpactSelection::WorkingTree)
1370        }
1371        MCP_RELATION_ANALYSIS_VCS_INDEX => {
1372            if params.vcs_base.is_some() || params.vcs_head.is_some() {
1373                return Err(CliError::Service(ServiceError::InvalidInput(
1374                    MCP_ERROR_VCS_REVISION_FIELDS.to_string(),
1375                )));
1376            }
1377            Ok(GitImpactSelection::Index)
1378        }
1379        MCP_RELATION_ANALYSIS_VCS_REVISION_RANGE => Ok(GitImpactSelection::RevisionRange {
1380            base: params.vcs_base.clone().ok_or_else(|| {
1381                CliError::Service(ServiceError::InvalidInput(
1382                    MCP_ERROR_VCS_BASE_REQUIRED.to_string(),
1383                ))
1384            })?,
1385            head: params.vcs_head.clone().ok_or_else(|| {
1386                CliError::Service(ServiceError::InvalidInput(
1387                    MCP_ERROR_VCS_HEAD_REQUIRED.to_string(),
1388                ))
1389            })?,
1390        }),
1391        _unsupported => Err(CliError::Service(ServiceError::InvalidInput(
1392            MCP_ERROR_UNSUPPORTED_ANALYSIS_VCS.to_string(),
1393        ))),
1394    }
1395}
1396
1397/// MCP parameter payload for token savings reports.
1398#[derive(Debug, Deserialize, schemars::JsonSchema)]
1399struct AtlasTokenParams {
1400    /// Optional project root for this call. Defaults to the active MCP project.
1401    project_path: Option<String>,
1402    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
1403    worktree: Option<String>,
1404    /// Optional session id filter.
1405    session: Option<String>,
1406    /// Include a readable ASCII chart in the MCP result.
1407    include_chart: Option<bool>,
1408    /// Optional trend grouping window: day, week, month, or year.
1409    trend_window: Option<String>,
1410    /// Optional repository-relative agent-navigation benchmark result.
1411    benchmark_results: Option<String>,
1412    /// Optional chart theme for TUI output: dark or light.
1413    theme: Option<String>,
1414}
1415
1416/// MCP parameter payload for bounded health finding lookup.
1417#[derive(Debug, Deserialize, schemars::JsonSchema)]
1418struct AtlasHealthParams {
1419    /// Optional project root for this call. Defaults to the active MCP project.
1420    project_path: Option<String>,
1421    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
1422    worktree: Option<String>,
1423    /// Pagination start index after filters are applied.
1424    start_index: Option<usize>,
1425    /// Maximum findings to return, capped to a safe MCP page size.
1426    limit: Option<usize>,
1427    /// Optional finding category filter.
1428    category: Option<String>,
1429    /// Optional severity filter: info, warning, or error.
1430    severity: Option<String>,
1431    /// Optional repository-relative primary or related path prefix.
1432    path_prefix: Option<String>,
1433    /// Return counts and paging metadata without finding rows.
1434    summary_only: Option<bool>,
1435    /// Restrict findings to source files and folders that contain source files.
1436    source_only: Option<bool>,
1437    /// Include non-source files and asset-only folders in the purpose queue.
1438    include_assets: Option<bool>,
1439    /// Include low-priority files in the purpose queue.
1440    include_low_priority_files: Option<bool>,
1441    /// Opt in to bounded current coverage discovery instead of structural findings.
1442    coverage: Option<bool>,
1443    /// Optional source parser coverage filter.
1444    parser: Option<String>,
1445    /// Optional derived-fact provider coverage filter.
1446    provider: Option<String>,
1447    /// Optional relationship-family coverage filter.
1448    relation: Option<String>,
1449    /// Optional complete, partial, failed, ignored, oversized, quarantined, or stale filter.
1450    coverage_state: Option<String>,
1451    /// Optional exact coverage reason filter.
1452    reason: Option<String>,
1453}
1454
1455/// MCP parameter payload for bounded task-scoped purpose curation.
1456#[derive(Debug, Deserialize, schemars::JsonSchema)]
1457struct AtlasPurposeQueueParams {
1458    /// Shared health paging and purpose-scope filters.
1459    #[serde(flatten)]
1460    health: AtlasHealthParams,
1461    /// Host-owned task label for deterministic purpose-curator work identity.
1462    task: Option<String>,
1463}
1464
1465/// MCP parameter payload for parity reports.
1466#[derive(Debug, Deserialize, schemars::JsonSchema)]
1467struct AtlasParityParams {
1468    /// Optional project root for this call. Defaults to the active MCP project.
1469    project_path: Option<String>,
1470    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
1471    worktree: Option<String>,
1472    /// Parity profile. Defaults to repository-intelligence.
1473    profile: Option<String>,
1474}
1475
1476/// MCP parameter payload for legacy purpose cleanup.
1477#[derive(Debug, Deserialize, schemars::JsonSchema)]
1478struct AtlasStripLegacyParams {
1479    /// Optional project root for this call. Defaults to the active MCP project.
1480    project_path: Option<String>,
1481    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
1482    worktree: Option<String>,
1483    /// Repository root path. Defaults to the configured or indexed project root.
1484    path: Option<String>,
1485    /// Opt in to nearest indexed `ProjectAtlas` project discovery for absolute paths.
1486    nearest_project: Option<bool>,
1487    /// Remove legacy `.purpose` files when true.
1488    apply: Option<bool>,
1489    /// Preview cleanup without modifying files.
1490    dry_run: Option<bool>,
1491    /// Also report conservative source Purpose header candidates.
1492    strip_source_headers: Option<bool>,
1493}
1494
1495/// MCP parameter payload for runtime index cleanup.
1496#[derive(Debug, Deserialize, schemars::JsonSchema)]
1497struct AtlasResetIndexParams {
1498    /// Optional project root for this call. Defaults to the active MCP project.
1499    project_path: Option<String>,
1500    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
1501    worktree: Option<String>,
1502    /// Remove runtime index/cache files when true.
1503    apply: Option<bool>,
1504    /// Preview cleanup without modifying files.
1505    dry_run: Option<bool>,
1506    /// Also remove generated project-local MCP config.
1507    include_mcp_config: Option<bool>,
1508}
1509
1510/// MCP parameter payload for setting purpose metadata.
1511#[derive(Debug, Deserialize, schemars::JsonSchema)]
1512struct AtlasPurposeSetParams {
1513    /// Optional project root for this call. Defaults to the active MCP project.
1514    project_path: Option<String>,
1515    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
1516    worktree: Option<String>,
1517    /// Indexed repository-relative path.
1518    path: String,
1519    /// Agent-approved purpose one-liner.
1520    purpose: String,
1521}
1522
1523/// MCP payload for one batch purpose review item.
1524#[derive(Debug, Deserialize, schemars::JsonSchema)]
1525#[schemars(inline)]
1526struct AtlasPurposeReviewItem {
1527    /// Indexed repository-relative path.
1528    path: String,
1529    /// Agent-reviewed purpose one-liner. Required for generated suggestions.
1530    purpose: Option<String>,
1531    /// Confirm the existing non-generated purpose after inspection.
1532    confirm_existing: Option<bool>,
1533    /// Queue task copied from the purpose-curation item.
1534    task: Option<String>,
1535    /// Queue work key copied from the purpose-curation item.
1536    work_key: Option<String>,
1537    /// Queue state token copied from the purpose-curation item.
1538    state_token: Option<String>,
1539}
1540
1541/// MCP parameter payload for batch purpose review.
1542#[derive(Debug, Deserialize, schemars::JsonSchema)]
1543struct AtlasPurposeReviewParams {
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    /// Purpose records to agent-review.
1549    items: Vec<AtlasPurposeReviewItem>,
1550    /// Apply reviewed purposes. Defaults to false for preview.
1551    apply: Option<bool>,
1552}
1553
1554/// MCP parameter payload for resolving health findings.
1555#[derive(Debug, Deserialize, schemars::JsonSchema)]
1556struct AtlasHealthResolveParams {
1557    /// Optional project root for this call. Defaults to the active MCP project.
1558    project_path: Option<String>,
1559    /// Optional registered worktree alias for this call. Mutually exclusive with `project_path`.
1560    worktree: Option<String>,
1561    /// Stable finding id from `atlas_health`.
1562    finding_id: String,
1563    /// Finding category.
1564    category: String,
1565    /// Primary path.
1566    path: String,
1567    /// Optional related path.
1568    related_path: Option<String>,
1569    /// Agent rationale for resolving the finding.
1570    rationale: String,
1571}
1572
1573/// Active `ProjectAtlas` database and configuration selected for MCP calls.
1574#[derive(Debug, Clone)]
1575struct McpProjectState {
1576    /// Canonical selected repository root.
1577    root: PathBuf,
1578    /// Selected durable `SQLite` index path.
1579    db_path: PathBuf,
1580    /// Selected scan/import configuration path.
1581    config_path: Option<PathBuf>,
1582    /// Registered alias captured for this call, when alias routing selected the state.
1583    worktree: Option<McpWorktreeSelection>,
1584}
1585
1586/// Stable control-catalog identity captured with one alias-routed project state.
1587#[derive(Debug, Clone, Eq, PartialEq)]
1588struct McpWorktreeSelection {
1589    /// Reserved `main` or registered short alias.
1590    alias: String,
1591    /// Durable registration identity; absent only for reserved `main`.
1592    registration_id: Option<i64>,
1593    /// Project identity captured for the selected alias target, when present.
1594    project_instance_id: Option<ProjectInstanceId>,
1595    /// Control-atlas identity that owns the captured registration ID.
1596    control_project_instance_id: Option<ProjectInstanceId>,
1597}
1598
1599/// Outcome of evaluating the control atlas as an init hydration source.
1600enum McpWorktreeHydration {
1601    /// One target-exact candidate was reconciled and activated.
1602    Activated {
1603        /// Public init diagnostic.
1604        hydration: InitHydrationPhase,
1605        /// Normal scan result produced against the private candidate.
1606        scan: Box<ScanReport>,
1607    },
1608    /// Ordinary init must be used and disclose this reason.
1609    Fallback(String),
1610}
1611
1612/// Store ownership selected by the existing relation tool request shape.
1613enum SymbolRelationStores<'a> {
1614    /// Compatibility-preserving selected-project query.
1615    Single(&'a AtlasStore),
1616    /// Explicit call-owned ordered read snapshots.
1617    Federated(Vec<FederatedStore>),
1618}
1619
1620impl SymbolRelationStores<'_> {
1621    /// Borrow the selected first project while validating local selectors.
1622    fn primary(&self) -> &AtlasStore {
1623        match self {
1624            Self::Single(store) => store,
1625            Self::Federated(stores) => stores[0].store(),
1626        }
1627    }
1628}
1629
1630/// Exact root/database/project identity where this MCP process recorded telemetry.
1631#[derive(Debug, Clone, Eq, Hash, PartialEq)]
1632struct McpUsageProjectBinding {
1633    /// Canonical selected repository root.
1634    root: PathBuf,
1635    /// Exact authoritative database used for the telemetry write.
1636    db_path: PathBuf,
1637    /// Project identity captured by the already-open selected store.
1638    project_instance_id: ProjectInstanceId,
1639    /// Captured routed origin; absent for native events and source baselines.
1640    worktree_registration_id: Option<i64>,
1641}
1642
1643/// One bounded broad-source token baseline keyed to a complete generation.
1644#[derive(Clone, Debug, Eq, Hash, PartialEq)]
1645struct McpSourceTokenBaselineKey {
1646    /// Exact project binding whose source files supplied the baseline.
1647    binding: McpUsageProjectBinding,
1648    /// Complete publication generation represented by the baseline.
1649    generation: projectatlas_core::IndexGeneration,
1650    /// Optional repository folder filter applied to the baseline.
1651    folder: Option<String>,
1652    /// Optional repository file-pattern filter applied to the baseline.
1653    file_pattern: Option<String>,
1654}
1655
1656/// Deferred telemetry payload recorded only after a verified result is accepted.
1657#[derive(Debug)]
1658struct McpUsageIntent {
1659    /// Stable MCP event family.
1660    command: &'static str,
1661    /// Optional selected path or filter.
1662    path: Option<String>,
1663    /// Optional caller query.
1664    query: Option<String>,
1665    /// Baseline used by the existing usage accounting contract.
1666    baseline: McpUsageBaseline,
1667}
1668
1669/// Existing telemetry baseline variants retained across verified-read acceptance.
1670#[derive(Debug)]
1671enum McpUsageBaseline {
1672    /// Modeled selected-candidate token count.
1673    Estimate(usize),
1674    /// Modeled avoided directory-walk token count.
1675    DirectoryWalk(usize),
1676    /// Exact source text replaced by the accepted response.
1677    Text(String),
1678}
1679
1680impl McpUsageIntent {
1681    /// Defer one selected-candidate token estimate until result acceptance.
1682    fn estimate(
1683        command: &'static str,
1684        path: Option<String>,
1685        query: Option<String>,
1686        baseline_tokens: usize,
1687    ) -> Self {
1688        Self {
1689            command,
1690            path,
1691            query,
1692            baseline: McpUsageBaseline::Estimate(baseline_tokens),
1693        }
1694    }
1695
1696    /// Defer one avoided directory-walk estimate until result acceptance.
1697    fn directory_walk(
1698        command: &'static str,
1699        path: Option<String>,
1700        query: Option<String>,
1701        baseline_tokens: usize,
1702    ) -> Self {
1703        Self {
1704            command,
1705            path,
1706            query,
1707            baseline: McpUsageBaseline::DirectoryWalk(baseline_tokens),
1708        }
1709    }
1710
1711    /// Defer one exact source-text replacement event until result acceptance.
1712    fn text(command: &'static str, path: Option<String>, baseline_text: String) -> Self {
1713        Self {
1714            command,
1715            path,
1716            query: None,
1717            baseline: McpUsageBaseline::Text(baseline_text),
1718        }
1719    }
1720}
1721
1722impl McpUsageProjectBinding {
1723    /// Capture one native telemetry binding in focused tests.
1724    #[cfg(test)]
1725    fn capture(state: &McpProjectState, store: &AtlasStore) -> Result<Self, DbError> {
1726        Self::capture_with_origin(state, store, None)
1727    }
1728
1729    /// Capture one exact telemetry authority and optional registered origin.
1730    fn capture_with_origin(
1731        state: &McpProjectState,
1732        store: &AtlasStore,
1733        worktree_registration_id: Option<i64>,
1734    ) -> Result<Self, DbError> {
1735        let captured = store.captured_project_binding()?;
1736        Ok(Self {
1737            root: state.root.clone(),
1738            db_path: state.db_path.clone(),
1739            project_instance_id: captured.project_instance_id,
1740            worktree_registration_id,
1741        })
1742    }
1743
1744    /// Return whether two origin bindings share one exact project database authority.
1745    fn same_project(&self, other: &Self) -> bool {
1746        self.root == other.root
1747            && self.db_path == other.db_path
1748            && self.project_instance_id == other.project_instance_id
1749    }
1750}
1751
1752/// Current telemetry identity for one exact selected project binding.
1753#[derive(Clone, Debug)]
1754struct McpUsageProjectRuntime {
1755    /// Exact root/database authority that owns this identity.
1756    binding: McpUsageProjectBinding,
1757    /// Current bounded identity for this process/project pair.
1758    instance: Arc<Mutex<UsageRuntimeInstance>>,
1759}
1760
1761/// Mutable bounded telemetry lifecycles shared by all MCP server clones.
1762#[derive(Debug, Default)]
1763struct McpUsageRuntime {
1764    /// Distinct selected-project identities owned by this MCP process.
1765    entries: Vec<McpUsageProjectRuntime>,
1766    /// Bounded modeled baselines reused without decoding every indexed file per call.
1767    source_token_baselines: VecDeque<(McpSourceTokenBaselineKey, usize)>,
1768}
1769
1770impl McpUsageRuntime {
1771    /// Return or create the identity for one selected project binding.
1772    fn instance_for_binding(
1773        &mut self,
1774        binding: McpUsageProjectBinding,
1775        selected_store: &AtlasStore,
1776    ) -> Option<Arc<Mutex<UsageRuntimeInstance>>> {
1777        if let Some(index) = self
1778            .entries
1779            .iter()
1780            .position(|entry| entry.binding == binding)
1781        {
1782            let entry = self.entries.remove(index);
1783            let instance = Arc::clone(&entry.instance);
1784            self.entries.push(entry);
1785            return Some(instance);
1786        }
1787        let instance = Arc::new(Mutex::new(UsageRuntimeInstance::new(
1788            UsageInstanceOwner::McpProcess,
1789        )?));
1790        if self.entries.len() >= MCP_TELEMETRY_PROJECT_BINDING_LIMIT {
1791            let index = (0..self.entries.len()).find(|index| {
1792                Self::seal_inactive_entry(&self.entries[*index], &binding, selected_store)
1793            })?;
1794            self.entries.remove(index);
1795        }
1796        self.entries.push(McpUsageProjectRuntime {
1797            binding,
1798            instance: Arc::clone(&instance),
1799        });
1800        Some(instance)
1801    }
1802
1803    /// Seal one unborrowed least-recent binding before bounded replacement.
1804    fn seal_inactive_entry(
1805        entry: &McpUsageProjectRuntime,
1806        selected_binding: &McpUsageProjectBinding,
1807        selected_store: &AtlasStore,
1808    ) -> bool {
1809        if Arc::strong_count(&entry.instance) != 1 {
1810            return false;
1811        }
1812        let Ok(instance) = entry.instance.try_lock() else {
1813            return false;
1814        };
1815        let seal = |store: &AtlasStore| {
1816            store.captured_project_binding().is_ok_and(|binding| {
1817                binding.project_instance_id == entry.binding.project_instance_id
1818            }) && matches!(
1819                (*instance).seal(store),
1820                Ok(()) | Err(CliError::Db(DbError::TelemetryInstanceInactive))
1821            )
1822        };
1823        if entry.binding.same_project(selected_binding) {
1824            return seal(selected_store);
1825        }
1826        open_atlas_store_for_project(&entry.binding.db_path, &entry.binding.root)
1827            .is_ok_and(|store| seal(&store))
1828    }
1829
1830    /// Clone the bounded project/runtime set before shutdown database I/O.
1831    fn snapshot(&self) -> Vec<McpUsageProjectRuntime> {
1832        self.entries.clone()
1833    }
1834
1835    /// Return a cached generation-bound broad-source token baseline.
1836    fn source_token_baseline(&self, key: &McpSourceTokenBaselineKey) -> Option<usize> {
1837        self.source_token_baselines
1838            .iter()
1839            .find_map(|(candidate, value)| (candidate == key).then_some(*value))
1840    }
1841
1842    /// Retain one baseline without allowing arbitrary filter keys to grow memory.
1843    fn insert_source_token_baseline(&mut self, key: McpSourceTokenBaselineKey, value: usize) {
1844        if let Some(index) = self
1845            .source_token_baselines
1846            .iter()
1847            .position(|(candidate, _value)| candidate == &key)
1848        {
1849            let _removed = self.source_token_baselines.remove(index);
1850        }
1851        while self.source_token_baselines.len() >= MCP_SOURCE_TOKEN_BASELINE_LIMIT {
1852            let _removed = self.source_token_baselines.pop_front();
1853        }
1854        self.source_token_baselines.push_back((key, value));
1855    }
1856}
1857
1858/// Select whether project-state discovery validates configuration content immediately.
1859#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1860enum McpConfigValidation {
1861    /// Validate configuration before returning selected project state.
1862    Immediate,
1863    /// Defer configuration reads to the admitted operation-owned work boundary.
1864    Deferred,
1865}
1866
1867/// MCP response for the current map and optional JSON sidecar.
1868#[derive(Debug, Serialize)]
1869struct McpMapReport {
1870    /// Canonical project root used for map generation.
1871    root: Option<String>,
1872    /// Map path from the effective config.
1873    map_path: Option<String>,
1874    /// Legacy TOON output is never written by this call.
1875    written: bool,
1876    /// Whether JSON map content and sidecar were requested.
1877    json: bool,
1878    /// Selected response format.
1879    format: crate::OutputFormat,
1880    /// Complete selected-format map content.
1881    content: String,
1882    /// Whether the adjacent JSON sidecar was written.
1883    json_written: bool,
1884    /// Human-readable reason when a requested sidecar was not written.
1885    skipped_reason: Option<String>,
1886}
1887
1888/// Bounded control-atlas view of Git and `ProjectAtlas` worktree state.
1889#[derive(Debug, Serialize)]
1890struct McpWorktreeListReport {
1891    /// Reserved alias for the selected control checkout.
1892    control_alias: &'static str,
1893    /// Exact selected control checkout root.
1894    control_root: Option<String>,
1895    /// Structurally validated Git common directory.
1896    common_directory: Option<String>,
1897    /// Active Git inventory, bounded for one agent response.
1898    worktrees: Vec<McpWorktreeRow>,
1899    /// Optional retired `ProjectAtlas` registrations retained for telemetry history.
1900    retired: Vec<McpRetiredWorktreeRow>,
1901    /// Complete structural row count before response truncation.
1902    total_worktrees: usize,
1903    /// Whether structural rows exceeded the public response bound.
1904    truncated: bool,
1905}
1906
1907/// One structurally discovered Git worktree joined to `ProjectAtlas` state.
1908#[derive(Debug, Serialize)]
1909struct McpWorktreeRow {
1910    /// Stable selector when the structural row is representable and registrable.
1911    selector: Option<String>,
1912    /// Reserved `main` or active registered alias when present.
1913    alias: Option<String>,
1914    /// Structural primary/linked role.
1915    role: McpGitWorktreeRole,
1916    /// Whether all displayed native paths have a lossless UTF-8 projection.
1917    path_display: McpWorktreePathDisplayState,
1918    /// Current reciprocal Git state.
1919    git_state: McpGitWorktreeState,
1920    /// `ProjectAtlas` registration relationship.
1921    registration: McpWorktreeRegistrationState,
1922    /// Stable Git administrative identity.
1923    administrative_directory: Option<String>,
1924    /// Exact current source root when active.
1925    root: Option<String>,
1926    /// Exact atlas availability for the current source root.
1927    atlas_state: McpWorktreeAtlasState,
1928    /// Local aggregate relationship to the accepted control snapshot.
1929    telemetry_state: McpWorktreeTelemetryState,
1930    /// Last local aggregate revision accepted by the control atlas.
1931    accepted_telemetry_revision: Option<u64>,
1932    /// Current local aggregate revision when safely readable.
1933    local_telemetry_revision: Option<u64>,
1934    /// Exact initialized project identity when safely readable.
1935    project_instance_id: Option<String>,
1936    /// Typed structural or atlas diagnostic for unavailable rows.
1937    blocker: Option<String>,
1938}
1939
1940/// Retired `ProjectAtlas` registration retained outside source selection.
1941#[derive(Debug, Serialize)]
1942struct McpRetiredWorktreeRow {
1943    /// Last human/agent-facing alias.
1944    alias: String,
1945    /// Last structurally validated source root.
1946    last_root: Option<String>,
1947    /// Whether the retained root has a lossless UTF-8 projection.
1948    path_display: McpWorktreePathDisplayState,
1949    /// Exact initialized project identity when one was bound.
1950    project_instance_id: Option<String>,
1951    /// Last local aggregate revision accepted before retirement.
1952    accepted_telemetry_revision: u64,
1953}
1954
1955/// Compact candidate returned for ambiguous or missing add selection.
1956#[derive(Debug, Serialize)]
1957struct McpWorktreeCandidate {
1958    /// Stable selector accepted by a later add call.
1959    selector: String,
1960    /// Exact active source root.
1961    root: Option<String>,
1962    /// Whether the candidate root has a lossless UTF-8 projection.
1963    path_display: McpWorktreePathDisplayState,
1964    /// Structural primary/linked role.
1965    role: McpGitWorktreeRole,
1966}
1967
1968/// Result of one ProjectAtlas-only worktree registration transition.
1969#[derive(Debug, Serialize)]
1970struct McpWorktreeMutationReport {
1971    /// Registration operation that was requested.
1972    operation: McpWorktreeMutationOperation,
1973    /// Mutation or bounded selection outcome.
1974    status: McpWorktreeMutationStatus,
1975    /// Stable structural selector when one candidate was selected.
1976    selector: Option<String>,
1977    /// Active or retired `ProjectAtlas` alias when known.
1978    alias: Option<String>,
1979    /// Exact source root when known.
1980    root: Option<String>,
1981    /// Display state for the selected native source root.
1982    path_display: Option<McpWorktreePathDisplayState>,
1983    /// Stable control-database registration identity when committed.
1984    registration_id: Option<i64>,
1985    /// Final local telemetry synchronization outcome when attempted.
1986    telemetry_sync: Option<WorktreeUsageSyncState>,
1987    /// Bounded candidates when a human selector was ambiguous.
1988    candidates: Vec<McpWorktreeCandidate>,
1989    /// Typed synchronization or structural note when the transition is partial by design.
1990    blocker: Option<String>,
1991    /// Git lifecycle is never changed by these operations.
1992    git_unchanged: bool,
1993    /// Source, `.projectatlas`, and database files are never deleted by unregister.
1994    files_unchanged: bool,
1995}
1996
1997/// Structural role serialized without leaking debug vocabulary.
1998#[derive(Clone, Copy, Debug, Serialize)]
1999#[serde(rename_all = "snake_case")]
2000enum McpGitWorktreeRole {
2001    /// Primary checkout for the Git common directory.
2002    Primary,
2003    /// Linked checkout with reciprocal registration evidence.
2004    Linked,
2005}
2006
2007impl From<GitWorktreeRole> for McpGitWorktreeRole {
2008    fn from(value: GitWorktreeRole) -> Self {
2009        match value {
2010            GitWorktreeRole::Primary => Self::Primary,
2011            GitWorktreeRole::Linked => Self::Linked,
2012        }
2013    }
2014}
2015
2016/// Git state exposed by structural worktree inventory.
2017#[derive(Clone, Copy, Debug, Serialize)]
2018#[serde(rename_all = "snake_case")]
2019enum McpGitWorktreeState {
2020    /// Reciprocal evidence resolves an exact source root.
2021    Active,
2022    /// Git retains registration evidence but the checkout is absent.
2023    Missing,
2024    /// Structural evidence is malformed or unsafe.
2025    Invalid,
2026}
2027
2028/// `ProjectAtlas` registration relationship for one structural row.
2029#[derive(Clone, Copy, Debug, Serialize)]
2030#[serde(rename_all = "snake_case")]
2031enum McpWorktreeRegistrationState {
2032    /// Reserved selected control checkout.
2033    Control,
2034    /// Active short alias is stored in the control atlas.
2035    Registered,
2036    /// Git knows the worktree but `ProjectAtlas` does not.
2037    Unregistered,
2038}
2039
2040/// Lossless UTF-8 availability for native paths at the display boundary.
2041#[derive(Clone, Copy, Debug, Serialize)]
2042#[serde(rename_all = "snake_case")]
2043enum McpWorktreePathDisplayState {
2044    /// The native path can be rendered as UTF-8 without changing bytes.
2045    Available,
2046    /// The native path remains routable by identity but cannot be rendered as UTF-8.
2047    Unavailable,
2048}
2049
2050/// Exact local atlas availability without modifying its database.
2051#[derive(Clone, Copy, Debug, Serialize)]
2052#[serde(rename_all = "snake_case")]
2053enum McpWorktreeAtlasState {
2054    /// A compatible atlas is bound to the exact current root.
2055    Initialized,
2056    /// No target-local database exists.
2057    Missing,
2058    /// A database exists but cannot be admitted for this exact root.
2059    Invalid,
2060    /// Git does not expose an active source root.
2061    Unavailable,
2062}
2063
2064/// Relationship between independent local and durable control telemetry.
2065#[derive(Clone, Copy, Debug, Serialize)]
2066#[serde(rename_all = "snake_case")]
2067enum McpWorktreeTelemetryState {
2068    /// Native telemetry belongs to the selected control atlas.
2069    Control,
2070    /// The latest readable local snapshot has been accepted.
2071    Current,
2072    /// A newer readable local snapshot awaits synchronization.
2073    Pending,
2074    /// No local atlas exists yet.
2075    MissingAtlas,
2076    /// No `ProjectAtlas` registration owns this structural row.
2077    Unregistered,
2078    /// Structural or atlas state prevents a trustworthy comparison.
2079    Unavailable,
2080}
2081
2082/// ProjectAtlas-only registry operation.
2083#[derive(Clone, Copy, Debug, Serialize)]
2084#[serde(rename_all = "snake_case")]
2085enum McpWorktreeMutationOperation {
2086    /// Add or reactivate one alias.
2087    Add,
2088    /// Retire one active alias after final synchronization.
2089    Remove,
2090}
2091
2092/// Result state for one worktree registry operation.
2093#[derive(Clone, Copy, Debug, Serialize)]
2094#[serde(rename_all = "snake_case")]
2095enum McpWorktreeMutationStatus {
2096    /// One active registration was committed.
2097    Registered,
2098    /// One active registration was retired.
2099    Retired,
2100    /// No active structural candidate matched.
2101    NotFound,
2102    /// More than one active structural candidate matched.
2103    Ambiguous,
2104}
2105
2106/// Read-only local atlas facts used by list/add/remove orchestration.
2107struct LocalWorktreeAtlas {
2108    /// Exact atlas identity.
2109    project_instance_id: ProjectInstanceId,
2110    /// Bounded local aggregate snapshot.
2111    snapshot: WorktreeUsageSnapshot,
2112}
2113
2114/// Role-typed selected root for MCP path routing.
2115#[derive(Debug, Clone)]
2116struct McpSelectedRoot(PathBuf);
2117
2118impl McpSelectedRoot {
2119    /// Build the selected root wrapper from active project state.
2120    fn from_state(state: &McpProjectState) -> Self {
2121        Self(state.root.clone())
2122    }
2123
2124    /// Convert an absolute path inside this root into a repository key.
2125    fn repo_key_for(&self, path: &McpAbsolutePath) -> Result<Option<McpRepoKey>, CliError> {
2126        if !path.as_path().starts_with(&self.0) {
2127            return Ok(None);
2128        }
2129        normalize_repo_path(&self.0, path.as_path())
2130            .map(McpRepoKey)
2131            .map(Some)
2132            .map_err(ProjectAtlasMcpServer::selected_project_path_error)
2133    }
2134}
2135
2136/// Role-typed indexed project root discovered by nearest-project routing.
2137#[derive(Debug, Clone)]
2138struct McpIndexedRoot {
2139    /// Canonical indexed project root.
2140    root: PathBuf,
2141    /// Durable index path that records the same root.
2142    db_path: PathBuf,
2143}
2144
2145/// Canonical absolute path supplied to an MCP path-bearing tool.
2146#[derive(Debug, Clone)]
2147struct McpAbsolutePath(PathBuf);
2148
2149impl McpAbsolutePath {
2150    /// Canonicalize an absolute path through its nearest existing ancestor.
2151    ///
2152    /// File selectors may refer to an indexed path that was deleted offline.
2153    /// Canonicalizing the existing ancestor preserves symlink/root-escape
2154    /// checks without requiring the selected leaf to still exist.
2155    fn canonicalize(path: &Path) -> Result<Self, CliError> {
2156        let mut existing = path;
2157        let mut missing_suffix = Vec::new();
2158        while !existing.exists() {
2159            let file_name = existing
2160                .file_name()
2161                .ok_or_else(|| missing_ancestor_error(path))?;
2162            missing_suffix.push(PathBuf::from(file_name));
2163            existing = existing
2164                .parent()
2165                .ok_or_else(|| missing_ancestor_error(path))?;
2166        }
2167        let mut canonical = canonical_project_root(existing)?;
2168        for component in missing_suffix.into_iter().rev() {
2169            canonical.push(component);
2170        }
2171        Ok(Self(canonical))
2172    }
2173
2174    /// Borrow the canonical path.
2175    fn as_path(&self) -> &Path {
2176        &self.0
2177    }
2178
2179    /// Return the directory where nearest-indexed-root discovery should begin.
2180    fn nearest_search_start(&self) -> &Path {
2181        if self.0.is_dir() {
2182            &self.0
2183        } else {
2184            self.0.parent().unwrap_or(self.as_path())
2185        }
2186    }
2187}
2188
2189/// Build the adapter error for an absolute selector without an inspectable ancestor.
2190fn missing_ancestor_error(path: &Path) -> CliError {
2191    CliError::InvalidInput(format!(
2192        "absolute path '{}' has no existing ancestor",
2193        path.display()
2194    ))
2195}
2196
2197/// Repository-relative path key derived from a typed root/path conversion.
2198#[derive(Debug, Clone)]
2199struct McpRepoKey(String);
2200
2201impl McpRepoKey {
2202    /// Consume the wrapper into the repository key string.
2203    fn into_string(self) -> String {
2204        self.0
2205    }
2206}
2207
2208/// MCP path resolution result with selected-project audit state.
2209#[derive(Debug, Clone)]
2210struct McpResolvedRepoPath {
2211    /// Project state selected for the request.
2212    state: McpProjectState,
2213    /// Repository-relative path key inside the selected project.
2214    key: String,
2215    /// Whether nearest-project routing changed the selected root/DB.
2216    routed_project: bool,
2217}
2218
2219/// Agent-facing error payload for failed MCP calls.
2220#[derive(Debug, Serialize)]
2221struct McpErrorResponse {
2222    /// Structured MCP error details.
2223    error: McpErrorPayload,
2224}
2225
2226/// Stable serialized schema for MCP error details.
2227#[derive(Debug, Serialize)]
2228struct McpErrorPayload {
2229    /// Stable machine-readable error kind.
2230    kind: AgentErrorKind,
2231    /// Human-readable error and recovery guidance.
2232    message: String,
2233    /// Bounded local-source mismatch details when refresh is required.
2234    #[serde(skip_serializing_if = "Option::is_none")]
2235    refresh_required: Option<IndexRefreshRequired>,
2236    /// Exact selected-root initialization handoff.
2237    #[serde(skip_serializing_if = "Option::is_none")]
2238    init_required: Option<IndexInitRequired>,
2239    /// Bare/common Git root selection diagnostic.
2240    #[serde(skip_serializing_if = "Option::is_none")]
2241    worktree_required: Option<ProjectWorktreeRequired>,
2242    /// Bounded source/policy diagnostic when verification cannot complete.
2243    #[serde(skip_serializing_if = "Option::is_none")]
2244    verification_incomplete: Option<IndexVerificationIncomplete>,
2245    /// Project/index identity mismatch details.
2246    #[serde(skip_serializing_if = "Option::is_none")]
2247    project_mismatch: Option<IndexProjectMismatch>,
2248    /// Content-free database placement details for a rejected `SQLite` profile.
2249    #[serde(skip_serializing_if = "Option::is_none")]
2250    database_filesystem: Option<DatabaseFilesystemErrorPayload>,
2251    /// Content-free incompatible schema details.
2252    #[serde(skip_serializing_if = "Option::is_none")]
2253    schema_version_mismatch: Option<SchemaVersionMismatchPayload>,
2254    /// Content-free supported migration handoff.
2255    #[serde(skip_serializing_if = "Option::is_none")]
2256    schema_migration_required: Option<SchemaMigrationRequiredPayload>,
2257    /// Optional retrieval capability state and recovery guidance.
2258    #[serde(skip_serializing_if = "Option::is_none")]
2259    search_capability: Option<crate::SearchCapabilityErrorPayload>,
2260    /// Reusable recovery call when refresh is required.
2261    #[serde(skip_serializing_if = "Option::is_none")]
2262    next: Option<McpNextCall>,
2263}
2264
2265/// Direct MCP recovery selector.
2266#[derive(Debug, Serialize)]
2267struct McpNextCall {
2268    /// Existing MCP tool that safely recovers the selected project.
2269    tool: &'static str,
2270    /// Canonical project root for legacy per-call isolation.
2271    #[serde(skip_serializing_if = "Option::is_none")]
2272    project_path: Option<String>,
2273    /// Registered alias preserved by worktree-routed recovery.
2274    #[serde(skip_serializing_if = "Option::is_none")]
2275    worktree: Option<String>,
2276}
2277
2278/// Agent-facing payload for the selected MCP project.
2279#[derive(Debug, Serialize)]
2280struct McpProjectStateResponse {
2281    /// Selected project details.
2282    project: McpProjectStatePayload,
2283}
2284
2285/// Stable serialized schema for the selected MCP project.
2286#[derive(Debug, Serialize)]
2287struct McpProjectStatePayload {
2288    /// Registered alias used to capture this project state.
2289    #[serde(skip_serializing_if = "Option::is_none")]
2290    worktree: Option<String>,
2291    /// Durable control-catalog registration identity.
2292    #[serde(skip_serializing_if = "Option::is_none")]
2293    registration_id: Option<i64>,
2294    /// Canonical repository root.
2295    root: Option<String>,
2296    /// Selected durable `SQLite` index path.
2297    db: Option<String>,
2298    /// Selected configuration path when present.
2299    config: Option<String>,
2300    /// Active selection status.
2301    status: McpProjectStatus,
2302}
2303
2304/// Status values for MCP project selection responses.
2305#[derive(Debug, Serialize)]
2306#[serde(rename_all = "snake_case")]
2307enum McpProjectStatus {
2308    /// The selected project is active for defaulted MCP calls.
2309    Active,
2310}
2311
2312/// Agent-facing payload for an MCP purpose update.
2313#[derive(Debug, Serialize)]
2314struct McpPurposeSetResponse {
2315    /// Purpose update result details.
2316    purpose_set: McpPurposeSetPayload,
2317}
2318
2319/// Stable serialized schema for an MCP purpose update.
2320#[derive(Debug, Serialize)]
2321struct McpPurposeSetPayload {
2322    /// Indexed repository-relative path whose purpose was updated.
2323    path: String,
2324    /// Registry-owned content role when the selected path is a file.
2325    #[serde(skip_serializing_if = "Option::is_none")]
2326    classification: Option<ContentClassification>,
2327    /// Durable purpose status after the update.
2328    status: PurposeStatus,
2329    /// Source of the durable purpose after the update.
2330    source: PurposeSource,
2331    /// Whether the purpose has been agent-reviewed.
2332    agent_reviewed: bool,
2333}
2334
2335/// MCP-session capability/settings payload.
2336#[derive(Debug, Serialize)]
2337struct McpSessionCapabilities {
2338    /// Runtime identity and compiled tool surface.
2339    runtime: RuntimeInfoReport,
2340    /// Selected project identity and index status.
2341    selected_project: McpSelectedProjectCapability,
2342    /// Route-affecting startup policy.
2343    startup_policy: McpStartupPolicy,
2344    /// Absolute-path routing scope.
2345    path_scope: McpPathScope,
2346    /// Scan behavior visible to harnesses.
2347    scan_policy: McpScanPolicy,
2348    /// Closed classified-content navigation surface.
2349    classified_navigation: SettingsClassifiedNavigationReport,
2350    /// Token telemetry write mode for this process.
2351    telemetry: McpTelemetryPolicy,
2352    /// Privacy guarantees for this payload.
2353    privacy: McpPrivacyPolicy,
2354}
2355
2356/// Selected project identity inside capability/settings payloads.
2357#[derive(Debug, Serialize)]
2358struct McpSelectedProjectCapability {
2359    /// Registered alias used to capture this project state.
2360    #[serde(skip_serializing_if = "Option::is_none")]
2361    worktree: Option<String>,
2362    /// Durable control-catalog registration identity.
2363    #[serde(skip_serializing_if = "Option::is_none")]
2364    registration_id: Option<i64>,
2365    /// Canonical repository root.
2366    root: Option<String>,
2367    /// Selected durable `SQLite` index path.
2368    db: Option<String>,
2369    /// Selected configuration path when present.
2370    config: Option<String>,
2371    /// Whether the selected durable index exists.
2372    index_status: McpIndexStatus,
2373}
2374
2375/// Startup policy fields for MCP sessions.
2376#[derive(Debug, Serialize)]
2377struct McpStartupPolicy {
2378    /// Whether nearest indexed project routing is enabled by default.
2379    nearest_project: McpPolicyState,
2380}
2381
2382/// Scan policy fields relevant before source reads.
2383#[derive(Debug, Serialize)]
2384struct McpScanPolicy {
2385    /// Settings calls and session briefs never scan implicitly.
2386    implicit_scan: McpPolicyState,
2387    /// Maximum `UTF-8` file size persisted into `SQLite` text search.
2388    text_index_max_bytes: u64,
2389}
2390
2391/// Telemetry write policy for this MCP process.
2392#[derive(Debug, Serialize)]
2393struct McpTelemetryPolicy {
2394    /// Whether token telemetry writes are enabled.
2395    mode: McpPolicyState,
2396}
2397
2398/// Privacy contract for capability/settings payloads.
2399#[derive(Debug, Serialize)]
2400struct McpPrivacyPolicy {
2401    /// No arbitrary process environment dump is included.
2402    environment_dump: bool,
2403    /// No secret or token values are included.
2404    secret_values: bool,
2405    /// Host paths are limited to `ProjectAtlas` runtime/root/DB/config paths.
2406    projectatlas_paths_only: bool,
2407}
2408
2409/// Two-state policy enum serialized for MCP contracts.
2410#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
2411#[serde(rename_all = "snake_case")]
2412enum McpPolicyState {
2413    /// Policy is enabled.
2414    Enabled,
2415    /// Policy is disabled.
2416    Disabled,
2417}
2418
2419/// Selected index availability.
2420#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
2421#[serde(rename_all = "snake_case")]
2422enum McpIndexStatus {
2423    /// The selected index file exists.
2424    Available,
2425    /// The selected index file is missing.
2426    Missing,
2427}
2428
2429/// Absolute path routing scope.
2430#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
2431#[serde(rename_all = "snake_case")]
2432enum McpPathScope {
2433    /// Calls stay within the selected project.
2434    SelectedProject,
2435    /// Absolute paths may route to the nearest indexed project.
2436    NearestIndexedProject,
2437}
2438
2439/// Explicit compact file-summary payload with actionable facts and redundant state removed.
2440#[derive(Debug, Serialize)]
2441struct McpFileSummaryPayload<'a> {
2442    /// Explicit compact file intelligence.
2443    file_summary: McpFileSummary<'a>,
2444}
2445
2446/// Compact projection used when an agent follows a default startup recommendation.
2447#[derive(Debug, Serialize)]
2448struct McpFileSummary<'a> {
2449    /// Repository-relative file path.
2450    file_path: &'a str,
2451    /// Persisted content role for the selected file.
2452    classification: projectatlas_core::language::ContentClassification,
2453    /// Detected language or file family.
2454    language: &'a str,
2455    /// Source line count.
2456    line_count: usize,
2457    /// Non-default source state when live source was unavailable.
2458    #[serde(skip_serializing_if = "Option::is_none")]
2459    source_status: Option<&'a str>,
2460    /// Source read diagnostic when one exists.
2461    #[serde(skip_serializing_if = "Option::is_none")]
2462    source_error: Option<&'a str>,
2463    /// Parser family that produced the summary.
2464    parser_kind: &'a str,
2465    /// Summary quality state agents must inspect before trusting generated prose.
2466    summary_status: &'a str,
2467    /// Durable file responsibility when present.
2468    #[serde(skip_serializing_if = "Option::is_none")]
2469    file_purpose: Option<&'a str>,
2470    /// Purpose status for suggestions or other unreviewed rows.
2471    #[serde(skip_serializing_if = "Option::is_none")]
2472    file_purpose_status: Option<&'a str>,
2473    /// Purpose source for suggestions or other unreviewed rows.
2474    #[serde(skip_serializing_if = "Option::is_none")]
2475    file_purpose_source: Option<&'a str>,
2476    /// Whether an agent approved the retained responsibility.
2477    #[serde(skip_serializing_if = "is_false")]
2478    file_purpose_agent_reviewed: bool,
2479    /// Current deterministic content summary.
2480    content_summary: &'a str,
2481    /// Package or module name when present.
2482    #[serde(skip_serializing_if = "Option::is_none")]
2483    package: Option<&'a str>,
2484    /// File documentation when present.
2485    #[serde(skip_serializing_if = "Option::is_none")]
2486    docstring: Option<&'a str>,
2487    /// Whether the default bounded repeated sections omitted rows.
2488    #[serde(skip_serializing_if = "is_false")]
2489    truncated: bool,
2490    /// Indexed functions when present.
2491    #[serde(skip_serializing_if = "Option::is_none")]
2492    functions: Option<Vec<McpFileSymbolSummary<'a>>>,
2493    /// Indexed methods when present.
2494    #[serde(skip_serializing_if = "Option::is_none")]
2495    methods: Option<Vec<McpFileSymbolSummary<'a>>>,
2496    /// Indexed classes when present.
2497    #[serde(skip_serializing_if = "Option::is_none")]
2498    classes: Option<Vec<McpFileSymbolSummary<'a>>>,
2499    /// Indexed type declarations when present.
2500    #[serde(skip_serializing_if = "Option::is_none")]
2501    types: Option<Vec<McpFileSymbolSummary<'a>>>,
2502    /// Imports when present.
2503    #[serde(skip_serializing_if = "Option::is_none")]
2504    imports: Option<&'a [String]>,
2505    /// Manifest dependencies when present.
2506    #[serde(skip_serializing_if = "Option::is_none")]
2507    dependencies: Option<&'a [String]>,
2508    /// Exported declarations when present.
2509    #[serde(skip_serializing_if = "Option::is_none")]
2510    exports: Option<&'a [String]>,
2511    /// Call rows when present.
2512    #[serde(skip_serializing_if = "Option::is_none")]
2513    calls: Option<&'a [FileCallSummary]>,
2514    /// Coverage details only when they require agent attention.
2515    #[serde(skip_serializing_if = "Option::is_none")]
2516    coverage: Option<McpCompactCoverageDigest<'a>>,
2517}
2518
2519/// Compact symbol row that omits empty legacy fields.
2520#[derive(Debug, Serialize)]
2521struct McpFileSymbolSummary<'a> {
2522    /// Symbol name.
2523    name: &'a str,
2524    /// Symbol kind.
2525    kind: &'a str,
2526    /// One-based start line.
2527    line: usize,
2528    /// One-based end line.
2529    end_line: usize,
2530    /// Declaration signature.
2531    signature: &'a str,
2532    /// Whether the declaration is externally visible.
2533    exported: bool,
2534    /// Extracted documentation when present.
2535    #[serde(skip_serializing_if = "Option::is_none")]
2536    documentation: Option<&'a str>,
2537    /// Parent declaration when present.
2538    #[serde(skip_serializing_if = "Option::is_none")]
2539    parent: Option<&'a str>,
2540    /// Indexed callers when present.
2541    #[serde(skip_serializing_if = "Option::is_none")]
2542    called_by: Option<&'a [String]>,
2543}
2544
2545impl<'a> From<&'a FileSymbolSummary> for McpFileSymbolSummary<'a> {
2546    fn from(symbol: &'a FileSymbolSummary) -> Self {
2547        Self {
2548            name: &symbol.name,
2549            kind: &symbol.kind,
2550            line: symbol.line,
2551            end_line: symbol.end_line,
2552            signature: &symbol.signature,
2553            exported: symbol.exported,
2554            documentation: nonempty_str(&symbol.documentation),
2555            parent: nonempty_str(&symbol.parent),
2556            called_by: nonempty_slice(&symbol.called_by),
2557        }
2558    }
2559}
2560
2561/// Project nonempty symbol rows into their compact MCP representation.
2562fn compact_file_symbols(symbols: &[FileSymbolSummary]) -> Option<Vec<McpFileSymbolSummary<'_>>> {
2563    (!symbols.is_empty()).then(|| symbols.iter().map(McpFileSymbolSummary::from).collect())
2564}
2565
2566impl<'a> From<&'a FileSummaryReport> for McpFileSummary<'a> {
2567    fn from(report: &'a FileSummaryReport) -> Self {
2568        let reviewed_purpose = report.file_purpose_agent_reviewed;
2569        let coverage_requires_attention = !report.coverage.available
2570            || report.coverage.trust != CoverageTrustState::Trusted
2571            || report.coverage.omitted > 0
2572            || report.coverage.truncated;
2573        Self {
2574            file_path: &report.file_path,
2575            classification: report.classification,
2576            language: &report.language,
2577            line_count: report.line_count,
2578            source_status: (report.source_status != MCP_FILE_SOURCE_STATUS_LIVE)
2579                .then_some(report.source_status.as_str()),
2580            source_error: nonempty_str(&report.source_error),
2581            parser_kind: &report.parser_kind,
2582            summary_status: &report.summary_status,
2583            file_purpose: nonempty_str(&report.file_purpose),
2584            file_purpose_status: (!reviewed_purpose).then_some(report.file_purpose_status.as_str()),
2585            file_purpose_source: (!reviewed_purpose).then_some(report.file_purpose_source.as_str()),
2586            file_purpose_agent_reviewed: reviewed_purpose,
2587            content_summary: &report.content_summary,
2588            package: nonempty_str(&report.package),
2589            docstring: nonempty_str(&report.docstring),
2590            truncated: report.truncated,
2591            functions: compact_file_symbols(&report.functions),
2592            methods: compact_file_symbols(&report.methods),
2593            classes: compact_file_symbols(&report.classes),
2594            types: compact_file_symbols(&report.types),
2595            imports: nonempty_slice(&report.imports),
2596            dependencies: nonempty_slice(&report.dependencies),
2597            exports: nonempty_slice(&report.exports),
2598            calls: nonempty_slice(&report.calls),
2599            coverage: coverage_requires_attention
2600                .then(|| McpCompactCoverageDigest::from(&report.coverage)),
2601        }
2602    }
2603}
2604
2605/// Adapter-local sparse coverage counts for the explicit compact summary.
2606#[derive(Debug, Serialize)]
2607struct McpCompactCoverageStateCounts {
2608    /// Complete coverage rows.
2609    #[serde(skip_serializing_if = "is_zero_u32")]
2610    complete: u32,
2611    /// Complete extraction scopes containing no supported candidates.
2612    #[serde(skip_serializing_if = "is_zero_u32")]
2613    no_candidates: u32,
2614    /// Partial coverage rows.
2615    #[serde(skip_serializing_if = "is_zero_u32")]
2616    partial: u32,
2617    /// Failed coverage rows.
2618    #[serde(skip_serializing_if = "is_zero_u32")]
2619    failed: u32,
2620    /// Intentionally ignored coverage rows.
2621    #[serde(skip_serializing_if = "is_zero_u32")]
2622    ignored: u32,
2623    /// Oversized coverage rows.
2624    #[serde(skip_serializing_if = "is_zero_u32")]
2625    oversized: u32,
2626    /// Quarantined coverage rows.
2627    #[serde(skip_serializing_if = "is_zero_u32")]
2628    quarantined: u32,
2629    /// Stale coverage rows.
2630    #[serde(skip_serializing_if = "is_zero_u32")]
2631    stale: u32,
2632}
2633
2634/// Adapter-local compact coverage projection that leaves shared serialization unchanged.
2635#[derive(Debug, Serialize)]
2636struct McpCompactCoverageDigest<'a> {
2637    /// False when no current coverage rows exist.
2638    #[serde(skip_serializing_if = "is_true")]
2639    available: bool,
2640    /// Active generation shared by retained rows.
2641    active_generation: &'a IndexGeneration,
2642    /// Source parser pass recorded for the file.
2643    #[serde(skip_serializing_if = "Option::is_none")]
2644    parser: Option<&'a ParserKind>,
2645    /// Fact provider pass recorded for the file.
2646    #[serde(skip_serializing_if = "Option::is_none")]
2647    provider: Option<&'a ParserKind>,
2648    /// Sparse per-state counts.
2649    states: McpCompactCoverageStateCounts,
2650    /// Total items declared by retained rows.
2651    total: u64,
2652    /// Covered items declared by retained rows.
2653    covered: u64,
2654    /// Omitted or untrusted items declared by retained rows.
2655    #[serde(skip_serializing_if = "is_zero_u64")]
2656    omitted: u64,
2657    /// Number of retained relation-family rows.
2658    #[serde(skip_serializing_if = "is_zero_u32")]
2659    relation_rows: u32,
2660    /// Whether digest bounds omitted additional selected-file rows.
2661    #[serde(skip_serializing_if = "is_false")]
2662    truncated: bool,
2663    /// Conservative trust state across retained rows.
2664    trust: &'a CoverageTrustState,
2665    /// Existing opt-in health call for deeper coverage discovery.
2666    next_call: &'a NavigationNextCall,
2667}
2668
2669impl<'a> From<&'a CoverageDigest> for McpCompactCoverageDigest<'a> {
2670    fn from(coverage: &'a CoverageDigest) -> Self {
2671        Self {
2672            available: coverage.available,
2673            active_generation: &coverage.active_generation,
2674            parser: coverage.parser.as_ref(),
2675            provider: coverage.provider.as_ref(),
2676            states: McpCompactCoverageStateCounts {
2677                complete: coverage.states.complete,
2678                no_candidates: coverage.states.no_candidates,
2679                partial: coverage.states.partial,
2680                failed: coverage.states.failed,
2681                ignored: coverage.states.ignored,
2682                oversized: coverage.states.oversized,
2683                quarantined: coverage.states.quarantined,
2684                stale: coverage.states.stale,
2685            },
2686            total: coverage.total,
2687            covered: coverage.covered,
2688            omitted: coverage.omitted,
2689            relation_rows: coverage.relation_rows,
2690            truncated: coverage.truncated,
2691            trust: &coverage.trust,
2692            next_call: &coverage.next_call,
2693        }
2694    }
2695}
2696
2697/// Adapter-local compact detailed-relation node without stable-key duplication.
2698#[derive(Debug, Serialize)]
2699struct McpCompactDetailedRelationNode<'a> {
2700    /// Exact selector accepted by later relation, summary, and slice calls.
2701    selector: &'a EntitySelector,
2702    /// Accepted, unavailable, or non-local purpose state.
2703    purpose: &'a RelationPurpose,
2704    /// Authoritative coverage rows for the selected local owner.
2705    #[serde(skip_serializing_if = "Option::is_none")]
2706    coverage: Option<&'a [CoverageRecord]>,
2707}
2708
2709impl<'a> From<&'a DetailedRelationNode> for McpCompactDetailedRelationNode<'a> {
2710    fn from(node: &'a DetailedRelationNode) -> Self {
2711        Self {
2712            selector: node.entity.selector(),
2713            purpose: &node.purpose,
2714            coverage: nonempty_slice(&node.coverage),
2715        }
2716    }
2717}
2718
2719/// Resolution facts retained by the compact relation projection.
2720#[derive(Debug, Serialize)]
2721#[serde(tag = "status", rename_all = "snake_case")]
2722enum McpCompactRelationResolution<'a> {
2723    /// Exactly one reusable local target was resolved.
2724    Resolved {
2725        /// Exact selector accepted by later calls.
2726        selector: &'a ReusableTargetSelector,
2727        /// Complete generation containing the target.
2728        generation: IndexGeneration,
2729    },
2730    /// More than one valid target remains.
2731    Ambiguous {
2732        /// Original normalized reference.
2733        reference: &'a GraphIdentityText,
2734        /// Number of retained candidates before limits.
2735        candidates: u32,
2736    },
2737    /// No supported static target was found.
2738    Unresolved {
2739        /// Original normalized reference.
2740        reference: &'a GraphIdentityText,
2741    },
2742    /// The target is intentionally outside the selected project.
2743    External {
2744        /// Typed external identity.
2745        external: &'a ExternalSelector,
2746        /// Complete generation containing the external record.
2747        generation: IndexGeneration,
2748    },
2749}
2750
2751impl<'a> From<&'a RelationResolution> for McpCompactRelationResolution<'a> {
2752    fn from(resolution: &'a RelationResolution) -> Self {
2753        match resolution {
2754            RelationResolution::Resolved {
2755                selector,
2756                generation,
2757                ..
2758            } => Self::Resolved {
2759                selector,
2760                generation: *generation,
2761            },
2762            RelationResolution::Ambiguous {
2763                reference,
2764                candidates,
2765            } => Self::Ambiguous {
2766                reference,
2767                candidates: candidates.get(),
2768            },
2769            RelationResolution::Unresolved { reference } => Self::Unresolved { reference },
2770            RelationResolution::External {
2771                external,
2772                generation,
2773                ..
2774            } => Self::External {
2775                external,
2776                generation: *generation,
2777            },
2778        }
2779    }
2780}
2781
2782/// Compact relation facts required for trust and direct navigation.
2783#[derive(Debug, Serialize)]
2784struct McpCompactLogicalRelation<'a> {
2785    /// Typed legacy or extended relation family.
2786    kind: GraphRelationKind,
2787    /// Resolution state and reusable target when local.
2788    resolution: McpCompactRelationResolution<'a>,
2789    /// Coarse trust class.
2790    confidence: ConfidenceClass,
2791    /// Producer completeness for this relation scope.
2792    completeness: Completeness,
2793    /// Complete generation containing the relation.
2794    generation: IndexGeneration,
2795}
2796
2797/// One compact detailed-relation row.
2798#[derive(Debug, Serialize)]
2799struct McpCompactDetailedRelationRow<'a> {
2800    /// One-based traversal depth.
2801    depth: u32,
2802    /// Direction relative to the selected frontier.
2803    direction: RelationDirection,
2804    /// Typed relation facts without stable-key duplication.
2805    relation: McpCompactLogicalRelation<'a>,
2806    /// Closed reason retained only for an unresolved canonical document relation.
2807    #[serde(skip_serializing_if = "Option::is_none")]
2808    document_unresolved_reason: Option<DocumentTargetUnresolvedReason>,
2809    /// Exact source selector, purpose, and coverage.
2810    source: McpCompactDetailedRelationNode<'a>,
2811    /// Retained local or external target when one exists.
2812    #[serde(skip_serializing_if = "Option::is_none")]
2813    target: Option<McpCompactDetailedRelationNode<'a>>,
2814    /// Purpose disposition when no target node exists.
2815    #[serde(skip_serializing_if = "Option::is_none")]
2816    target_purpose: Option<&'a RelationPurpose>,
2817    /// Exact selectors for a multi-hop path; direct source/target rows omit this duplicate.
2818    #[serde(skip_serializing_if = "Option::is_none")]
2819    path: Option<Vec<&'a EntitySelector>>,
2820    /// Exact supporting occurrences when requested.
2821    #[serde(skip_serializing_if = "Option::is_none")]
2822    occurrences: Option<Vec<McpCompactRelationOccurrence<'a>>>,
2823    /// Whether the per-relation occurrence ceiling omitted rows.
2824    #[serde(skip_serializing_if = "is_false")]
2825    occurrences_truncated: bool,
2826    /// Existing exact call that consumes the selected local endpoint.
2827    #[serde(skip_serializing_if = "Option::is_none")]
2828    next_call: Option<&'a RelationNextCall>,
2829}
2830
2831/// Compact source occurrence without another copy of the owning relation key.
2832#[derive(Debug, Serialize)]
2833struct McpCompactRelationOccurrence<'a> {
2834    /// Repository-local file containing the evidence.
2835    file: &'a RepositoryFilePath,
2836    /// Exact supporting source range.
2837    span: SourceSpan,
2838    /// Complete generation containing the occurrence.
2839    generation: IndexGeneration,
2840}
2841
2842impl<'a> From<&'a RelationOccurrence> for McpCompactRelationOccurrence<'a> {
2843    fn from(occurrence: &'a RelationOccurrence) -> Self {
2844        Self {
2845            file: occurrence.file(),
2846            span: occurrence.span(),
2847            generation: occurrence.generation(),
2848        }
2849    }
2850}
2851
2852impl<'a> From<&'a DetailedRelationRow> for McpCompactDetailedRelationRow<'a> {
2853    fn from(row: &'a DetailedRelationRow) -> Self {
2854        Self {
2855            depth: row.depth,
2856            direction: row.direction,
2857            relation: McpCompactLogicalRelation {
2858                kind: row.relation.kind(),
2859                resolution: McpCompactRelationResolution::from(row.relation.resolution()),
2860                confidence: row.relation.confidence(),
2861                completeness: row.relation.completeness(),
2862                generation: row.relation.generation(),
2863            },
2864            document_unresolved_reason: row.document_unresolved_reason,
2865            source: McpCompactDetailedRelationNode::from(&row.source),
2866            target: row
2867                .target
2868                .as_ref()
2869                .map(McpCompactDetailedRelationNode::from),
2870            target_purpose: row.target.is_none().then_some(&row.target_purpose),
2871            path: (row.path.len() > 2)
2872                .then(|| row.path.iter().map(|node| node.entity.selector()).collect()),
2873            occurrences: (!row.occurrences.is_empty()).then(|| {
2874                row.occurrences
2875                    .iter()
2876                    .map(McpCompactRelationOccurrence::from)
2877                    .collect()
2878            }),
2879            occurrences_truncated: row.occurrences_truncated,
2880            next_call: row.next_call.as_ref(),
2881        }
2882    }
2883}
2884
2885/// Opt-in compact projection of one detailed relation page.
2886#[derive(Debug, Serialize)]
2887struct McpCompactDetailedRelationReport<'a> {
2888    /// Exact selected anchor.
2889    anchor: McpCompactDetailedRelationNode<'a>,
2890    /// Complete graph generation captured by the page.
2891    generation: IndexGeneration,
2892    /// Accepted authored-purpose revision captured by the page.
2893    authored_purpose_revision: u64,
2894    /// Direction followed from the anchor.
2895    direction: RelationDirection,
2896    /// Number of retained relation steps.
2897    returned: u32,
2898    /// Number of cyclic or duplicate-node paths pruned.
2899    #[serde(skip_serializing_if = "is_zero_u64")]
2900    pruned_paths: u64,
2901    /// Whether a declared boundary stopped traversal.
2902    #[serde(skip_serializing_if = "is_false")]
2903    truncated: bool,
2904    /// Directly reusable continuation call with the exact original query and budget.
2905    #[serde(skip_serializing_if = "Option::is_none")]
2906    next_call: Option<McpCompactRelationContinuationCall<'a>>,
2907    /// Exact, lower-bound, or unknown cardinality.
2908    total: &'a RelationTotalState,
2909    /// Stable hard limits reached while constructing the response.
2910    #[serde(skip_serializing_if = "Option::is_none")]
2911    reached_limits: Option<&'a [GraphLimitKind]>,
2912    /// Aggregate page and retained-state work.
2913    work: &'a DetailedRelationWork,
2914    /// Ranked node-simple relation steps.
2915    rows: Vec<McpCompactDetailedRelationRow<'a>>,
2916}
2917
2918/// Existing MCP relation call that resumes one exact compact page.
2919#[derive(Debug, Serialize)]
2920struct McpCompactRelationContinuationCall<'a> {
2921    /// Existing MCP tool that owns relation continuation.
2922    tool: &'static str,
2923    /// Exact original request plus its generation-bound cursor.
2924    arguments: McpCompactRelationContinuationArguments<'a>,
2925}
2926
2927/// Exact result-defining arguments required to resume a detailed relation page.
2928#[derive(Debug, Serialize)]
2929struct McpCompactRelationContinuationArguments<'a> {
2930    /// Explicit project root when the original request supplied one.
2931    #[serde(skip_serializing_if = "Option::is_none")]
2932    project_path: Option<&'a str>,
2933    /// Registered primary worktree when the original request supplied one.
2934    #[serde(skip_serializing_if = "Option::is_none")]
2935    worktree: Option<&'a str>,
2936    /// Selected repository-relative anchor file.
2937    file: &'a str,
2938    /// Original nearest-project policy when explicitly supplied.
2939    #[serde(skip_serializing_if = "Option::is_none")]
2940    nearest_project: Option<bool>,
2941    /// Original ordered federated roots when supplied.
2942    #[serde(skip_serializing_if = "Option::is_none")]
2943    roots: Option<&'a [String]>,
2944    /// Original ordered registered worktree aliases when supplied.
2945    #[serde(skip_serializing_if = "Option::is_none")]
2946    worktrees: Option<&'a [String]>,
2947    /// Detailed relation view.
2948    view: &'static str,
2949    /// Preserve the compact response projection.
2950    compact: bool,
2951    /// Generation-, purpose-, query-, order-, and budget-bound cursor.
2952    cursor: &'a str,
2953    /// Exact symbol name when the anchor is a declaration.
2954    #[serde(skip_serializing_if = "Option::is_none")]
2955    symbol: Option<&'a str>,
2956    /// Exact nonempty symbol parent when supplied.
2957    #[serde(skip_serializing_if = "Option::is_none")]
2958    symbol_parent: Option<&'a str>,
2959    /// Exact nonempty symbol kind when supplied.
2960    #[serde(skip_serializing_if = "Option::is_none")]
2961    symbol_kind: Option<&'a str>,
2962    /// Exact nonempty symbol signature when supplied.
2963    #[serde(skip_serializing_if = "Option::is_none")]
2964    symbol_signature: Option<&'a str>,
2965    /// Original traversal direction when explicitly supplied.
2966    #[serde(skip_serializing_if = "Option::is_none")]
2967    direction: Option<&'a str>,
2968    /// Original relation filter when explicitly supplied.
2969    #[serde(skip_serializing_if = "Option::is_none")]
2970    relation: Option<&'a str>,
2971    /// Original confidence floor when explicitly supplied.
2972    #[serde(skip_serializing_if = "Option::is_none")]
2973    minimum_confidence: Option<&'a str>,
2974    /// Original resolution filter when explicitly supplied.
2975    #[serde(skip_serializing_if = "Option::is_none")]
2976    resolution: Option<&'a str>,
2977    /// Original traversal depth when explicitly supplied.
2978    #[serde(skip_serializing_if = "Option::is_none")]
2979    depth: Option<u32>,
2980    /// Original occurrence-retention choice when explicitly supplied.
2981    #[serde(skip_serializing_if = "Option::is_none")]
2982    include_occurrences: Option<bool>,
2983    /// Original returned-row limit when explicitly supplied.
2984    #[serde(skip_serializing_if = "Option::is_none")]
2985    limit: Option<usize>,
2986    /// Original per-relation occurrence limit when explicitly supplied.
2987    #[serde(skip_serializing_if = "Option::is_none")]
2988    occurrence_limit: Option<u32>,
2989    /// Original edge budget when explicitly supplied.
2990    #[serde(skip_serializing_if = "Option::is_none")]
2991    edge_limit: Option<u32>,
2992    /// Original node budget when explicitly supplied.
2993    #[serde(skip_serializing_if = "Option::is_none")]
2994    node_limit: Option<u32>,
2995    /// Original visited-node budget when explicitly supplied.
2996    #[serde(skip_serializing_if = "Option::is_none")]
2997    visited_limit: Option<u32>,
2998    /// Original aggregate occurrence budget when explicitly supplied.
2999    #[serde(skip_serializing_if = "Option::is_none")]
3000    occurrence_total_limit: Option<u32>,
3001    /// Original intermediate-byte budget when explicitly supplied.
3002    #[serde(skip_serializing_if = "Option::is_none")]
3003    intermediate_bytes: Option<u64>,
3004    /// Original service deadline when explicitly supplied.
3005    #[serde(skip_serializing_if = "Option::is_none")]
3006    deadline_ms: Option<u64>,
3007    /// Original rendered-output budget when explicitly supplied.
3008    #[serde(skip_serializing_if = "Option::is_none")]
3009    output_bytes: Option<u32>,
3010}
3011
3012impl<'a> McpCompactDetailedRelationReport<'a> {
3013    /// Project one detailed page and its directly reusable continuation.
3014    fn new(
3015        report: &'a DetailedRelationReport,
3016        file: &'a str,
3017        params: &'a AtlasSymbolRelationsParams,
3018    ) -> Self {
3019        Self {
3020            anchor: McpCompactDetailedRelationNode::from(&report.anchor),
3021            generation: report.generation,
3022            authored_purpose_revision: report.authored_purpose_revision,
3023            direction: report.direction,
3024            returned: report.returned,
3025            pruned_paths: report.pruned_paths,
3026            truncated: report.truncated,
3027            next_call: report.continuation.as_deref().map(|cursor| {
3028                McpCompactRelationContinuationCall {
3029                    tool: MCP_TOOL_ATLAS_SYMBOL_RELATIONS,
3030                    arguments: McpCompactRelationContinuationArguments {
3031                        project_path: params.project_path.as_deref(),
3032                        worktree: params.worktree.as_deref(),
3033                        file,
3034                        nearest_project: params.nearest_project,
3035                        roots: params.roots.as_deref(),
3036                        worktrees: params.worktrees.as_deref(),
3037                        view: MCP_SYMBOL_RELATION_VIEW_DETAILED,
3038                        compact: true,
3039                        cursor,
3040                        symbol: params.symbol.as_deref(),
3041                        symbol_parent: params.symbol_parent.as_deref().and_then(nonempty_str),
3042                        symbol_kind: params.symbol_kind.as_deref().and_then(nonempty_str),
3043                        symbol_signature: params.symbol_signature.as_deref().and_then(nonempty_str),
3044                        direction: params.direction.as_deref(),
3045                        relation: params.relation.as_deref(),
3046                        minimum_confidence: params.minimum_confidence.as_deref(),
3047                        resolution: params.resolution.as_deref(),
3048                        depth: params.depth,
3049                        include_occurrences: params.include_occurrences,
3050                        limit: params.limit,
3051                        occurrence_limit: params.occurrence_limit,
3052                        edge_limit: params.edge_limit,
3053                        node_limit: params.node_limit,
3054                        visited_limit: params.visited_limit,
3055                        occurrence_total_limit: params.occurrence_total_limit,
3056                        intermediate_bytes: params.intermediate_bytes,
3057                        deadline_ms: params.deadline_ms,
3058                        output_bytes: params.output_bytes,
3059                    },
3060                }
3061            }),
3062            total: &report.total,
3063            reached_limits: nonempty_slice(&report.reached_limits),
3064            work: &report.work,
3065            rows: report
3066                .rows
3067                .iter()
3068                .map(McpCompactDetailedRelationRow::from)
3069                .collect(),
3070        }
3071    }
3072}
3073
3074/// Compact federated wrapper that preserves cross-root evidence and work.
3075#[derive(Debug, Serialize)]
3076struct McpCompactFederatedDetailedRelationReport<'a> {
3077    /// Ordered validated participants.
3078    participants: &'a [FederatedParticipant],
3079    /// Worktree alias of the first-root result when aliases selected federation.
3080    #[serde(skip_serializing_if = "Option::is_none")]
3081    primary_worktree: Option<&'a str>,
3082    /// Compact first-root detailed relation page.
3083    primary: McpCompactDetailedRelationReport<'a>,
3084    /// Exact cross-root external rendezvous evidence.
3085    #[serde(skip_serializing_if = "Option::is_none")]
3086    rendezvous: Option<&'a [FederatedRendezvous]>,
3087    /// Whether primary or rendezvous work was truncated.
3088    #[serde(skip_serializing_if = "is_false")]
3089    truncated: bool,
3090    /// Stable aggregate limits reached by either stage.
3091    #[serde(skip_serializing_if = "Option::is_none")]
3092    reached_limits: Option<&'a [GraphLimitKind]>,
3093    /// Exact aggregate work.
3094    work: &'a FederatedRelationWork,
3095}
3096
3097impl<'a> McpCompactFederatedDetailedRelationReport<'a> {
3098    /// Project a federated page while preserving its exact continuation call.
3099    fn new(
3100        report: &'a FederatedDetailedRelationReport,
3101        file: &'a str,
3102        params: &'a AtlasSymbolRelationsParams,
3103    ) -> Self {
3104        Self {
3105            participants: &report.participants,
3106            primary_worktree: report.primary_worktree.as_deref(),
3107            primary: McpCompactDetailedRelationReport::new(&report.primary, file, params),
3108            rendezvous: nonempty_slice(&report.rendezvous),
3109            truncated: report.truncated,
3110            reached_limits: nonempty_slice(&report.reached_limits),
3111            work: &report.work,
3112        }
3113    }
3114}
3115
3116/// Borrow non-empty text into one optional compact field.
3117fn nonempty_str(value: &str) -> Option<&str> {
3118    (!value.is_empty()).then_some(value)
3119}
3120
3121/// Parse an optional adapter selection before any project or database work.
3122fn parse_content_selection(value: Option<&str>) -> Result<ContentSelection, CliError> {
3123    value
3124        .map(str::parse::<ContentSelection>)
3125        .transpose()
3126        .map(Option::unwrap_or_default)
3127        .map_err(|error| CliError::InvalidInput(error.to_string()))
3128}
3129
3130/// Borrow non-empty rows into one optional compact section.
3131fn nonempty_slice<T>(value: &[T]) -> Option<&[T]> {
3132    (!value.is_empty()).then_some(value)
3133}
3134
3135/// Return whether a compact unsigned count is zero.
3136#[allow(clippy::trivially_copy_pass_by_ref)]
3137const fn is_zero_u32(value: &u32) -> bool {
3138    *value == 0
3139}
3140
3141/// Return whether a compact unsigned total is zero.
3142#[allow(clippy::trivially_copy_pass_by_ref)]
3143const fn is_zero_u64(value: &u64) -> bool {
3144    *value == 0
3145}
3146
3147/// Agent startup brief payload.
3148#[derive(Debug, Serialize)]
3149struct McpSessionBrief {
3150    /// Selected project and index identity.
3151    project: McpSelectedProjectCapability,
3152    /// Route-affecting startup policy.
3153    policy: McpBriefPolicy,
3154    /// Overview counts when an index exists.
3155    overview: Option<Overview>,
3156    /// Indexed candidate folder rows.
3157    folders: Vec<McpBriefCandidate>,
3158    /// Indexed candidate file rows.
3159    files: Vec<McpBriefCandidate>,
3160    /// Bounded health blockers.
3161    blockers: McpBriefBlockers,
3162    /// Actionable bounded purpose-curator handoff for the selected project.
3163    purpose_handoff: Option<PurposeCuratorHandoff>,
3164    /// Recommended next calls.
3165    recommendations: Vec<McpBriefRecommendation>,
3166    /// Effective limits and truncation metadata.
3167    limits: McpBriefLimits,
3168}
3169
3170/// Additive compact projection of the compatibility-preserving startup brief.
3171#[derive(Debug, Serialize)]
3172struct McpCompactSessionBrief {
3173    /// Selected project identity needed for routing.
3174    project: McpCompactBriefProject,
3175    /// Non-default route-affecting startup policy.
3176    #[serde(skip_serializing_if = "Option::is_none")]
3177    policy: Option<McpBriefPolicy>,
3178    /// Compact overview counts when an index exists.
3179    #[serde(skip_serializing_if = "Option::is_none")]
3180    overview: Option<McpCompactBriefOverview>,
3181    /// Folder candidates only when no ready file candidate exists.
3182    #[serde(skip_serializing_if = "Vec::is_empty")]
3183    folders: Vec<McpCompactBriefCandidate>,
3184    /// Ready file candidates for the task.
3185    #[serde(skip_serializing_if = "Vec::is_empty")]
3186    files: Vec<McpCompactBriefCandidate>,
3187    /// Unsafe health blocker count when any exist.
3188    #[serde(skip_serializing_if = "Option::is_none")]
3189    blockers: Option<McpCompactBriefBlockers>,
3190    /// Exact host-owned purpose-curator follow-up when work is actionable.
3191    #[serde(skip_serializing_if = "Option::is_none")]
3192    purpose_handoff: Option<McpCompactBriefPurposeHandoff>,
3193    /// Recommended next calls.
3194    recommendations: Vec<McpCompactBriefRecommendation>,
3195    /// Non-default limits and truncation metadata.
3196    #[serde(skip_serializing_if = "Option::is_none")]
3197    limits: Option<McpCompactBriefLimits>,
3198}
3199
3200/// Compact selected-project state for the startup path.
3201#[derive(Debug, Serialize)]
3202struct McpCompactBriefProject {
3203    /// Registered alias used to capture this project state.
3204    #[serde(skip_serializing_if = "Option::is_none")]
3205    worktree: Option<String>,
3206    /// Durable control-catalog registration identity.
3207    #[serde(skip_serializing_if = "Option::is_none")]
3208    registration_id: Option<i64>,
3209    /// Canonical repository root.
3210    root: Option<String>,
3211    /// Whether the durable index is available.
3212    index_status: McpIndexStatus,
3213}
3214
3215/// Compact project counts for task startup.
3216#[derive(Debug, Serialize)]
3217struct McpCompactBriefOverview {
3218    /// Number of indexed files.
3219    files: usize,
3220    /// Number of indexed folders.
3221    folders: usize,
3222}
3223
3224/// Brief policy fields.
3225#[derive(Clone, Copy, Debug, Serialize)]
3226struct McpBriefPolicy {
3227    /// Whether nearest indexed project routing is enabled by default.
3228    nearest_project: McpPolicyState,
3229    /// Absolute-path routing scope.
3230    path_scope: McpPathScope,
3231}
3232
3233impl McpBriefPolicy {
3234    /// Return whether routing uses the ordinary selected-project policy.
3235    fn is_default(self) -> bool {
3236        self.nearest_project == McpPolicyState::Disabled
3237            && self.path_scope == McpPathScope::SelectedProject
3238    }
3239}
3240
3241/// Bounded ranked candidate row for startup briefs.
3242#[derive(Debug, Serialize)]
3243struct McpBriefCandidate {
3244    /// Repository-relative path.
3245    path: String,
3246    /// Indexed node kind.
3247    kind: String,
3248    /// Purpose lifecycle status.
3249    purpose_status: PurposeStatus,
3250    /// Purpose source.
3251    purpose_source: PurposeSource,
3252    /// Whether the purpose is current agent-approved authored responsibility state.
3253    purpose_agent_reviewed: bool,
3254    /// Purpose one-liner when present.
3255    purpose: Option<String>,
3256    /// Observed content summary when present.
3257    summary: Option<String>,
3258    /// Bounded ranking reasons.
3259    reasons: Vec<String>,
3260    /// Bounded compact ranking reason codes.
3261    reason_codes: Vec<RankedReasonCode>,
3262    /// Sparse stable-order connection counts.
3263    connection_counts: Vec<RankedConnectionCount>,
3264    /// Bounded high-value current connection sample.
3265    connections: Vec<RankedConnection>,
3266    /// Whether the bounded sample omitted any validated relation through family or global overflow.
3267    connections_truncated: bool,
3268    /// Existing navigation capability recommended after this row.
3269    next_call: NavigationNextCall,
3270}
3271
3272/// Bounded compact ranked candidate row for task startup.
3273#[derive(Debug, Serialize)]
3274struct McpCompactBriefCandidate {
3275    /// Repository-relative path.
3276    path: String,
3277    /// Purpose lifecycle status when it is not already agent-approved.
3278    #[serde(skip_serializing_if = "Option::is_none")]
3279    purpose_status: Option<PurposeStatus>,
3280    /// Purpose source when it is not already agent-approved.
3281    #[serde(skip_serializing_if = "Option::is_none")]
3282    purpose_source: Option<PurposeSource>,
3283    /// Whether the purpose is current agent-approved authored responsibility state.
3284    #[serde(skip_serializing_if = "is_false")]
3285    purpose_agent_reviewed: bool,
3286    /// Purpose one-liner when present.
3287    #[serde(skip_serializing_if = "Option::is_none")]
3288    purpose: Option<String>,
3289    /// One high-value current connection when available.
3290    #[serde(skip_serializing_if = "Vec::is_empty")]
3291    connections: Vec<RankedConnection>,
3292    /// Whether the bounded sample omitted any validated relation through family or global overflow.
3293    #[serde(skip_serializing_if = "is_false")]
3294    connections_truncated: bool,
3295    /// Existing navigation capability recommended after this row.
3296    next_call: NavigationNextCall,
3297}
3298
3299/// Compact host-owned purpose-curator handoff for startup briefs.
3300#[derive(Debug, Serialize)]
3301#[allow(clippy::struct_excessive_bools)]
3302struct McpCompactBriefPurposeHandoff {
3303    /// Whether this report is intended for an agent harness.
3304    #[serde(skip_serializing_if = "is_true")]
3305    agent_harness_expected: bool,
3306    /// Recommended host-relative reliable subagent tier selection.
3307    recommended_subagent_reasoning: &'static str,
3308    /// Single host-neutral selection instruction retained from the expanded handoff.
3309    instructions: Vec<String>,
3310    /// Whether the current main agent may process the same bounded batch.
3311    #[serde(skip_serializing_if = "is_true")]
3312    main_agent_fallback: bool,
3313    /// Explicitly records that `ProjectAtlas` did not spawn a host agent.
3314    #[serde(skip_serializing_if = "is_false")]
3315    server_started_curator: bool,
3316    /// Successful maintenance should not add ordinary conversation output.
3317    #[serde(skip_serializing_if = "is_true")]
3318    silent_on_success: bool,
3319    /// Whether the queue call has more rows after this bounded batch.
3320    #[serde(skip_serializing_if = "is_false")]
3321    truncated: bool,
3322    /// Exact existing MCP call that returns conditional-review tokens and bounded row context.
3323    next_call: McpCompactBriefRecommendation,
3324}
3325
3326/// Bounded health blocker section.
3327#[derive(Clone, Debug, Serialize)]
3328struct McpBriefBlockers {
3329    /// Findings after filters are applied.
3330    total: usize,
3331    /// Findings returned in this brief.
3332    returned: usize,
3333    /// Whether more blockers exist.
3334    truncated: bool,
3335    /// Blocker rows.
3336    items: Vec<McpBriefBlocker>,
3337}
3338
3339/// Compact blocker count; the recommendation carries the exact bounded health call.
3340#[derive(Debug, Serialize)]
3341struct McpCompactBriefBlockers {
3342    /// Findings after filters are applied.
3343    total: usize,
3344}
3345
3346/// Return whether a serialized optional fact is false and can be omitted.
3347#[allow(clippy::trivially_copy_pass_by_ref)]
3348const fn is_false(value: &bool) -> bool {
3349    !*value
3350}
3351
3352/// Return whether a serialized invariant is true and can be omitted.
3353#[allow(clippy::trivially_copy_pass_by_ref)]
3354const fn is_true(value: &bool) -> bool {
3355    *value
3356}
3357
3358/// One health blocker row for startup briefs.
3359#[derive(Clone, Debug, Serialize)]
3360struct McpBriefBlocker {
3361    /// Stable finding id.
3362    id: String,
3363    /// Finding severity.
3364    severity: Severity,
3365    /// Finding category.
3366    category: String,
3367    /// Primary path.
3368    path: String,
3369    /// Related path when applicable.
3370    related_path: Option<String>,
3371    /// Health message.
3372    message: String,
3373    /// Recommended agent action.
3374    recommendation: String,
3375}
3376
3377/// One typed startup recommendation.
3378#[derive(Debug, Serialize)]
3379struct McpBriefRecommendation {
3380    /// Stable recommendation kind.
3381    kind: McpBriefRecommendationKind,
3382    /// MCP tool name or filesystem/tool family.
3383    target: String,
3384    /// Concise machine-readable reason.
3385    reason: String,
3386    /// Suggested arguments for the target.
3387    arguments: serde_json::Value,
3388}
3389
3390/// One typed recommendation in the compact startup projection.
3391#[derive(Debug, Serialize)]
3392struct McpCompactBriefRecommendation {
3393    /// Stable recommendation kind.
3394    kind: McpBriefRecommendationKind,
3395    /// MCP tool name or filesystem/tool family.
3396    target: String,
3397    /// Concise machine-readable reason.
3398    reason: String,
3399    /// Suggested arguments for the target.
3400    arguments: serde_json::Value,
3401}
3402
3403/// Startup recommendation kinds.
3404#[derive(Clone, Copy, Debug, Serialize)]
3405#[serde(rename_all = "snake_case")]
3406enum McpBriefRecommendationKind {
3407    /// Initialize the selected project-local atlas.
3408    Init,
3409    /// Inspect the already-ranked file summary.
3410    Summary,
3411    /// Search the index when ranking has no directly navigable file.
3412    Search,
3413    /// Inspect detailed relations for the already-ranked file.
3414    Relations,
3415    /// Inspect structural health.
3416    Health,
3417    /// Load one bounded task-scoped purpose-curation queue.
3418    PurposeQueue,
3419    /// Read exact source or non-indexed files with normal filesystem tools.
3420    FilesystemTools,
3421}
3422
3423/// Effective startup brief row limits.
3424#[derive(Debug, Serialize)]
3425struct McpBriefLimits {
3426    /// Effective folder row limit.
3427    folder_limit: usize,
3428    /// Effective file row limit.
3429    file_limit: usize,
3430    /// Effective blocker row limit.
3431    blocker_limit: usize,
3432    /// Effective actionable purpose row limit.
3433    purpose_limit: usize,
3434    /// Whether folder candidates were truncated.
3435    folders_truncated: bool,
3436    /// Whether file candidates were truncated.
3437    files_truncated: bool,
3438    /// Whether more actionable low-scope purpose rows exist.
3439    purposes_truncated: bool,
3440}
3441
3442/// Non-default limits and truncation state in the compact startup projection.
3443#[derive(Debug, Serialize)]
3444struct McpCompactBriefLimits {
3445    /// Effective folder row limit.
3446    #[serde(skip_serializing_if = "is_compact_brief_default_limit")]
3447    folder_limit: usize,
3448    /// Effective file row limit.
3449    #[serde(skip_serializing_if = "is_compact_brief_default_limit")]
3450    file_limit: usize,
3451    /// Effective blocker row limit.
3452    #[serde(skip_serializing_if = "is_compact_brief_default_limit")]
3453    blocker_limit: usize,
3454    /// Effective actionable purpose row limit.
3455    #[serde(skip_serializing_if = "is_compact_brief_default_limit")]
3456    purpose_limit: usize,
3457    /// Whether folder candidates were omitted or truncated.
3458    #[serde(skip_serializing_if = "is_false")]
3459    folders_truncated: bool,
3460    /// Whether file candidates were truncated.
3461    #[serde(skip_serializing_if = "is_false")]
3462    files_truncated: bool,
3463    /// Whether more actionable low-scope purpose rows exist.
3464    #[serde(skip_serializing_if = "is_false")]
3465    purposes_truncated: bool,
3466}
3467
3468/// Return whether a startup row limit is the compact projection default.
3469#[allow(clippy::trivially_copy_pass_by_ref)]
3470const fn is_compact_brief_default_limit(value: &usize) -> bool {
3471    *value == COMPACT_SESSION_BRIEF_DEFAULT_LIMIT
3472}
3473
3474/// Task status lookup response.
3475#[derive(Debug, Serialize)]
3476struct McpTaskStatusResponse {
3477    /// Requested task id.
3478    task_id: String,
3479    /// Lookup outcome.
3480    lookup: McpTaskLookupStatus,
3481    /// Supported task states in this contract.
3482    states: Vec<McpTaskState>,
3483    /// Supported operation families in this contract.
3484    operations: Vec<McpTaskOperation>,
3485    /// Registry capacity.
3486    registry_capacity: usize,
3487    /// Task record when found.
3488    task: Option<McpTaskRecord>,
3489}
3490
3491/// Task lookup outcome.
3492#[derive(Debug, Eq, PartialEq, Serialize)]
3493#[serde(rename_all = "snake_case")]
3494enum McpTaskLookupStatus {
3495    /// The task was found.
3496    Found,
3497    /// The task id is unknown to this MCP session.
3498    NotFound,
3499}
3500
3501/// Task cancellation response.
3502#[derive(Debug, Serialize)]
3503struct McpTaskCancelResponse {
3504    /// Requested task id.
3505    task_id: String,
3506    /// Cancellation outcome.
3507    result: McpTaskCancelResult,
3508    /// Registry capacity.
3509    registry_capacity: usize,
3510    /// Task record when found.
3511    task: Option<McpTaskRecord>,
3512}
3513
3514/// Immediate response for one accepted background indexing task.
3515#[derive(Debug, Serialize)]
3516struct McpTaskStartResponse {
3517    /// Opaque session-local task id.
3518    task_id: String,
3519    /// Accepted operation family.
3520    operation: McpTaskOperation,
3521    /// Initial task state.
3522    state: McpTaskState,
3523    /// Existing MCP tool used to poll the task.
3524    status_tool: &'static str,
3525    /// Existing MCP tool used to request cancellation.
3526    cancel_tool: &'static str,
3527}
3528
3529/// Task cancellation outcome.
3530#[derive(Debug, Eq, PartialEq, Serialize)]
3531#[serde(rename_all = "snake_case")]
3532enum McpTaskCancelResult {
3533    /// Cooperative cancellation was delivered to active work.
3534    CancellationRequested,
3535    /// The task id is unknown to this MCP session.
3536    NotFound,
3537    /// The task was already finished.
3538    AlreadyFinished,
3539    /// The task exists but cannot currently be canceled.
3540    NotCancelable,
3541}
3542
3543/// Fixed worker partition shared by background indexing tasks in one MCP server.
3544#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3545struct McpBackgroundResourceEnvelope {
3546    /// Maximum concurrently admitted background tasks.
3547    task_limit: usize,
3548    /// Maximum scan and parser workers available to each admitted task.
3549    workers_per_task: usize,
3550    /// Maximum aggregate workers owned by all admitted tasks.
3551    total_worker_limit: usize,
3552}
3553
3554impl McpBackgroundResourceEnvelope {
3555    /// Derive one fixed partition from supported host availability and process policy.
3556    fn for_host() -> Self {
3557        let available_workers = thread::available_parallelism().map_or(1, usize::from);
3558        Self::from_available_workers(available_workers)
3559    }
3560
3561    /// Derive a deterministic envelope from an observed host worker count.
3562    fn from_available_workers(available_workers: usize) -> Self {
3563        let total_worker_limit = available_workers.clamp(1, INDEX_WORKER_SAFE_CEILING);
3564        let task_limit = total_worker_limit.min(MCP_BACKGROUND_TASK_SAFE_CEILING);
3565        let workers_per_task = (total_worker_limit / task_limit).max(1);
3566        Self {
3567            task_limit,
3568            workers_per_task,
3569            total_worker_limit,
3570        }
3571    }
3572}
3573
3574/// Native `ProjectAtlas` MCP server backed by the same services as the CLI.
3575#[derive(Debug, Clone)]
3576pub(crate) struct ProjectAtlasMcpServer {
3577    /// Immutable registry/control authority selected when the MCP process started.
3578    control_state: McpProjectState,
3579    /// Active project state for calls that omit `project_path`.
3580    project_state: Arc<RwLock<McpProjectState>>,
3581    /// Caller-visible compatibility label applied to this MCP process's events.
3582    session: String,
3583    /// Bounded telemetry lifecycle shared by every server clone and routed project.
3584    usage_runtime: Arc<Mutex<McpUsageRuntime>>,
3585    /// Whether absolute path arguments may select the nearest indexed project by default.
3586    allow_nearest_project: bool,
3587    /// Bounded MCP task-progress records for this server session.
3588    task_registry: Arc<RwLock<McpTaskRegistry>>,
3589    /// Fixed aggregate worker partition for background indexing tasks.
3590    background_resources: McpBackgroundResourceEnvelope,
3591    /// Monotonic session-local task identifier source.
3592    next_task_sequence: Arc<AtomicU64>,
3593    /// Bounded per-project source observers and verified read epochs.
3594    source_observations: Arc<SourceObservationRegistry>,
3595    /// Official RMCP tool router.
3596    tool_router: ToolRouter<Self>,
3597}
3598
3599/// Bridge one RMCP request cancellation token into synchronous index work.
3600struct McpRequestCancellationBridge {
3601    /// Signal used to stop the request-local cancellation monitor.
3602    stop: Arc<std::sync::atomic::AtomicBool>,
3603    /// Join handle for the bounded request-local monitor thread.
3604    monitor: Option<thread::JoinHandle<()>>,
3605    /// Direct request probe retained for synchronous cancellation fences.
3606    probe: Arc<dyn Fn() -> bool + Send + Sync>,
3607}
3608
3609impl McpRequestCancellationBridge {
3610    /// Start a request-local monitor from one owned RMCP context.
3611    fn start(
3612        context: &RequestContext<RoleServer>,
3613        control: &IndexWorkControl,
3614    ) -> Result<Self, CliError> {
3615        let token = context.ct.clone();
3616        Self::start_with_probe(move || token.is_cancelled(), control)
3617    }
3618
3619    /// Start a cancellation monitor from a deterministic probe for tests.
3620    fn start_with_probe<P>(probe: P, control: &IndexWorkControl) -> Result<Self, CliError>
3621    where
3622        P: Fn() -> bool + Send + Sync + 'static,
3623    {
3624        let probe: Arc<dyn Fn() -> bool + Send + Sync> = Arc::new(probe);
3625        if probe() {
3626            control.cancel();
3627        }
3628        let observed_control = control.clone();
3629        let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
3630        let monitor_stop = Arc::clone(&stop);
3631        let monitor_probe = Arc::clone(&probe);
3632        let monitor = thread::Builder::new()
3633            .name(MCP_CANCELLATION_MONITOR_THREAD_NAME.to_string())
3634            .spawn(move || {
3635                while !monitor_stop.load(Ordering::Acquire) {
3636                    if monitor_probe() {
3637                        observed_control.cancel();
3638                        break;
3639                    }
3640                    thread::sleep(std::time::Duration::from_millis(5));
3641                }
3642            })
3643            .map_err(|source| {
3644                let mut message = MCP_CANCELLATION_MONITOR_START_ERROR_PREFIX.to_string();
3645                message.push_str(&source.to_string());
3646                CliError::InvalidInput(message)
3647            })?;
3648        Ok(Self {
3649            stop,
3650            monitor: Some(monitor),
3651            probe,
3652        })
3653    }
3654
3655    /// Copy the request token into synchronous index-work cancellation.
3656    fn synchronize(&self, control: &IndexWorkControl) {
3657        if (self.probe)() {
3658            control.cancel();
3659        }
3660    }
3661}
3662
3663impl Drop for McpRequestCancellationBridge {
3664    fn drop(&mut self) {
3665        self.stop.store(true, Ordering::Release);
3666        if let Some(monitor) = self.monitor.take() {
3667            drop(monitor.join());
3668        }
3669    }
3670}
3671
3672impl ProjectAtlasMcpServer {
3673    /// Create a `ProjectAtlas` MCP server instance.
3674    pub(crate) fn new(
3675        db_path: PathBuf,
3676        config_path: Option<PathBuf>,
3677        session: String,
3678        allow_nearest_project: bool,
3679    ) -> Self {
3680        let startup_state = Self::startup_project_state(db_path, config_path);
3681        Self {
3682            control_state: startup_state.clone(),
3683            project_state: Arc::new(RwLock::new(startup_state)),
3684            session,
3685            usage_runtime: Arc::new(Mutex::new(McpUsageRuntime::default())),
3686            allow_nearest_project,
3687            task_registry: Arc::new(RwLock::new(McpTaskRegistry::new())),
3688            background_resources: McpBackgroundResourceEnvelope::for_host(),
3689            next_task_sequence: Arc::new(AtomicU64::new(1)),
3690            source_observations: Arc::new(SourceObservationRegistry::default()),
3691            tool_router: Self::tool_router(),
3692        }
3693    }
3694
3695    /// Open the durable index through one root-bound read snapshot.
3696    fn open_read_store(state: &McpProjectState) -> Result<AtlasStore, CliError> {
3697        if !state.db_path.exists() {
3698            return Err(Self::with_target_error_context(
3699                index_init_required(&state.root, &state.db_path),
3700                state,
3701            ));
3702        }
3703        let store = open_atlas_store_read_only_for_project(&state.db_path, &state.root)
3704            .map_err(|error| Self::with_target_error_context(error, state))?;
3705        Self::require_captured_worktree_identity(state.worktree.as_ref(), &store)
3706            .map_err(|error| Self::with_target_error_context(error, state))?;
3707        Ok(store)
3708    }
3709
3710    /// Require registered worktree aliases to be initialized outside `atlas_init`.
3711    fn require_initialized_worktree_target(state: &McpProjectState) -> Result<(), CliError> {
3712        if state.worktree.is_some() && !state.db_path.is_file() {
3713            return Err(Self::with_target_error_context(
3714                index_init_required(&state.root, &state.db_path),
3715                state,
3716            ));
3717        }
3718        Ok(())
3719    }
3720
3721    /// Preserve a captured worktree alias on typed init/refresh recovery state.
3722    fn with_target_error_context(mut error: CliError, state: &McpProjectState) -> CliError {
3723        let Some(selection) = state.worktree.as_ref() else {
3724            return error;
3725        };
3726        match &mut error {
3727            CliError::InitRequired(report) => report.worktree = Some(selection.alias.clone()),
3728            CliError::RefreshRequired(report) => report.worktree = Some(selection.alias.clone()),
3729            CliError::VerificationIncomplete(report) => {
3730                report.worktree = Some(selection.alias.clone());
3731            }
3732            CliError::ProjectMismatch(report) => {
3733                report.worktree = Some(selection.alias.clone());
3734            }
3735            _ => {}
3736        }
3737        error
3738    }
3739
3740    /// Run one normal MCP query inside the verified source-epoch boundary.
3741    #[cfg(test)]
3742    fn with_fresh_store<T, F>(
3743        &self,
3744        state: &McpProjectState,
3745        query: F,
3746    ) -> Result<VerifiedReadOutcome<T>, CliError>
3747    where
3748        F: FnMut(&AtlasStore, VerifiedReadStamp) -> Result<T, CliError>,
3749    {
3750        self.with_fresh_store_for_request(state, None, query)
3751    }
3752
3753    /// Run one verified query with optional RMCP request cancellation bridging.
3754    fn with_fresh_store_for_request<T, F>(
3755        &self,
3756        state: &McpProjectState,
3757        context: Option<RequestContext<RoleServer>>,
3758        mut query: F,
3759    ) -> Result<VerifiedReadOutcome<T>, CliError>
3760    where
3761        F: FnMut(&AtlasStore, VerifiedReadStamp) -> Result<T, CliError>,
3762    {
3763        self.with_fresh_store_controlled_for_request(state, context, |store, stamp, _control| {
3764            query(store, stamp)
3765        })
3766    }
3767
3768    /// Run one verified query that consumes the request cancellation boundary.
3769    fn with_fresh_store_controlled_for_request<T, F>(
3770        &self,
3771        state: &McpProjectState,
3772        context: Option<RequestContext<RoleServer>>,
3773        mut query: F,
3774    ) -> Result<VerifiedReadOutcome<T>, CliError>
3775    where
3776        F: FnMut(&AtlasStore, VerifiedReadStamp, &IndexWorkControl) -> Result<T, CliError>,
3777    {
3778        if !state.db_path.is_file() {
3779            return Err(Self::with_target_error_context(
3780                index_init_required(&state.root, &state.db_path),
3781                state,
3782            ));
3783        }
3784        let control =
3785            index_work_control(&SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, None, None));
3786        let bridge = context
3787            .map(|context| McpRequestCancellationBridge::start(&context, &control))
3788            .transpose()?;
3789        let result = self
3790            .source_observations
3791            .with_verified_read(
3792                &state.db_path,
3793                &state.root,
3794                state.config_path.as_deref(),
3795                &control,
3796                |store, stamp| {
3797                    Self::require_captured_worktree_identity(state.worktree.as_ref(), store)?;
3798                    query(store, stamp, &control)
3799                },
3800            )
3801            .map_err(|error| Self::with_target_error_context(error, state));
3802        if let Some(bridge) = bridge.as_ref() {
3803            bridge.synchronize(&control);
3804        }
3805        drop(bridge);
3806        result
3807    }
3808
3809    /// Revalidate one alias selection against the exact opened read snapshot.
3810    fn require_captured_worktree_identity(
3811        selection: Option<&McpWorktreeSelection>,
3812        store: &AtlasStore,
3813    ) -> Result<(), CliError> {
3814        let Some(expected) = selection.and_then(|selection| selection.project_instance_id) else {
3815            return Ok(());
3816        };
3817        if store.project_instance_id()? != Some(expected) {
3818            return Err(CliError::InvalidInput(
3819                MCP_ERROR_WORKTREE_IDENTITY_CONFLICT.to_string(),
3820            ));
3821        }
3822        Ok(())
3823    }
3824
3825    /// Revalidate the control catalog that owns one captured alias registration.
3826    fn require_captured_control_identity(
3827        selection: Option<&McpWorktreeSelection>,
3828        control: &AtlasStore,
3829    ) -> Result<(), CliError> {
3830        let Some(expected) = selection.and_then(|selection| selection.control_project_instance_id)
3831        else {
3832            return Ok(());
3833        };
3834        if control.project_instance_id()? != Some(expected) {
3835            return Err(CliError::InvalidInput(
3836                MCP_ERROR_WORKTREE_CONTROL_IDENTITY_CONFLICT.to_string(),
3837            ));
3838        }
3839        Ok(())
3840    }
3841
3842    /// Admit only federation snapshots that retain their captured alias identities.
3843    fn require_federated_worktree_identities(
3844        stores: Vec<FederatedStore>,
3845        selections: &[McpWorktreeSelection],
3846    ) -> Result<Vec<FederatedStore>, CliError> {
3847        if stores.len() != selections.len() {
3848            for store in stores {
3849                drop(store.finish());
3850            }
3851            return Err(CliError::InvalidInput(
3852                MCP_ERROR_FEDERATED_ALIAS_MISSING.to_string(),
3853            ));
3854        }
3855        for (store, selection) in stores.iter().zip(selections) {
3856            if let Err(error) =
3857                Self::require_captured_worktree_identity(Some(selection), store.store())
3858            {
3859                let error = federated_worktree_error(error, &selection.alias);
3860                for store in stores {
3861                    drop(store.finish());
3862                }
3863                return Err(error);
3864            }
3865        }
3866        Ok(stores)
3867    }
3868
3869    /// Run one rendered MCP query with request cancellation bridged into index work.
3870    fn with_fresh_string_for_request<F>(
3871        &self,
3872        state: &McpProjectState,
3873        context: Option<RequestContext<RoleServer>>,
3874        mut query: F,
3875    ) -> Result<String, CliError>
3876    where
3877        F: FnMut(&AtlasStore, VerifiedReadStamp) -> Result<String, CliError>,
3878    {
3879        self.with_fresh_string_and_usage_for_request(state, context, |store, stamp| {
3880            Ok((query(store, stamp)?, None))
3881        })
3882    }
3883
3884    /// Record optional usage only after source and `SQLite` result acceptance succeeds.
3885    fn with_fresh_string_and_usage_for_request<F>(
3886        &self,
3887        state: &McpProjectState,
3888        context: Option<RequestContext<RoleServer>>,
3889        mut query: F,
3890    ) -> Result<String, CliError>
3891    where
3892        F: FnMut(
3893            &AtlasStore,
3894            VerifiedReadStamp,
3895        ) -> Result<(String, Option<McpUsageIntent>), CliError>,
3896    {
3897        self.with_fresh_string_and_usage_controlled_for_request(
3898            state,
3899            context,
3900            |store, stamp, _control| query(store, stamp),
3901        )
3902    }
3903
3904    /// Render one verified MCP query that consumes request cancellation.
3905    fn with_fresh_string_and_usage_controlled_for_request<F>(
3906        &self,
3907        state: &McpProjectState,
3908        context: Option<RequestContext<RoleServer>>,
3909        query: F,
3910    ) -> Result<String, CliError>
3911    where
3912        F: FnMut(
3913            &AtlasStore,
3914            VerifiedReadStamp,
3915            &IndexWorkControl,
3916        ) -> Result<(String, Option<McpUsageIntent>), CliError>,
3917    {
3918        let outcome = self.with_fresh_store_controlled_for_request(state, context, query)?;
3919        let stamp = outcome.stamp.clone();
3920        let (value, usage) = outcome.value;
3921        let output_bytes = value.len();
3922        let outcome = VerifiedReadOutcome {
3923            value,
3924            stamp,
3925            work: outcome.work,
3926        }
3927        .with_output_bytes(output_bytes);
3928        if let Some(usage) = usage {
3929            self.record_accepted_usage(state, &outcome.stamp, &usage, &outcome.value);
3930        }
3931        Ok(outcome.value)
3932    }
3933
3934    /// Open the durable index for mutation and bind any migrated alias identity first.
3935    fn open_mut_store(
3936        state: &McpProjectState,
3937        control_state: &McpProjectState,
3938    ) -> Result<AtlasStore, CliError> {
3939        let Some(selection) = state
3940            .worktree
3941            .as_ref()
3942            .filter(|selection| selection.registration_id.is_some())
3943        else {
3944            let store = open_atlas_store_for_project(&state.db_path, &state.root)?;
3945            Self::require_captured_worktree_identity(state.worktree.as_ref(), &store)?;
3946            return Ok(store);
3947        };
3948        Self::open_registered_worktree_mut_store(state, control_state, selection)
3949    }
3950
3951    /// Admit a read-only observed no-op or run the existing mutable refresh owner.
3952    fn run_observed_watch_refresh(
3953        state: &McpProjectState,
3954        control_state: &McpProjectState,
3955        plan: &ScanRuntimePlan,
3956        options: &SymbolBuildOptions,
3957        control: &IndexWorkControl,
3958        observations: &SourceObservationRegistry,
3959        allow_observed_noop: bool,
3960    ) -> Result<WatchReport, CliError> {
3961        if allow_observed_noop && plan.root == state.root && state.db_path.is_file() {
3962            let current = observations.with_verified_current_read(
3963                &state.db_path,
3964                &state.root,
3965                state.config_path.as_deref(),
3966                control,
3967                |store, _stamp| {
3968                    Self::require_captured_worktree_identity(state.worktree.as_ref(), store)?;
3969                    publication_contract_matches(store, plan)
3970                },
3971            );
3972            match current {
3973                Ok(SourceReadAdmission::Current(outcome)) if outcome.value => {
3974                    return Ok(WatchReport::unchanged(plan.text_options));
3975                }
3976                Ok(SourceReadAdmission::Reconcile(assessment)) => {
3977                    let mut store = Self::open_existing_mut_store(state, control_state)?;
3978                    let refresh = refresh_index_from_assessment_controlled(
3979                        &mut store,
3980                        plan,
3981                        options,
3982                        *assessment,
3983                        control,
3984                    )?;
3985                    return Ok(WatchReport::from_refresh(refresh));
3986                }
3987                Ok(_) | Err(CliError::RefreshRequired(_) | CliError::VerificationIncomplete(_)) => {
3988                }
3989                Err(CliError::Db(source)) if source.supported_schema_migration().is_some() => {}
3990                Err(error) => return Err(error),
3991            }
3992        }
3993        let mut store = Self::open_existing_mut_store(state, control_state)?;
3994        run_single_watch_refresh_controlled(&mut store, plan, options, control)
3995    }
3996
3997    /// Open and, when needed, bind one alias under exact control-writer exclusion.
3998    fn open_registered_worktree_mut_store(
3999        state: &McpProjectState,
4000        control_state: &McpProjectState,
4001        selection: &McpWorktreeSelection,
4002    ) -> Result<AtlasStore, CliError> {
4003        let alias = WorktreeAlias::parse(&selection.alias)?;
4004        let registration_id =
4005            selection
4006                .registration_id
4007                .ok_or_else(|| DbError::WorktreeRegistrationNotFound {
4008                    alias: selection.alias.clone(),
4009                })?;
4010        let control =
4011            open_existing_atlas_store_for_project(&control_state.db_path, &control_state.root)?;
4012        Self::require_captured_control_identity(Some(selection), &control)?;
4013        control.with_active_worktree_registration(registration_id, &alias, |guard| {
4014            if let Err(error) =
4015                require_registered_worktree_lifecycle(guard.registration(), &state.root)
4016            {
4017                return Ok(Err(error));
4018            }
4019            if !state.db_path.is_file() {
4020                return Ok(Err(Self::with_target_error_context(
4021                    index_init_required(&state.root, &state.db_path),
4022                    state,
4023                )));
4024            }
4025            let target = match open_existing_atlas_store_for_project(&state.db_path, &state.root) {
4026                Ok(store) => store,
4027                Err(error) => return Ok(Err(error)),
4028            };
4029            let project = match target.captured_project_binding() {
4030                Ok(binding) => binding.project_instance_id,
4031                Err(error) => return Ok(Err(error.into())),
4032            };
4033            if selection
4034                .project_instance_id
4035                .is_some_and(|expected| expected != project)
4036            {
4037                return Ok(Err(CliError::InvalidInput(
4038                    MCP_ERROR_WORKTREE_IDENTITY_CONFLICT.to_string(),
4039                )));
4040            }
4041            match guard.registration().project_instance_id {
4042                Some(bound) if bound == project => {}
4043                Some(_) => {
4044                    return Ok(Err(CliError::InvalidInput(
4045                        MCP_ERROR_WORKTREE_IDENTITY_CONFLICT.to_string(),
4046                    )));
4047                }
4048                None => {
4049                    let snapshot = match target.export_worktree_usage_snapshot() {
4050                        Ok(snapshot) => snapshot,
4051                        Err(error) => return Ok(Err(error.into())),
4052                    };
4053                    if let Err(error) =
4054                        require_registered_worktree_lifecycle(guard.registration(), &state.root)
4055                    {
4056                        return Ok(Err(error));
4057                    }
4058                    if let Err(error) = require_current_worktree_usage_snapshot(
4059                        &state.db_path,
4060                        &state.root,
4061                        &snapshot,
4062                    ) {
4063                        return Ok(Err(error));
4064                    }
4065                    guard.bind_project_with_usage_snapshot(&state.root, project, &snapshot)?;
4066                }
4067            }
4068            Ok(Ok(target))
4069        })?
4070    }
4071
4072    /// Open an existing selected-project index for purpose or health mutation.
4073    fn open_existing_mut_store(
4074        state: &McpProjectState,
4075        control_state: &McpProjectState,
4076    ) -> Result<AtlasStore, CliError> {
4077        if !state.db_path.is_file() {
4078            return Err(Self::with_target_error_context(
4079                index_init_required(&state.root, &state.db_path),
4080                state,
4081            ));
4082        }
4083        if state
4084            .worktree
4085            .as_ref()
4086            .is_some_and(|selection| selection.registration_id.is_some())
4087        {
4088            return Self::open_mut_store(state, control_state);
4089        }
4090        let store = open_existing_atlas_store_for_project(&state.db_path, &state.root)?;
4091        Self::require_captured_worktree_identity(state.worktree.as_ref(), &store)?;
4092        Ok(store)
4093    }
4094
4095    /// Apply one purpose mutation under a source witness retained through commit.
4096    fn with_admitted_purpose_mutation<T>(
4097        &self,
4098        state: &McpProjectState,
4099        context: Option<RequestContext<RoleServer>>,
4100        mutation: impl FnOnce(&AtlasStore) -> Result<T, CliError>,
4101    ) -> Result<T, CliError> {
4102        if !state.db_path.is_file() {
4103            return Err(Self::with_target_error_context(
4104                index_init_required(&state.root, &state.db_path),
4105                state,
4106            ));
4107        }
4108        let control =
4109            index_work_control(&SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, None, None));
4110        let bridge = context
4111            .map(|context| McpRequestCancellationBridge::start(&context, &control))
4112            .transpose()?;
4113        let result = self
4114            .with_admitted_purpose_mutation_controlled(state, &control, bridge.as_ref(), mutation)
4115            .map_err(|error| Self::with_target_error_context(error, state));
4116        drop(bridge);
4117        result
4118    }
4119
4120    /// Apply one admitted mutation with explicit cancellation and rollback boundaries.
4121    fn with_admitted_purpose_mutation_controlled<T>(
4122        &self,
4123        state: &McpProjectState,
4124        control: &IndexWorkControl,
4125        bridge: Option<&McpRequestCancellationBridge>,
4126        mutation: impl FnOnce(&AtlasStore) -> Result<T, CliError>,
4127    ) -> Result<T, CliError> {
4128        if let Some(bridge) = bridge {
4129            bridge.synchronize(control);
4130        }
4131        let admission = self.source_observations.admit_mutation(
4132            &state.db_path,
4133            &state.root,
4134            state.config_path.as_deref(),
4135            control,
4136        )?;
4137        let store = Self::open_existing_mut_store(state, &self.control_state)?;
4138        Self::require_captured_worktree_identity(state.worktree.as_ref(), &store)?;
4139        let transaction = store.begin_purpose_mutation()?;
4140        let operation = (|| {
4141            let value = mutation(&store)?;
4142            if let Some(bridge) = bridge {
4143                bridge.synchronize(control);
4144            }
4145            admission.verify()?;
4146            if let Some(bridge) = bridge {
4147                bridge.synchronize(control);
4148            }
4149            control.check(IndexWorkStage::Publication)?;
4150            Ok(value)
4151        })();
4152        match operation {
4153            Ok(value) => {
4154                transaction.commit()?;
4155                Ok(value)
4156            }
4157            Err(operation) => Err(crate::rollback_rejected_purpose_mutation(
4158                transaction,
4159                operation,
4160            )),
4161        }
4162    }
4163
4164    /// Return whether this MCP process can record optional telemetry.
4165    fn telemetry_enabled() -> bool {
4166        !telemetry_disabled()
4167    }
4168
4169    /// Record telemetry and rotate only this project's scope if baselines are full.
4170    fn record_usage_for_state<F>(&self, state: &McpProjectState, store: &AtlasStore, record: F)
4171    where
4172        F: FnMut(UsageRuntimeInstance) -> Result<(), CliError>,
4173    {
4174        self.record_usage_for_origin(state, store, None, record);
4175    }
4176
4177    /// Record telemetry under one exact authority and optional worktree origin.
4178    fn record_usage_for_origin<F>(
4179        &self,
4180        state: &McpProjectState,
4181        store: &AtlasStore,
4182        worktree_registration_id: Option<i64>,
4183        mut record: F,
4184    ) where
4185        F: FnMut(UsageRuntimeInstance) -> Result<(), CliError>,
4186    {
4187        if telemetry_disabled() {
4188            return;
4189        }
4190        let Ok(binding) =
4191            McpUsageProjectBinding::capture_with_origin(state, store, worktree_registration_id)
4192        else {
4193            return;
4194        };
4195        let Some(project_instance) = self
4196            .usage_runtime
4197            .lock()
4198            .ok()
4199            .and_then(|mut runtime| runtime.instance_for_binding(binding, store))
4200        else {
4201            return;
4202        };
4203        let Ok(mut usage_instance) = project_instance.lock() else {
4204            return;
4205        };
4206        if !matches!(
4207            record(*usage_instance),
4208            Err(CliError::Db(DbError::TelemetryBaselineCapacity))
4209        ) {
4210            return;
4211        }
4212
4213        let Some(next_instance) = UsageRuntimeInstance::new(UsageInstanceOwner::McpProcess) else {
4214            return;
4215        };
4216        // Creating a candidate has no lifecycle effect; install it only after
4217        // the old identity is durably sealed so failure cannot leak or replace it.
4218        if usage_instance.seal(store).is_err() {
4219            return;
4220        }
4221        *usage_instance = next_instance;
4222        drop(record(next_instance));
4223    }
4224
4225    /// Best-effort telemetry for one result whose source epoch has already been accepted.
4226    fn record_accepted_usage(
4227        &self,
4228        state: &McpProjectState,
4229        stamp: &VerifiedReadStamp,
4230        intent: &McpUsageIntent,
4231        output: &str,
4232    ) {
4233        if !Self::telemetry_enabled() {
4234            return;
4235        }
4236        let Ok(store) = Self::open_read_store(state) else {
4237            return;
4238        };
4239        let Ok(binding) = store.captured_project_binding() else {
4240            return;
4241        };
4242        if binding.project_instance_id != stamp.project_instance_id {
4243            return;
4244        }
4245        if let Some(selection) = state
4246            .worktree
4247            .as_ref()
4248            .filter(|selection| selection.registration_id.is_some())
4249        {
4250            let Some(registration_id) = selection.registration_id else {
4251                return;
4252            };
4253            let event = match &intent.baseline {
4254                McpUsageBaseline::Estimate(baseline_tokens) => usage_from_estimates_with_context(
4255                    &self.session,
4256                    intent.command,
4257                    intent.path.clone(),
4258                    intent.query.clone(),
4259                    *baseline_tokens,
4260                    projectatlas_core::outline::estimate_tokens(output),
4261                    TOKEN_BUCKET_NAVIGATION_AVOIDANCE,
4262                    TOKEN_BASELINE_SELECTED_CANDIDATES,
4263                    TOKEN_CONFIDENCE_INFERRED,
4264                ),
4265                McpUsageBaseline::DirectoryWalk(baseline_tokens) => {
4266                    usage_from_estimates_with_context(
4267                        &self.session,
4268                        intent.command,
4269                        intent.path.clone(),
4270                        intent.query.clone(),
4271                        *baseline_tokens,
4272                        projectatlas_core::outline::estimate_tokens(output),
4273                        TOKEN_BUCKET_NAVIGATION_AVOIDANCE,
4274                        TOKEN_BASELINE_DIRECTORY_WALK,
4275                        TOKEN_CONFIDENCE_POLICY_ESTIMATE,
4276                    )
4277                }
4278                McpUsageBaseline::Text(baseline_text) => usage_from_text(
4279                    &self.session,
4280                    intent.command,
4281                    intent.path.clone(),
4282                    intent.query.clone(),
4283                    baseline_text,
4284                    output,
4285                ),
4286            };
4287            let Ok(control) = Self::open_read_store(&self.control_state) else {
4288                return;
4289            };
4290            if Self::require_captured_control_identity(Some(selection), &control).is_err() {
4291                return;
4292            }
4293            if control.finish_index_read_snapshot().is_ok() {
4294                self.record_usage_for_origin(
4295                    &self.control_state,
4296                    &control,
4297                    Some(registration_id),
4298                    |usage_instance| {
4299                        usage_instance.record_for_worktree(&control, registration_id, &event)
4300                    },
4301                );
4302            }
4303            return;
4304        }
4305        self.record_usage_for_state(state, &store, |usage_instance| match &intent.baseline {
4306            McpUsageBaseline::Estimate(baseline_tokens) => record_usage_estimate(
4307                &store,
4308                Some(usage_instance),
4309                &self.session,
4310                intent.command,
4311                intent.path.clone(),
4312                intent.query.clone(),
4313                *baseline_tokens,
4314                output,
4315            ),
4316            McpUsageBaseline::DirectoryWalk(baseline_tokens) => {
4317                record_directory_walk_usage_estimate(
4318                    &store,
4319                    Some(usage_instance),
4320                    &self.session,
4321                    intent.command,
4322                    intent.path.clone(),
4323                    intent.query.clone(),
4324                    *baseline_tokens,
4325                    output,
4326                )
4327            }
4328            McpUsageBaseline::Text(baseline_text) => record_usage_text(
4329                &store,
4330                Some(usage_instance),
4331                &self.session,
4332                intent.command,
4333                intent.path.clone(),
4334                intent.query.clone(),
4335                baseline_text,
4336                output,
4337            ),
4338        });
4339    }
4340
4341    /// Reuse an optional broad-source telemetry baseline within one complete generation.
4342    fn estimated_source_tokens_cached(
4343        &self,
4344        state: &McpProjectState,
4345        store: &AtlasStore,
4346        stamp: &VerifiedReadStamp,
4347        folder: Option<&str>,
4348        file_pattern: Option<&str>,
4349    ) -> Result<usize, CliError> {
4350        let key = McpSourceTokenBaselineKey {
4351            binding: McpUsageProjectBinding {
4352                root: state.root.clone(),
4353                db_path: state.db_path.clone(),
4354                project_instance_id: stamp.project_instance_id,
4355                worktree_registration_id: None,
4356            },
4357            generation: stamp.generation,
4358            folder: folder.map(ToOwned::to_owned),
4359            file_pattern: file_pattern.map(ToOwned::to_owned),
4360        };
4361        if let Some(value) = self
4362            .usage_runtime
4363            .lock()
4364            .ok()
4365            .and_then(|runtime| runtime.source_token_baseline(&key))
4366        {
4367            return Ok(value);
4368        }
4369        let value = estimated_source_tokens_for_indexed_files(store, folder, file_pattern)?;
4370        if let Ok(mut runtime) = self.usage_runtime.lock() {
4371            runtime.insert_source_token_baseline(key, value);
4372        }
4373        Ok(value)
4374    }
4375
4376    /// Best-effort seal each selected project's current identity at shutdown.
4377    fn seal_usage_instances_for_projects(&self) {
4378        let Some(projects) = self
4379            .usage_runtime
4380            .lock()
4381            .ok()
4382            .map(|runtime| runtime.snapshot())
4383        else {
4384            return;
4385        };
4386        for project in projects {
4387            let Ok(instance) = project.instance.lock() else {
4388                continue;
4389            };
4390            if let Ok(store) =
4391                open_atlas_store_for_project(&project.binding.db_path, &project.binding.root)
4392            {
4393                drop(instance.seal(&store));
4394            }
4395        }
4396    }
4397
4398    /// Load effective atlas config for the selected state.
4399    fn load_config_for_state(state: &McpProjectState) -> Result<AtlasMapConfig, CliError> {
4400        state
4401            .config_path
4402            .as_deref()
4403            .map_or_else(
4404                || load_atlas_config_for_root(&state.root).map_err(CliError::from),
4405                |config_path| load_atlas_config(Some(config_path)).map_err(CliError::from),
4406            )
4407            .map(|config| config.with_database_path(&state.db_path))
4408    }
4409
4410    /// Return the selected project root used by initialization.
4411    fn init_project_root(
4412        &self,
4413        project_path: Option<String>,
4414        worktree: Option<String>,
4415    ) -> Result<McpProjectState, CliError> {
4416        self.state_for_target_with_config_validation(
4417            project_path,
4418            worktree,
4419            McpConfigValidation::Immediate,
4420        )
4421    }
4422
4423    /// Initialize one registered worktree, preferring a reconciled control-atlas baseline.
4424    fn run_registered_worktree_init(
4425        &self,
4426        state: &McpProjectState,
4427        config_path: &Path,
4428        options: &InitBootstrapOptions,
4429    ) -> Result<InitSetupReport, CliError> {
4430        let Some(selection) = state
4431            .worktree
4432            .as_ref()
4433            .filter(|selection| selection.registration_id.is_some())
4434        else {
4435            let report =
4436                run_init_bootstrap(&state.root, &state.db_path, Some(config_path), options)?;
4437            if report.ok {
4438                self.bind_initialized_registration_for_root(state)?;
4439            }
4440            return Ok(report);
4441        };
4442        if state.db_path.is_file() {
4443            let mut report =
4444                run_init_bootstrap(&state.root, &state.db_path, Some(config_path), options)?;
4445            report.hydration = Some(InitHydrationPhase {
4446                status: InitHydrationStatus::Existing,
4447                source_root: None,
4448                source_project_instance_id: None,
4449                target_project_instance_id: None,
4450                baseline_generation: None,
4451                reconciled_generation: None,
4452                fallback_reason: None,
4453            });
4454            if report.ok {
4455                self.bind_initialized_worktree(selection, state)?;
4456            }
4457            return Ok(report);
4458        }
4459
4460        let state_guard =
4461            crate::runtime::preflight_generated_state(&state.root, &state.db_path, config_path)?;
4462        preflight_existing_project_binding(&state.db_path, &state.root)?;
4463        let project_dir = state.root.join(PROJECTATLAS_DIR_NAME);
4464        let nonsource_file = project_dir.join(MCP_NONSOURCE_FILE_NAME);
4465        let project_dir_existed = project_dir.exists();
4466        let config_existed = config_path.exists();
4467        let nonsource_existed = nonsource_file.exists();
4468        init_project_with_config(&state.root, Some(config_path))?;
4469
4470        state_guard.verify().map_err(|source| CliError::Io {
4471            path: project_dir.clone(),
4472            source,
4473        })?;
4474
4475        let hydration = if options.no_scan {
4476            McpWorktreeHydration::Fallback(MCP_HYDRATION_NO_SCAN_REASON.to_string())
4477        } else {
4478            self.attempt_worktree_hydration(state, config_path, options)?
4479        };
4480        let mut report = match hydration {
4481            McpWorktreeHydration::Activated { hydration, scan } => {
4482                let mut report = run_init_bootstrap(
4483                    &state.root,
4484                    &state.db_path,
4485                    Some(config_path),
4486                    &InitBootstrapOptions {
4487                        no_scan: true,
4488                        force_rescan: options.force_rescan,
4489                        text_index_max_bytes: options.text_index_max_bytes,
4490                    },
4491                )?;
4492                report.scan = InitScanPhase {
4493                    status: InitPhaseStatus::Verified,
4494                    requested: true,
4495                    force_rescan: options.force_rescan,
4496                    report: Some(*scan),
4497                    error: None,
4498                };
4499                report.next_steps =
4500                    init_next_steps(false, false, report.purpose_handoff.queue.total);
4501                report.hydration = Some(hydration);
4502                report
4503            }
4504            McpWorktreeHydration::Fallback(reason) => {
4505                let mut report =
4506                    run_init_bootstrap(&state.root, &state.db_path, Some(config_path), options)?;
4507                report.hydration = Some(InitHydrationPhase {
4508                    status: InitHydrationStatus::Fallback,
4509                    source_root: lossless_project_root_display(&self.control_state.root),
4510                    source_project_instance_id: None,
4511                    target_project_instance_id: None,
4512                    baseline_generation: None,
4513                    reconciled_generation: None,
4514                    fallback_reason: Some(reason),
4515                });
4516                report
4517            }
4518        };
4519        report.project_dir.status = if project_dir_existed {
4520            InitPhaseStatus::Exists
4521        } else {
4522            InitPhaseStatus::Created
4523        };
4524        report.config.status = if config_existed {
4525            InitPhaseStatus::Exists
4526        } else {
4527            InitPhaseStatus::Created
4528        };
4529        report.nonsource_files.status = if nonsource_existed {
4530            InitPhaseStatus::Exists
4531        } else {
4532            InitPhaseStatus::Created
4533        };
4534        report.db.status = InitPhaseStatus::Created;
4535        if report.ok {
4536            self.bind_initialized_worktree(selection, state)?;
4537        }
4538        Ok(report)
4539    }
4540
4541    /// Build, reconcile, and no-clobber activate one private hydration candidate.
4542    fn attempt_worktree_hydration(
4543        &self,
4544        state: &McpProjectState,
4545        config_path: &Path,
4546        options: &InitBootstrapOptions,
4547    ) -> Result<McpWorktreeHydration, CliError> {
4548        let selection = state
4549            .worktree
4550            .as_ref()
4551            .filter(|selection| selection.registration_id.is_some())
4552            .ok_or_else(|| CliError::InvalidInput(MCP_ERROR_FEDERATED_ALIAS_MISSING.to_string()))?;
4553        let source = match Self::open_read_store(&self.control_state) {
4554            Ok(source) => source,
4555            Err(error) if Self::hydration_can_fallback(&error) => {
4556                return Ok(McpWorktreeHydration::Fallback(error.to_string()));
4557            }
4558            Err(error) => return Err(error),
4559        };
4560        Self::require_captured_control_identity(state.worktree.as_ref(), &source)?;
4561        let symbol_options = SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, None, None);
4562        let control = index_work_control(&symbol_options);
4563        let mut candidate =
4564            match source.prepare_worktree_hydration(&state.root, &state.db_path, &control) {
4565                Ok(candidate) => candidate,
4566                Err(error) => {
4567                    let error = Self::hydration_db_error(error);
4568                    if Self::hydration_can_fallback(&error) {
4569                        return Ok(McpWorktreeHydration::Fallback(error.to_string()));
4570                    }
4571                    return Err(error);
4572                }
4573            };
4574        let candidate_path = candidate
4575            .path()
4576            .map_err(Self::hydration_db_error)?
4577            .to_path_buf();
4578        let mut target = open_atlas_store_for_project(&candidate_path, &state.root)?;
4579        let plan = ScanRuntimePlan::for_path_controlled(
4580            Some(config_path),
4581            &state.root,
4582            options.text_index_max_bytes,
4583            &control,
4584        )?;
4585        let (scan, source_unchanged) =
4586            reconcile_hydrated_index_controlled(&mut target, &plan, &symbol_options, &control)?;
4587        drop(target);
4588        if source_unchanged {
4589            candidate
4590                .accept_verified_source_state(&control)
4591                .map_err(Self::hydration_db_error)?;
4592        }
4593        drop(source);
4594        let candidate = candidate
4595            .prepare_activation(&control)
4596            .map_err(Self::hydration_db_error)?;
4597        let activation = match self
4598            .activate_registered_worktree_hydration(state, selection, candidate, &control)
4599        {
4600            Ok(activation) => activation,
4601            Err(CliError::Db(error @ DbError::WorktreeHydrationDestinationExists { .. })) => {
4602                return Ok(McpWorktreeHydration::Fallback(error.to_string()));
4603            }
4604            Err(error) => return Err(error),
4605        };
4606        Ok(McpWorktreeHydration::Activated {
4607            hydration: InitHydrationPhase {
4608                status: InitHydrationStatus::Hydrated,
4609                source_root: lossless_project_root_display(&self.control_state.root),
4610                source_project_instance_id: Some(activation.source_project_instance_id.to_string()),
4611                target_project_instance_id: Some(activation.target_project_instance_id.to_string()),
4612                baseline_generation: Some(activation.baseline_generation.get()),
4613                reconciled_generation: Some(activation.reconciled_generation.get()),
4614                fallback_reason: None,
4615            },
4616            scan: Box::new(scan),
4617        })
4618    }
4619
4620    /// Publish one prepared hydration candidate only for the still-exact registered lifecycle.
4621    fn activate_registered_worktree_hydration(
4622        &self,
4623        state: &McpProjectState,
4624        selection: &McpWorktreeSelection,
4625        candidate: PreparedWorktreeHydrationCandidate,
4626        work_control: &IndexWorkControl,
4627    ) -> Result<WorktreeHydrationActivation, CliError> {
4628        self.activate_registered_worktree_hydration_with_post_publication(
4629            state,
4630            selection,
4631            candidate,
4632            work_control,
4633            || Ok(()),
4634        )
4635    }
4636
4637    /// Publish and bind a verified candidate with one deterministic final-admission seam.
4638    fn activate_registered_worktree_hydration_with_post_publication<F>(
4639        &self,
4640        state: &McpProjectState,
4641        selection: &McpWorktreeSelection,
4642        candidate: PreparedWorktreeHydrationCandidate,
4643        work_control: &IndexWorkControl,
4644        post_publication: F,
4645    ) -> Result<WorktreeHydrationActivation, CliError>
4646    where
4647        F: FnOnce() -> Result<(), CliError>,
4648    {
4649        let alias = WorktreeAlias::parse(&selection.alias)?;
4650        let registration_id =
4651            selection
4652                .registration_id
4653                .ok_or_else(|| DbError::WorktreeRegistrationNotFound {
4654                    alias: selection.alias.clone(),
4655                })?;
4656        let control = Self::open_existing_mut_store(&self.control_state, &self.control_state)?;
4657        Self::require_captured_control_identity(Some(selection), &control)?;
4658        control.with_active_worktree_registration(registration_id, &alias, |guard| {
4659            if let Err(error) =
4660                require_registered_worktree_lifecycle(guard.registration(), &state.root)
4661            {
4662                return Ok(Err(error));
4663            }
4664            let activation = candidate.activate(work_control)?;
4665            if let Err(error) = post_publication() {
4666                return Ok(Err(error));
4667            }
4668            let target =
4669                match open_atlas_store_read_only_for_project(&activation.database, &state.root) {
4670                    Ok(target) => target,
4671                    Err(error) => return Ok(Err(error)),
4672                };
4673            let snapshot = match target.export_worktree_usage_snapshot() {
4674                Ok(snapshot) => snapshot,
4675                Err(error) => return Ok(Err(error.into())),
4676            };
4677            if let Err(error) =
4678                require_registered_worktree_lifecycle(guard.registration(), &state.root)
4679            {
4680                return Ok(Err(error));
4681            }
4682            if let Err(error) = require_current_worktree_usage_snapshot(
4683                &activation.database,
4684                &state.root,
4685                &snapshot,
4686            ) {
4687                return Ok(Err(error));
4688            }
4689            guard.bind_project_with_usage_snapshot(
4690                &state.root,
4691                activation.target_project_instance_id,
4692                &snapshot,
4693            )?;
4694            Ok(Ok(activation))
4695        })?
4696    }
4697
4698    /// Bind a successfully initialized alias to its exact local atlas identity.
4699    fn bind_initialized_worktree(
4700        &self,
4701        selection: &McpWorktreeSelection,
4702        state: &McpProjectState,
4703    ) -> Result<(), CliError> {
4704        drop(Self::open_registered_worktree_mut_store(
4705            state,
4706            &self.control_state,
4707            selection,
4708        )?);
4709        Ok(())
4710    }
4711
4712    /// Bind a legacy exact-path init when its root already has an active alias.
4713    fn bind_initialized_registration_for_root(
4714        &self,
4715        state: &McpProjectState,
4716    ) -> Result<(), CliError> {
4717        let Some(repository) = self.control_git_repository_if_present()? else {
4718            return Ok(());
4719        };
4720        let Some(entry) = repository
4721            .worktrees
4722            .iter()
4723            .find(|entry| Self::active_worktree_root(entry).is_some_and(|root| root == state.root))
4724        else {
4725            return Ok(());
4726        };
4727        let administrative_identity = git_administrative_identity(&entry.administrative_directory)?;
4728        let administrative_directory =
4729            CanonicalProjectRoot::from_path(&entry.administrative_directory)
4730                .map_err(|source| CliError::InvalidInput(source.to_string()))?;
4731        let control = Self::open_existing_mut_store(&self.control_state, &self.control_state)?;
4732        let control_project_instance_id = control.captured_project_binding()?.project_instance_id;
4733        let Some(registration) =
4734            control
4735                .worktree_registrations(false)?
4736                .into_iter()
4737                .find(|registration| {
4738                    registration.git_administrative_directory_identity == administrative_directory
4739                        && registration.git_administrative_identity == administrative_identity
4740                })
4741        else {
4742            return Ok(());
4743        };
4744        self.bind_initialized_worktree(
4745            &McpWorktreeSelection {
4746                alias: registration.alias.to_string(),
4747                registration_id: Some(registration.registration_id),
4748                project_instance_id: registration.project_instance_id,
4749                control_project_instance_id: Some(control_project_instance_id),
4750            },
4751            state,
4752        )
4753    }
4754
4755    /// Preserve cancellation/resource failures and fallback only for unusable source baselines.
4756    fn hydration_can_fallback(error: &CliError) -> bool {
4757        matches!(
4758            error,
4759            CliError::Db(
4760                DbError::WorktreeHydrationInvalid { .. }
4761                    | DbError::WorktreeHydrationDestinationExists { .. }
4762                    | DbError::WorktreeHydrationBackupBusy { .. }
4763                    | DbError::GraphPublicationUnavailable
4764                    | DbError::GraphProjectIdentityMismatch { .. }
4765                    | DbError::DerivedSnapshotInvalid { .. }
4766                    | DbError::DerivedSnapshotLimit { .. }
4767                    | DbError::SchemaVersion { .. }
4768                    | DbError::SchemaVersionMissing
4769                    | DbError::SchemaShape { .. }
4770                    | DbError::IntegrityCheck { .. }
4771                    | DbError::ProjectRootMissing
4772                    | DbError::ProjectInstanceIdentityMissing
4773            )
4774        )
4775    }
4776
4777    /// Lift database-owned cancellation into the shared CLI work-control error.
4778    fn hydration_db_error(error: DbError) -> CliError {
4779        match error {
4780            DbError::IndexWork(failure) => failure.into(),
4781            error => error.into(),
4782        }
4783    }
4784
4785    /// Return the initialized selected project root used by admin-style MCP calls.
4786    fn admin_project_root(
4787        &self,
4788        project_path: Option<String>,
4789        worktree: Option<String>,
4790    ) -> Result<McpProjectState, CliError> {
4791        self.state_for_target(project_path, worktree)
4792    }
4793
4794    /// Parse an MCP ignore kind parameter.
4795    fn parse_ignore_kind(
4796        kind: Option<&str>,
4797        required: bool,
4798    ) -> Result<Option<IgnoreEntryKind>, CliError> {
4799        let Some(kind) = kind.map(str::trim).filter(|value| !value.is_empty()) else {
4800            return if required {
4801                Err(CliError::InvalidInput(
4802                    MCP_ERROR_IGNORE_KIND_REQUIRED.to_string(),
4803                ))
4804            } else {
4805                Ok(None)
4806            };
4807        };
4808        match kind {
4809            MCP_IGNORE_KIND_DIR_NAME | MCP_IGNORE_KIND_DIR_NAME_ALIAS => {
4810                Ok(Some(IgnoreEntryKind::DirName))
4811            }
4812            MCP_IGNORE_KIND_PATH_PREFIX | MCP_IGNORE_KIND_PATH_PREFIX_ALIAS => {
4813                Ok(Some(IgnoreEntryKind::PathPrefix))
4814            }
4815            other => Err(CliError::InvalidInput(Self::invalid_parameter_message(
4816                MCP_ERROR_INVALID_IGNORE_KIND_PREFIX,
4817                other,
4818                MCP_ERROR_INVALID_IGNORE_KIND_SUFFIX,
4819            ))),
4820        }
4821    }
4822
4823    /// Parse an MCP purpose lint level parameter.
4824    fn parse_purpose_lint_level(value: Option<&str>) -> Result<PurposeLintLevel, CliError> {
4825        match value.map(str::trim).filter(|value| !value.is_empty()) {
4826            None | Some(MCP_PURPOSE_LEVEL_LOW) => Ok(PurposeLintLevel::Low),
4827            Some(MCP_PURPOSE_LEVEL_MEDIUM) => Ok(PurposeLintLevel::Medium),
4828            Some(MCP_PURPOSE_LEVEL_STRICT) => Ok(PurposeLintLevel::Strict),
4829            Some(other) => Err(CliError::InvalidInput(Self::invalid_parameter_message(
4830                MCP_ERROR_INVALID_PURPOSE_LEVEL_PREFIX,
4831                other,
4832                MCP_ERROR_INVALID_PURPOSE_LEVEL_SUFFIX,
4833            ))),
4834        }
4835    }
4836
4837    /// Parse an MCP harness config parameter.
4838    fn parse_harness_config(value: Option<&str>) -> Result<HarnessConfig, CliError> {
4839        match value.map(str::trim).filter(|value| !value.is_empty()) {
4840            None | Some(MCP_HARNESS_MCP_JSON | MCP_HARNESS_MCP_JSON_ALIAS) => {
4841                Ok(HarnessConfig::McpJson)
4842            }
4843            Some(MCP_HARNESS_CODEX) => Ok(HarnessConfig::Codex),
4844            Some(MCP_HARNESS_CLAUDE_CODE | MCP_HARNESS_CLAUDE_CODE_ALIAS) => {
4845                Ok(HarnessConfig::ClaudeCode)
4846            }
4847            Some(MCP_HARNESS_OPENCODE) => Ok(HarnessConfig::OpenCode),
4848            Some(other) => Err(CliError::InvalidInput(Self::invalid_parameter_message(
4849                MCP_ERROR_INVALID_HARNESS_PREFIX,
4850                other,
4851                MCP_ERROR_INVALID_HARNESS_SUFFIX,
4852            ))),
4853        }
4854    }
4855
4856    /// Parse the optional token chart theme parameter.
4857    fn parse_token_chart_theme(value: Option<&str>) -> Result<TokenDashboardTheme, CliError> {
4858        match value.map(str::trim).filter(|value| !value.is_empty()) {
4859            None => Ok(TokenDashboardTheme::Dark),
4860            Some(theme) => TokenDashboardTheme::parse(theme).ok_or_else(|| {
4861                CliError::InvalidInput(Self::invalid_parameter_message(
4862                    TOKEN_CHART_THEME_ERROR_PREFIX,
4863                    theme,
4864                    TOKEN_CHART_THEME_ERROR_SUFFIX,
4865                ))
4866            }),
4867        }
4868    }
4869
4870    /// Build an invalid-parameter diagnostic from centralized fragments.
4871    fn invalid_parameter_message(prefix: &str, value: &str, suffix: &str) -> String {
4872        let mut message = String::with_capacity(prefix.len() + value.len() + suffix.len());
4873        message.push_str(prefix);
4874        message.push_str(value);
4875        message.push_str(suffix);
4876        message
4877    }
4878
4879    /// Build the current map report, applying CI policy only to the optional sidecar.
4880    fn build_map_report(
4881        state: &McpProjectState,
4882        json: bool,
4883        force: bool,
4884    ) -> Result<String, CliError> {
4885        Self::build_map_report_with_limit(state, json, force, MAX_MAP_RESPONSE_BYTES)
4886    }
4887
4888    /// Keep the complete-envelope refusal before the optional sidecar write.
4889    fn build_map_report_with_limit(
4890        state: &McpProjectState,
4891        json: bool,
4892        force: bool,
4893        response_limit: usize,
4894    ) -> Result<String, CliError> {
4895        let config = Self::load_config_for_state(state)?;
4896        let skipped_reason = if json
4897            && !force
4898            && (crate::truthy_env(MCP_ENV_CI) || crate::truthy_env(MCP_ENV_GITHUB_ACTIONS))
4899        {
4900            Some(MCP_MAP_SKIPPED_IN_CI_REASON.to_string())
4901        } else {
4902            None
4903        };
4904        let json_written = json && skipped_reason.is_none();
4905        let response = render_map(&config, json, false)?;
4906        let report = McpMapReport {
4907            root: lossless_project_root_display(&config.root),
4908            map_path: lossless_native_path_display(&config.map_path),
4909            written: false,
4910            json,
4911            format: if json {
4912                crate::OutputFormat::Json
4913            } else {
4914                crate::OutputFormat::Toon
4915            },
4916            content: response.content,
4917            json_written,
4918            skipped_reason,
4919        };
4920        let encoded = Self::encode_named_payload(MCP_PAYLOAD_MAP, &report)?;
4921        enforce_map_response_limit(&encoded, response_limit)?;
4922        if json_written {
4923            write_json_map(&report.content, &config)?;
4924        }
4925        Ok(encoded)
4926    }
4927
4928    /// Build typed MCP session capabilities from active server state.
4929    fn session_capabilities(
4930        &self,
4931        state: &McpProjectState,
4932        text_index_max_bytes: u64,
4933    ) -> McpSessionCapabilities {
4934        McpSessionCapabilities {
4935            runtime: build_runtime_info(),
4936            selected_project: Self::selected_project_capability(state),
4937            startup_policy: McpStartupPolicy {
4938                nearest_project: Self::policy_state(self.allow_nearest_project),
4939            },
4940            path_scope: self.path_scope(),
4941            scan_policy: McpScanPolicy {
4942                implicit_scan: McpPolicyState::Disabled,
4943                text_index_max_bytes,
4944            },
4945            classified_navigation: classified_navigation_capabilities(),
4946            telemetry: McpTelemetryPolicy {
4947                mode: Self::policy_state(!telemetry_disabled()),
4948            },
4949            privacy: McpPrivacyPolicy {
4950                environment_dump: false,
4951                secret_values: false,
4952                projectatlas_paths_only: true,
4953            },
4954        }
4955    }
4956
4957    /// Encode settings plus additive MCP-session capability fields.
4958    fn render_settings_with_capabilities(
4959        &self,
4960        state: &McpProjectState,
4961    ) -> Result<String, CliError> {
4962        let admission = crate::runtime::preflight_managed_database(&state.db_path, &state.root)?;
4963        let database = crate::runtime::admitted_database_path(&state.db_path, admission.as_ref());
4964        let report =
4965            build_settings_report(database, state.config_path.as_deref(), OutputFormat::Toon)?;
4966        let capabilities = self.session_capabilities(state, report.text_index_max_bytes);
4967        let rendered = Self::encode_two_named_payloads(
4968            MCP_PAYLOAD_SETTINGS,
4969            &report,
4970            MCP_PAYLOAD_SESSION_CAPABILITIES,
4971            &capabilities,
4972        )?;
4973        if rendered.len() > MCP_SETTINGS_RESPONSE_MAX_BYTES {
4974            let mut message = MCP_SETTINGS_RESPONSE_LIMIT_PREFIX.to_string();
4975            message.push_str(&rendered.len().to_string());
4976            message.push_str(MCP_SETTINGS_RESPONSE_LIMIT_SEPARATOR);
4977            message.push_str(&MCP_SETTINGS_RESPONSE_MAX_BYTES.to_string());
4978            message.push_str(MCP_SETTINGS_RESPONSE_LIMIT_SUFFIX);
4979            return Err(CliError::InvalidInput(message));
4980        }
4981        Ok(rendered)
4982    }
4983
4984    /// Build the selected-project capability row.
4985    fn selected_project_capability(state: &McpProjectState) -> McpSelectedProjectCapability {
4986        McpSelectedProjectCapability {
4987            worktree: state
4988                .worktree
4989                .as_ref()
4990                .map(|selection| selection.alias.clone()),
4991            registration_id: state
4992                .worktree
4993                .as_ref()
4994                .and_then(|selection| selection.registration_id),
4995            root: lossless_project_root_display(&state.root),
4996            db: lossless_native_path_display(&state.db_path),
4997            config: state
4998                .config_path
4999                .as_ref()
5000                .and_then(|path| lossless_native_path_display(path)),
5001            index_status: if state.db_path.exists() {
5002                McpIndexStatus::Available
5003            } else {
5004                McpIndexStatus::Missing
5005            },
5006        }
5007    }
5008
5009    /// Return the path scope for this MCP server.
5010    fn path_scope(&self) -> McpPathScope {
5011        if self.allow_nearest_project {
5012            McpPathScope::NearestIndexedProject
5013        } else {
5014            McpPathScope::SelectedProject
5015        }
5016    }
5017
5018    /// Convert a bool into a serialized policy state.
5019    fn policy_state(enabled: bool) -> McpPolicyState {
5020        if enabled {
5021            McpPolicyState::Enabled
5022        } else {
5023            McpPolicyState::Disabled
5024        }
5025    }
5026
5027    /// Clamp a caller-provided brief limit to the supported range.
5028    fn brief_limit(value: Option<usize>) -> usize {
5029        value
5030            .unwrap_or(SESSION_BRIEF_DEFAULT_LIMIT)
5031            .clamp(1, SESSION_BRIEF_MAX_LIMIT)
5032    }
5033
5034    /// Build a read-only agent startup brief.
5035    fn build_session_brief(
5036        &self,
5037        params: AtlasSessionBriefParams,
5038        context: Option<RequestContext<RoleServer>>,
5039    ) -> Result<McpSessionBrief, CliError> {
5040        let selected_project_path = params.project_path.clone();
5041        let selected_worktree = params.worktree.clone();
5042        let state =
5043            self.state_for_target(selected_project_path.clone(), selected_worktree.clone())?;
5044        let query = Self::query_or_empty(params.query);
5045        let purpose_task = params
5046            .purpose_task
5047            .unwrap_or_else(|| MCP_PURPOSE_TASK_SESSION_STARTUP.to_string());
5048        let folder_limit = Self::brief_limit(params.folder_limit);
5049        let file_limit = Self::brief_limit(params.file_limit);
5050        let blocker_limit = Self::brief_limit(params.blocker_limit);
5051        let purpose_limit = Self::brief_limit(params.purpose_limit);
5052        let project = Self::selected_project_capability(&state);
5053        if !state.db_path.exists() {
5054            let init_project_path = selected_worktree
5055                .is_none()
5056                .then(|| lossless_project_root_display(&state.root))
5057                .flatten();
5058            return Ok(McpSessionBrief {
5059                project,
5060                policy: self.brief_policy(),
5061                overview: None,
5062                folders: Vec::new(),
5063                files: Vec::new(),
5064                blockers: McpBriefBlockers {
5065                    total: 0,
5066                    returned: 0,
5067                    truncated: false,
5068                    items: Vec::new(),
5069                },
5070                purpose_handoff: None,
5071                recommendations: Self::missing_index_recommendations(
5072                    init_project_path,
5073                    selected_worktree,
5074                ),
5075                limits: McpBriefLimits {
5076                    folder_limit,
5077                    file_limit,
5078                    blocker_limit,
5079                    purpose_limit,
5080                    folders_truncated: false,
5081                    files_truncated: false,
5082                    purposes_truncated: false,
5083                },
5084            });
5085        }
5086        let outcome = self.with_fresh_store_for_request(&state, context, |store, _stamp| {
5087            let overview = store.overview()?;
5088            let folder_rows =
5089                ranked_folder_nodes_with_reasons(store, &query, folder_limit.saturating_add(1))?;
5090            let file_rows = ranked_file_nodes_with_reasons(
5091                store,
5092                &query,
5093                None,
5094                None,
5095                file_limit.saturating_add(1),
5096                false,
5097            )?;
5098            let blockers = Self::brief_blockers(store, blocker_limit)?;
5099            let purpose_query = HealthQuery {
5100                start_index: 0,
5101                limit: purpose_limit,
5102                category: None,
5103                severity: None,
5104                path_prefix: None,
5105                summary_only: false,
5106                scope: HealthScope::purpose_default(),
5107            };
5108            let purpose_queue = purpose_curation_page(store, &purpose_query, &purpose_task)?;
5109            let purposes_truncated = purpose_queue.truncated;
5110            let folders_truncated = folder_rows.len() > folder_limit;
5111            let files_truncated = file_rows.len() > file_limit;
5112            let next_navigation_call = file_rows.first().map(|row| row.next_call.clone());
5113            Ok(McpSessionBrief {
5114                project: Self::selected_project_capability(&state),
5115                policy: self.brief_policy(),
5116                overview: Some(overview),
5117                folders: folder_rows
5118                    .into_iter()
5119                    .take(folder_limit)
5120                    .map(Self::brief_candidate)
5121                    .collect(),
5122                files: file_rows
5123                    .into_iter()
5124                    .take(file_limit)
5125                    .map(Self::brief_candidate)
5126                    .collect(),
5127                recommendations: Self::indexed_project_recommendations(
5128                    &query,
5129                    next_navigation_call,
5130                    blockers.total,
5131                    blocker_limit,
5132                    selected_project_path.clone(),
5133                    selected_worktree.clone(),
5134                ),
5135                blockers,
5136                purpose_handoff: purpose_queue
5137                    .actionable
5138                    .then(|| purpose_curator_handoff(purpose_queue)),
5139                limits: McpBriefLimits {
5140                    folder_limit,
5141                    file_limit,
5142                    blocker_limit,
5143                    purpose_limit,
5144                    folders_truncated,
5145                    files_truncated,
5146                    purposes_truncated,
5147                },
5148            })
5149        })?;
5150        Ok(outcome.value)
5151    }
5152
5153    /// Build brief policy fields.
5154    fn brief_policy(&self) -> McpBriefPolicy {
5155        McpBriefPolicy {
5156            nearest_project: Self::policy_state(self.allow_nearest_project),
5157            path_scope: self.path_scope(),
5158        }
5159    }
5160
5161    /// Build the additive compact projection without changing legacy defaults or query behavior.
5162    fn build_compact_session_brief(
5163        &self,
5164        mut params: AtlasSessionBriefParams,
5165        context: Option<RequestContext<RoleServer>>,
5166    ) -> Result<McpCompactSessionBrief, CliError> {
5167        let project_path = params.project_path.clone();
5168        let worktree = params.worktree.clone();
5169        params.compact = None;
5170        params
5171            .folder_limit
5172            .get_or_insert(COMPACT_SESSION_BRIEF_DEFAULT_LIMIT);
5173        params
5174            .file_limit
5175            .get_or_insert(COMPACT_SESSION_BRIEF_DEFAULT_LIMIT);
5176        params
5177            .blocker_limit
5178            .get_or_insert(COMPACT_SESSION_BRIEF_DEFAULT_LIMIT);
5179        params
5180            .purpose_limit
5181            .get_or_insert(COMPACT_SESSION_BRIEF_DEFAULT_LIMIT);
5182        let brief = self.build_session_brief(params, context)?;
5183        Ok(Self::compact_session_brief(
5184            &brief,
5185            project_path.as_deref(),
5186            worktree.as_deref(),
5187        ))
5188    }
5189
5190    /// Project the compatibility report into the explicit compact response shape.
5191    fn compact_session_brief(
5192        brief: &McpSessionBrief,
5193        project_path: Option<&str>,
5194        worktree: Option<&str>,
5195    ) -> McpCompactSessionBrief {
5196        let omit_folders = !brief.files.is_empty();
5197        let folders_truncated =
5198            brief.limits.folders_truncated || (omit_folders && !brief.folders.is_empty());
5199        let compact_limits = McpCompactBriefLimits {
5200            folder_limit: brief.limits.folder_limit,
5201            file_limit: brief.limits.file_limit,
5202            blocker_limit: brief.limits.blocker_limit,
5203            purpose_limit: brief.limits.purpose_limit,
5204            folders_truncated,
5205            files_truncated: brief.limits.files_truncated,
5206            purposes_truncated: brief.limits.purposes_truncated,
5207        };
5208        let limits_are_default = is_compact_brief_default_limit(&compact_limits.folder_limit)
5209            && is_compact_brief_default_limit(&compact_limits.file_limit)
5210            && is_compact_brief_default_limit(&compact_limits.blocker_limit)
5211            && is_compact_brief_default_limit(&compact_limits.purpose_limit)
5212            && !compact_limits.folders_truncated
5213            && !compact_limits.files_truncated
5214            && !compact_limits.purposes_truncated;
5215        McpCompactSessionBrief {
5216            project: McpCompactBriefProject {
5217                worktree: brief.project.worktree.clone(),
5218                registration_id: brief.project.registration_id,
5219                root: brief.project.root.clone(),
5220                index_status: brief.project.index_status,
5221            },
5222            policy: (!brief.policy.is_default()).then_some(brief.policy),
5223            overview: brief
5224                .overview
5225                .as_ref()
5226                .map(|overview| McpCompactBriefOverview {
5227                    files: overview.files,
5228                    folders: overview.folders,
5229                }),
5230            folders: if omit_folders {
5231                Vec::new()
5232            } else {
5233                brief
5234                    .folders
5235                    .iter()
5236                    .map(Self::compact_brief_candidate)
5237                    .collect()
5238            },
5239            files: brief
5240                .files
5241                .iter()
5242                .map(Self::compact_brief_candidate)
5243                .collect(),
5244            blockers: (brief.blockers.total > 0).then_some(McpCompactBriefBlockers {
5245                total: brief.blockers.total,
5246            }),
5247            purpose_handoff: brief.purpose_handoff.as_ref().map(|handoff| {
5248                Self::compact_brief_purpose_handoff(
5249                    handoff,
5250                    project_path.map(ToString::to_string),
5251                    worktree.map(ToString::to_string),
5252                )
5253            }),
5254            recommendations: brief
5255                .recommendations
5256                .iter()
5257                .map(Self::compact_brief_recommendation)
5258                .collect(),
5259            limits: (!limits_are_default).then_some(compact_limits),
5260        }
5261    }
5262
5263    /// Convert a ranked node into the compatibility-preserving startup candidate.
5264    fn brief_candidate(row: RankedNode) -> McpBriefCandidate {
5265        let purpose_agent_reviewed = row.node.purpose.agent_reviewed();
5266        McpBriefCandidate {
5267            path: row.node.node.path,
5268            kind: row.node.node.kind.to_string(),
5269            purpose_status: row.node.purpose.status,
5270            purpose_source: row.node.purpose.source,
5271            purpose_agent_reviewed,
5272            purpose: row.node.purpose.purpose,
5273            summary: row.node.summary,
5274            reasons: row.reasons,
5275            reason_codes: row.reason_codes,
5276            connection_counts: row.connection_counts,
5277            connections: row.connections,
5278            connections_truncated: row.connections_truncated,
5279            next_call: row.next_call,
5280        }
5281    }
5282
5283    /// Project one compatibility candidate into the bounded compact shape.
5284    fn compact_brief_candidate(row: &McpBriefCandidate) -> McpCompactBriefCandidate {
5285        let connections = row
5286            .connections
5287            .iter()
5288            .find(|connection| Self::brief_connection_is_crisp(connection))
5289            .cloned()
5290            .into_iter()
5291            .collect::<Vec<_>>();
5292        let next_call = if row.next_call.capability == NavigationNextCapability::Relations {
5293            NavigationNextCall {
5294                capability: NavigationNextCapability::Summary,
5295                path: row.path.clone(),
5296            }
5297        } else {
5298            row.next_call.clone()
5299        };
5300        McpCompactBriefCandidate {
5301            path: row.path.clone(),
5302            purpose_status: (!row.purpose_agent_reviewed).then_some(row.purpose_status),
5303            purpose_source: (!row.purpose_agent_reviewed).then_some(row.purpose_source),
5304            purpose_agent_reviewed: row.purpose_agent_reviewed,
5305            purpose: row.purpose.clone(),
5306            connections_truncated: row.connections_truncated
5307                || connections.len() < row.connections.len(),
5308            connections,
5309            next_call,
5310        }
5311    }
5312
5313    /// Prefer a resolved non-import edge as the single default startup connection sample.
5314    fn brief_connection_is_crisp(connection: &RankedConnection) -> bool {
5315        connection.kind != RankedConnectionKind::Import
5316            && !matches!(
5317                &connection.target,
5318                RankedConnectionTarget::Unresolved { .. }
5319            )
5320    }
5321
5322    /// Project an actionable handoff into one exact follow-up call instead of duplicating rows.
5323    fn compact_brief_purpose_handoff(
5324        handoff: &PurposeCuratorHandoff,
5325        project_path: Option<String>,
5326        worktree: Option<String>,
5327    ) -> McpCompactBriefPurposeHandoff {
5328        McpCompactBriefPurposeHandoff {
5329            agent_harness_expected: handoff.agent_harness_expected,
5330            recommended_subagent_reasoning: handoff.recommended_subagent_reasoning,
5331            instructions: handoff.instructions.first().cloned().into_iter().collect(),
5332            main_agent_fallback: handoff.main_agent_fallback,
5333            server_started_curator: handoff.server_started_curator,
5334            silent_on_success: handoff.silent_on_success,
5335            truncated: handoff.queue.truncated,
5336            next_call: McpCompactBriefRecommendation {
5337                kind: McpBriefRecommendationKind::PurposeQueue,
5338                target: MCP_TOOL_ATLAS_PURPOSE_QUEUE.to_string(),
5339                reason: MCP_BRIEF_REASON_PURPOSE_QUEUE.to_string(),
5340                arguments: Self::brief_call_arguments(
5341                    project_path,
5342                    worktree,
5343                    &[(MCP_BRIEF_ARG_TASK, &handoff.queue.task)],
5344                    Some((MCP_BRIEF_ARG_LIMIT, handoff.queue.limit)),
5345                ),
5346            },
5347        }
5348    }
5349
5350    /// Add compact opt-ins to one legacy recommendation without changing its source report.
5351    fn compact_brief_recommendation(
5352        recommendation: &McpBriefRecommendation,
5353    ) -> McpCompactBriefRecommendation {
5354        let relation_to_summary =
5355            matches!(recommendation.kind, McpBriefRecommendationKind::Relations);
5356        let mut arguments = recommendation.arguments.clone();
5357        if let Some(object) = arguments.as_object_mut() {
5358            if relation_to_summary {
5359                object.remove(MCP_BRIEF_ARG_VIEW);
5360            }
5361            if relation_to_summary
5362                || matches!(recommendation.kind, McpBriefRecommendationKind::Summary)
5363            {
5364                object.insert(
5365                    MCP_BRIEF_ARG_COMPACT.to_string(),
5366                    serde_json::Value::Bool(true),
5367                );
5368            }
5369        }
5370        McpCompactBriefRecommendation {
5371            kind: if relation_to_summary {
5372                McpBriefRecommendationKind::Summary
5373            } else {
5374                recommendation.kind
5375            },
5376            target: if relation_to_summary {
5377                MCP_TOOL_ATLAS_FILE_SUMMARY.to_string()
5378            } else {
5379                recommendation.target.clone()
5380            },
5381            reason: if relation_to_summary {
5382                MCP_BRIEF_REASON_RANKED_FILE_SUMMARY.to_string()
5383            } else {
5384                recommendation.reason.clone()
5385            },
5386            arguments,
5387        }
5388    }
5389
5390    /// Build bounded health blockers for a session brief.
5391    fn brief_blockers(
5392        store: &AtlasStore,
5393        blocker_limit: usize,
5394    ) -> Result<McpBriefBlockers, CliError> {
5395        let query = HealthQuery {
5396            start_index: 0,
5397            limit: blocker_limit,
5398            category: None,
5399            severity: None,
5400            path_prefix: None,
5401            summary_only: false,
5402            scope: HealthScope::all(),
5403        };
5404        let page = store.unresolved_health_findings_page_current(&query)?;
5405        let total = page.total;
5406        let returned = page.returned;
5407        Ok(McpBriefBlockers {
5408            total,
5409            returned,
5410            truncated: returned < total,
5411            items: page
5412                .findings
5413                .into_iter()
5414                .map(|finding| McpBriefBlocker {
5415                    id: finding.id,
5416                    severity: finding.severity,
5417                    category: finding.category,
5418                    path: finding.path,
5419                    related_path: finding.related_path,
5420                    message: finding.message,
5421                    recommendation: finding.recommendation,
5422                })
5423                .collect(),
5424        })
5425    }
5426
5427    /// Recommend next calls for a missing index.
5428    fn missing_index_recommendations(
5429        project_path: Option<String>,
5430        worktree: Option<String>,
5431    ) -> Vec<McpBriefRecommendation> {
5432        vec![
5433            McpBriefRecommendation {
5434                kind: McpBriefRecommendationKind::Init,
5435                target: MCP_TOOL_ATLAS_INIT.to_string(),
5436                reason: MCP_BRIEF_REASON_SELECTED_INDEX_MISSING.to_string(),
5437                arguments: Self::target_arguments(project_path.clone(), worktree.clone()),
5438            },
5439            McpBriefRecommendation {
5440                kind: McpBriefRecommendationKind::FilesystemTools,
5441                target: MCP_BRIEF_TARGET_FILESYSTEM_TOOLS.to_string(),
5442                reason: MCP_BRIEF_REASON_FILESYSTEM_UNTIL_INDEX.to_string(),
5443                arguments: Self::target_arguments(project_path, worktree),
5444            },
5445        ]
5446    }
5447
5448    /// Recommend next calls for an indexed project.
5449    fn indexed_project_recommendations(
5450        query: &str,
5451        next_navigation_call: Option<NavigationNextCall>,
5452        blocker_total: usize,
5453        blocker_limit: usize,
5454        project_path: Option<String>,
5455        worktree: Option<String>,
5456    ) -> Vec<McpBriefRecommendation> {
5457        let mut recommendations = match next_navigation_call {
5458            Some(next_call) if next_call.capability == NavigationNextCapability::Summary => {
5459                vec![McpBriefRecommendation {
5460                    kind: McpBriefRecommendationKind::Summary,
5461                    target: MCP_TOOL_ATLAS_FILE_SUMMARY.to_string(),
5462                    reason: MCP_BRIEF_REASON_RANKED_FILE_SUMMARY.to_string(),
5463                    arguments: Self::brief_call_arguments(
5464                        project_path.clone(),
5465                        worktree.clone(),
5466                        &[(MCP_BRIEF_ARG_FILE, &next_call.path)],
5467                        None,
5468                    ),
5469                }]
5470            }
5471            Some(next_call) if next_call.capability == NavigationNextCapability::Relations => {
5472                vec![McpBriefRecommendation {
5473                    kind: McpBriefRecommendationKind::Relations,
5474                    target: MCP_TOOL_ATLAS_SYMBOL_RELATIONS.to_string(),
5475                    reason: MCP_BRIEF_REASON_RANKED_FILE_RELATIONS.to_string(),
5476                    arguments: Self::brief_call_arguments(
5477                        project_path.clone(),
5478                        worktree.clone(),
5479                        &[
5480                            (MCP_BRIEF_ARG_FILE, &next_call.path),
5481                            (MCP_BRIEF_ARG_VIEW, MCP_SYMBOL_RELATION_VIEW_DETAILED),
5482                        ],
5483                        None,
5484                    ),
5485                }]
5486            }
5487            _ if !query.trim().is_empty() => vec![McpBriefRecommendation {
5488                kind: McpBriefRecommendationKind::Search,
5489                target: MCP_TOOL_ATLAS_SEARCH.to_string(),
5490                reason: MCP_BRIEF_REASON_SEARCH_FALLBACK.to_string(),
5491                arguments: Self::brief_call_arguments(
5492                    project_path.clone(),
5493                    worktree.clone(),
5494                    &[(MCP_BRIEF_ARG_PATTERN, query)],
5495                    None,
5496                ),
5497            }],
5498            _ => vec![McpBriefRecommendation {
5499                kind: McpBriefRecommendationKind::FilesystemTools,
5500                target: MCP_BRIEF_TARGET_FILESYSTEM_TOOLS.to_string(),
5501                reason: MCP_BRIEF_REASON_NO_FILE_CANDIDATE.to_string(),
5502                arguments: Self::target_arguments(project_path.clone(), worktree.clone()),
5503            }],
5504        };
5505        if blocker_total > 0 {
5506            recommendations.push(McpBriefRecommendation {
5507                kind: McpBriefRecommendationKind::Health,
5508                target: MCP_TOOL_ATLAS_HEALTH.to_string(),
5509                reason: MCP_BRIEF_REASON_HEALTH_BLOCKERS.to_string(),
5510                arguments: Self::brief_call_arguments(
5511                    project_path,
5512                    worktree,
5513                    &[],
5514                    Some((MCP_BRIEF_ARG_LIMIT, blocker_limit)),
5515                ),
5516            });
5517        }
5518        recommendations
5519    }
5520
5521    /// Build a `JSON` object containing one mutually exclusive root selector.
5522    fn target_arguments(
5523        project_path: Option<String>,
5524        worktree: Option<String>,
5525    ) -> serde_json::Value {
5526        let mut arguments = serde_json::Map::new();
5527        if let Some(path) = project_path {
5528            arguments.insert(
5529                MCP_BRIEF_ARG_PROJECT_PATH.to_string(),
5530                serde_json::Value::String(path),
5531            );
5532        } else if let Some(alias) = worktree {
5533            arguments.insert(
5534                MCP_BRIEF_ARG_WORKTREE.to_string(),
5535                serde_json::Value::String(alias),
5536            );
5537        }
5538        serde_json::Value::Object(arguments)
5539    }
5540
5541    /// Build recommendation call arguments with optional project path and one payload argument.
5542    fn brief_call_arguments(
5543        project_path: Option<String>,
5544        worktree: Option<String>,
5545        string_args: &[(&'static str, &str)],
5546        usize_arg: Option<(&'static str, usize)>,
5547    ) -> serde_json::Value {
5548        let mut arguments = serde_json::Map::new();
5549        if let Some(path) = project_path {
5550            arguments.insert(
5551                MCP_BRIEF_ARG_PROJECT_PATH.to_string(),
5552                serde_json::Value::String(path),
5553            );
5554        } else if let Some(alias) = worktree {
5555            arguments.insert(
5556                MCP_BRIEF_ARG_WORKTREE.to_string(),
5557                serde_json::Value::String(alias),
5558            );
5559        }
5560        for (key, value) in string_args {
5561            arguments.insert(
5562                (*key).to_string(),
5563                serde_json::Value::String((*value).to_string()),
5564            );
5565        }
5566        if let Some((key, value)) = usize_arg {
5567            arguments.insert(key.to_string(), serde_json::json!(value));
5568        }
5569        serde_json::Value::Object(arguments)
5570    }
5571
5572    /// Return all task model states for contract discovery.
5573    fn task_state_values() -> Vec<McpTaskState> {
5574        vec![
5575            McpTaskState::Pending,
5576            McpTaskState::Running,
5577            McpTaskState::Complete,
5578            McpTaskState::Failed,
5579            McpTaskState::Canceled,
5580        ]
5581    }
5582
5583    /// Return all task operation values for contract discovery.
5584    fn task_operation_values() -> Vec<McpTaskOperation> {
5585        vec![
5586            McpTaskOperation::Contract,
5587            McpTaskOperation::Scan,
5588            McpTaskOperation::WatchOnce,
5589            McpTaskOperation::SymbolsBuild,
5590            McpTaskOperation::Search,
5591        ]
5592    }
5593
5594    /// Start one bounded session-local indexing task.
5595    fn start_index_task<F>(
5596        &self,
5597        operation: McpTaskOperation,
5598        options: SymbolBuildOptions,
5599        result_ref: &'static str,
5600        work: F,
5601    ) -> Result<McpTaskStartResponse, CliError>
5602    where
5603        F: FnOnce(&IndexWorkControl, SymbolBuildOptions) -> Result<(), CliError> + Send + 'static,
5604    {
5605        let options = options.with_worker_ceiling(self.background_resources.workers_per_task);
5606        let control = index_work_control(&options);
5607        let mut task_id = MCP_INDEX_TASK_ID_PREFIX.to_string();
5608        task_id.push_str(
5609            &self
5610                .next_task_sequence
5611                .fetch_add(1, Ordering::Relaxed)
5612                .to_string(),
5613        );
5614        let now = mcp_unix_time_ms();
5615        {
5616            let mut registry = self
5617                .task_registry
5618                .write()
5619                .map_err(|_poisoned| CliError::Mcp(MCP_PROJECT_STATE_LOCK_POISONED.to_string()))?;
5620            let active = registry.active_count();
5621            if active >= self.background_resources.task_limit {
5622                let mut message = MCP_INDEX_TASK_LIMIT_PREFIX.to_string();
5623                message.push_str(&self.background_resources.task_limit.to_string());
5624                message.push_str(MCP_INDEX_TASK_LIMIT_SUFFIX);
5625                return Err(CliError::Mcp(message));
5626            }
5627            registry.insert(McpTaskRecord {
5628                task_id: task_id.clone(),
5629                operation: operation.clone(),
5630                state: McpTaskState::Pending,
5631                created_at_ms: now,
5632                updated_at_ms: now,
5633                progress: Some(McpTaskProgress {
5634                    current: None,
5635                    total: None,
5636                    message: Some(MCP_TASK_PROGRESS_ACCEPTED.to_string()),
5637                }),
5638                error: None,
5639                result_ref: None,
5640                cancelable: true,
5641                control: Some(control.clone()),
5642            });
5643        }
5644
5645        let registry = Arc::clone(&self.task_registry);
5646        let worker_task_id = task_id.clone();
5647        let mut worker_name = MCP_INDEX_WORKER_NAME_PREFIX.to_string();
5648        worker_name.push_str(&task_id);
5649        let spawn_result = thread::Builder::new().name(worker_name).spawn(move || {
5650            if let Ok(mut registry) = registry.write() {
5651                registry.update(&worker_task_id, |record| {
5652                    record.state = McpTaskState::Running;
5653                    record.updated_at_ms = mcp_unix_time_ms();
5654                    record.progress = Some(McpTaskProgress {
5655                        current: None,
5656                        total: None,
5657                        message: Some(MCP_TASK_PROGRESS_RUNNING.to_string()),
5658                    });
5659                });
5660            }
5661            let outcome =
5662                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| work(&control, options)));
5663            let (state, progress, error, completed_result_ref) = match outcome {
5664                Ok(Ok(())) => (
5665                    McpTaskState::Complete,
5666                    MCP_TASK_PROGRESS_COMPLETE,
5667                    None,
5668                    Some(result_ref.to_string()),
5669                ),
5670                Ok(Err(error)) => {
5671                    let state = if task_error_is_canceled(&error) {
5672                        McpTaskState::Canceled
5673                    } else {
5674                        McpTaskState::Failed
5675                    };
5676                    (
5677                        state,
5678                        if state == McpTaskState::Canceled {
5679                            MCP_TASK_PROGRESS_CANCELED
5680                        } else {
5681                            MCP_TASK_PROGRESS_FAILED
5682                        },
5683                        Some(bounded_task_error(&error)),
5684                        None,
5685                    )
5686                }
5687                Err(_panic) => (
5688                    McpTaskState::Failed,
5689                    MCP_TASK_PROGRESS_FAILED,
5690                    Some(MCP_INDEX_WORKER_PANIC_ERROR.to_string()),
5691                    None,
5692                ),
5693            };
5694            if let Ok(mut registry) = registry.write() {
5695                registry.update(&worker_task_id, |record| {
5696                    record.state = state;
5697                    record.updated_at_ms = mcp_unix_time_ms();
5698                    record.progress = Some(McpTaskProgress {
5699                        current: None,
5700                        total: None,
5701                        message: Some(progress.to_string()),
5702                    });
5703                    record.error = error;
5704                    record.result_ref = completed_result_ref;
5705                    record.cancelable = false;
5706                    record.control = None;
5707                });
5708            }
5709        });
5710        if let Err(source) = spawn_result {
5711            let mut spawn_error = MCP_INDEX_WORKER_SPAWN_ERROR_PREFIX.to_string();
5712            spawn_error.push_str(&source.to_string());
5713            if let Ok(mut registry) = self.task_registry.write() {
5714                registry.update(&task_id, |record| {
5715                    record.state = McpTaskState::Failed;
5716                    record.updated_at_ms = mcp_unix_time_ms();
5717                    record.progress = Some(McpTaskProgress {
5718                        current: None,
5719                        total: None,
5720                        message: Some(MCP_TASK_PROGRESS_FAILED.to_string()),
5721                    });
5722                    record.error = Some(spawn_error.clone());
5723                    record.cancelable = false;
5724                    record.control = None;
5725                });
5726            }
5727            return Err(CliError::Mcp(spawn_error));
5728        }
5729
5730        Ok(McpTaskStartResponse {
5731            task_id,
5732            operation,
5733            state: McpTaskState::Pending,
5734            status_tool: MCP_TOOL_ATLAS_TASK_STATUS,
5735            cancel_tool: MCP_TOOL_ATLAS_TASK_CANCEL,
5736        })
5737    }
5738
5739    /// Look up one MCP task status.
5740    fn task_status(&self, task_id: String) -> Result<McpTaskStatusResponse, CliError> {
5741        let registry = self
5742            .task_registry
5743            .read()
5744            .map_err(|_poisoned| CliError::Mcp(MCP_TASK_REGISTRY_LOCK_POISONED.to_string()))?;
5745        let task = registry.get(&task_id);
5746        Ok(McpTaskStatusResponse {
5747            task_id,
5748            lookup: if task.is_some() {
5749                McpTaskLookupStatus::Found
5750            } else {
5751                McpTaskLookupStatus::NotFound
5752            },
5753            states: Self::task_state_values(),
5754            operations: Self::task_operation_values(),
5755            registry_capacity: MCP_TASK_REGISTRY_CAPACITY,
5756            task,
5757        })
5758    }
5759
5760    /// Cancel one MCP task when cancellation is supported.
5761    fn task_cancel(&self, task_id: String) -> Result<McpTaskCancelResponse, CliError> {
5762        let mut registry = self
5763            .task_registry
5764            .write()
5765            .map_err(|_poisoned| CliError::Mcp(MCP_TASK_REGISTRY_LOCK_POISONED.to_string()))?;
5766        let Some(record) = registry.get(&task_id) else {
5767            return Ok(McpTaskCancelResponse {
5768                task_id,
5769                result: McpTaskCancelResult::NotFound,
5770                registry_capacity: MCP_TASK_REGISTRY_CAPACITY,
5771                task: None,
5772            });
5773        };
5774        if matches!(
5775            record.state,
5776            McpTaskState::Complete | McpTaskState::Failed | McpTaskState::Canceled
5777        ) {
5778            return Ok(McpTaskCancelResponse {
5779                task_id,
5780                result: McpTaskCancelResult::AlreadyFinished,
5781                registry_capacity: MCP_TASK_REGISTRY_CAPACITY,
5782                task: Some(record),
5783            });
5784        }
5785        if !record.cancelable {
5786            return Ok(McpTaskCancelResponse {
5787                task_id,
5788                result: McpTaskCancelResult::NotCancelable,
5789                registry_capacity: MCP_TASK_REGISTRY_CAPACITY,
5790                task: Some(record),
5791            });
5792        }
5793        let task = registry.update(&task_id, |record| {
5794            if let Some(control) = &record.control {
5795                control.cancel();
5796            }
5797            record.updated_at_ms = mcp_unix_time_ms();
5798            record.progress = Some(McpTaskProgress {
5799                current: None,
5800                total: None,
5801                message: Some(MCP_TASK_PROGRESS_CANCELLATION_REQUESTED.to_string()),
5802            });
5803            record.cancelable = false;
5804        });
5805        Ok(McpTaskCancelResponse {
5806            task_id,
5807            result: McpTaskCancelResult::CancellationRequested,
5808            registry_capacity: MCP_TASK_REGISTRY_CAPACITY,
5809            task,
5810        })
5811    }
5812
5813    /// Build a CLI-compatible lint report for MCP callers.
5814    fn lint_report_for_state(
5815        state: &McpProjectState,
5816        params: &AtlasLintParams,
5817    ) -> Result<crate::runtime::LintReport, CliError> {
5818        let config = Self::load_config_for_state(state)?;
5819        let purpose_level = Self::parse_purpose_lint_level(params.purpose_level.as_deref())?;
5820        lint_project(
5821            &config,
5822            &state.db_path,
5823            state.config_path.as_deref(),
5824            LintOptions {
5825                strict_folders: params.strict_folders.unwrap_or(false),
5826                report_untracked: params.report_untracked.unwrap_or(false),
5827                strict_untracked: params.strict_untracked.unwrap_or(false),
5828            },
5829            purpose_level,
5830        )
5831    }
5832
5833    /// Build startup state from CLI-supplied DB/config paths.
5834    fn startup_project_state(db_path: PathBuf, config_path: Option<PathBuf>) -> McpProjectState {
5835        let root = Self::startup_project_root(&db_path, config_path.as_deref());
5836        let config_path = config_path.filter(|path| Self::config_matches_project_root(&root, path));
5837        McpProjectState {
5838            root,
5839            db_path,
5840            config_path,
5841            worktree: None,
5842        }
5843    }
5844
5845    /// Resolve startup root best-effort so server construction stays infallible.
5846    fn startup_project_root(db_path: &Path, config_path: Option<&Path>) -> PathBuf {
5847        if let Ok(root) = default_mcp_project_root(db_path, config_path) {
5848            return root;
5849        }
5850        if let Some(root) = Self::project_root_from_default_db_path(db_path) {
5851            return root;
5852        }
5853        std::env::current_dir()
5854            .ok()
5855            .and_then(|root| canonical_project_root(&root).ok())
5856            .unwrap_or_else(|| PathBuf::from(CURRENT_DIR_ALIAS))
5857    }
5858
5859    /// Infer a root from a conventional `root/.projectatlas/projectatlas.db` path.
5860    fn project_root_from_default_db_path(db_path: &Path) -> Option<PathBuf> {
5861        let atlas_dir = db_path.parent()?;
5862        if atlas_dir.file_name()? != PROJECTATLAS_DIR_NAME {
5863            return None;
5864        }
5865        let root = atlas_dir.parent()?;
5866        canonical_project_root(root)
5867            .ok()
5868            .or_else(|| Some(root.to_path_buf()))
5869    }
5870
5871    /// Read the active MCP project state.
5872    fn active_project_state(&self) -> Result<McpProjectState, CliError> {
5873        let state = self
5874            .project_state
5875            .read()
5876            .map(|state| state.clone())
5877            .map_err(|_poisoned| CliError::Mcp(MCP_PROJECT_STATE_LOCK_POISONED.to_string()))?;
5878        canonical_source_project_root(&state.root)?;
5879        Ok(state)
5880    }
5881
5882    /// Replace the active MCP project state.
5883    fn set_active_project_state(&self, state: McpProjectState) -> Result<(), CliError> {
5884        *self
5885            .project_state
5886            .write()
5887            .map_err(|_poisoned| CliError::Mcp(MCP_PROJECT_STATE_LOCK_POISONED.to_string()))? =
5888            state;
5889        Ok(())
5890    }
5891
5892    /// Return the bounded reciprocal Git inventory for the immutable control checkout.
5893    fn control_git_repository(&self) -> Result<GitRepositoryStructure, CliError> {
5894        self.control_git_repository_if_present()?.ok_or_else(|| {
5895            CliError::InvalidInput(MCP_ERROR_WORKTREE_CONTROL_REPOSITORY_REQUIRED.to_string())
5896        })
5897    }
5898
5899    /// Return the control Git inventory, or none for a true non-Git control root.
5900    fn control_git_repository_if_present(
5901        &self,
5902    ) -> Result<Option<GitRepositoryStructure>, CliError> {
5903        let repository = match discover_repository_structure(&self.control_state.root)? {
5904            RepositoryStructure::Git(repository) => repository,
5905            RepositoryStructure::NonGit { .. } => return Ok(None),
5906            RepositoryStructure::InvalidGit { issue, .. } => {
5907                return Err(CliError::InvalidInput(format!(
5908                    "invalid control-repository Git evidence at '{}': {:?}",
5909                    normalize_native_path_display(issue.path),
5910                    issue.kind
5911                )));
5912            }
5913        };
5914        let control_present = repository.worktrees.iter().any(|entry| {
5915            matches!(
5916                &entry.state,
5917                GitWorktreeState::Active { root, .. } if root == &self.control_state.root
5918            )
5919        });
5920        if !control_present {
5921            return Err(CliError::InvalidInput(format!(
5922                "selected control root '{}' is not one active worktree in its reciprocal Git inventory",
5923                normalize_native_path_display(&self.control_state.root)
5924            )));
5925        }
5926        Ok(Some(repository))
5927    }
5928
5929    /// Derive one stable, short candidate selector from Git administrative identity.
5930    fn worktree_candidate_selector(entry: &GitWorktreeEntry) -> String {
5931        Self::worktree_candidate_selector_from_path(&entry.administrative_directory)
5932    }
5933
5934    /// Derive one stable selector from a native administrative-directory path.
5935    fn worktree_candidate_selector_from_path(identity: &Path) -> String {
5936        if let Some(identity) = identity.to_str() {
5937            return Self::worktree_candidate_selector_from_identity(
5938                &normalize_native_path_display_str(identity),
5939            );
5940        }
5941        Self::worktree_candidate_selector_from_native_identity(
5942            identity.as_os_str().as_encoded_bytes(),
5943        )
5944    }
5945
5946    /// Derive one stable, short candidate selector from normalized Git identity text.
5947    fn worktree_candidate_selector_from_identity(identity: &str) -> String {
5948        Self::worktree_candidate_selector_from_native_identity(identity.as_bytes())
5949    }
5950
5951    /// Derive a selector from exact native administrative-directory bytes.
5952    fn worktree_candidate_selector_from_native_identity(identity: &[u8]) -> String {
5953        let digest = blake3::hash(identity).to_hex();
5954        let mut selector = String::with_capacity(
5955            MCP_WORKTREE_SELECTOR_PREFIX.len() + MCP_WORKTREE_SELECTOR_DIGEST_CHARS,
5956        );
5957        selector.push_str(MCP_WORKTREE_SELECTOR_PREFIX);
5958        selector.push_str(&digest.as_str()[..MCP_WORKTREE_SELECTOR_DIGEST_CHARS]);
5959        selector
5960    }
5961
5962    /// Derive the same selector from a persisted native identity that a live
5963    /// Git entry would expose, retaining the exact bytes when UTF-8 display is
5964    /// unavailable.
5965    fn worktree_candidate_selector_from_canonical_identity(
5966        identity: &CanonicalProjectRoot,
5967    ) -> String {
5968        Self::worktree_candidate_selector_from_path(identity.as_path())
5969    }
5970
5971    /// Borrow one active source root from structural worktree evidence.
5972    fn active_worktree_root(entry: &GitWorktreeEntry) -> Option<&Path> {
5973        match &entry.state {
5974            GitWorktreeState::Active { root, .. } => Some(root),
5975            GitWorktreeState::Missing { .. } | GitWorktreeState::Invalid { .. } => None,
5976        }
5977    }
5978
5979    /// Return whether one short selector identifies this structural candidate.
5980    fn worktree_candidate_matches(entry: &GitWorktreeEntry, selector: &str) -> bool {
5981        if Self::worktree_candidate_selector(entry) == selector {
5982            return true;
5983        }
5984        let root_name = Self::active_worktree_root(entry)
5985            .and_then(Path::file_name)
5986            .and_then(std::ffi::OsStr::to_str);
5987        let administrative_name = entry
5988            .administrative_directory
5989            .file_name()
5990            .and_then(std::ffi::OsStr::to_str);
5991        root_name.is_some_and(|name| name.eq_ignore_ascii_case(selector))
5992            || administrative_name.is_some_and(|name| name.eq_ignore_ascii_case(selector))
5993    }
5994
5995    /// Return bounded active non-control candidates for one stable or human selector.
5996    fn matching_worktree_candidates<'a>(
5997        &'a self,
5998        repository: &'a GitRepositoryStructure,
5999        selector: &str,
6000    ) -> Vec<&'a GitWorktreeEntry> {
6001        repository
6002            .worktrees
6003            .iter()
6004            .filter(|entry| {
6005                Self::active_worktree_root(entry)
6006                    .is_some_and(|root| root != self.control_state.root)
6007            })
6008            .filter(|entry| Self::worktree_candidate_matches(entry, selector))
6009            .collect()
6010    }
6011
6012    /// Revalidate one selected candidate and its filesystem lifecycle before registration.
6013    fn revalidate_worktree_candidate(
6014        &self,
6015        expected_repository: &GitRepositoryStructure,
6016        expected_entry: &GitWorktreeEntry,
6017        expected_identity: &str,
6018    ) -> Result<(GitRepositoryStructure, GitWorktreeEntry), CliError> {
6019        let expected_root = Self::active_worktree_root(expected_entry).ok_or_else(|| {
6020            CliError::InvalidInput(MCP_ERROR_WORKTREE_NO_LONGER_ACTIVE.to_string())
6021        })?;
6022        let repository = self.control_git_repository()?;
6023        let entry = repository
6024            .worktrees
6025            .iter()
6026            .find(|entry| entry.administrative_directory == expected_entry.administrative_directory)
6027            .cloned()
6028            .ok_or_else(|| {
6029                CliError::InvalidInput(MCP_ERROR_WORKTREE_LIFECYCLE_CHANGED.to_string())
6030            })?;
6031        let root = Self::active_worktree_root(&entry).ok_or_else(|| {
6032            CliError::InvalidInput(MCP_ERROR_WORKTREE_LIFECYCLE_CHANGED.to_string())
6033        })?;
6034        let identity = git_administrative_identity(&entry.administrative_directory)?;
6035        if repository.common_directory != expected_repository.common_directory
6036            || entry.role != expected_entry.role
6037            || root != expected_root
6038            || identity != expected_identity
6039        {
6040            return Err(CliError::InvalidInput(
6041                MCP_ERROR_WORKTREE_LIFECYCLE_CHANGED.to_string(),
6042            ));
6043        }
6044        Ok((repository, entry))
6045    }
6046
6047    /// Build one concise add-candidate row from active structural evidence.
6048    fn worktree_candidate(
6049        common_directory: &Path,
6050        entry: &GitWorktreeEntry,
6051    ) -> Option<McpWorktreeCandidate> {
6052        let root = Self::active_worktree_root(entry)?;
6053        Some(McpWorktreeCandidate {
6054            selector: Self::worktree_candidate_selector(entry),
6055            path_display: if common_directory.to_str().is_some()
6056                && entry.administrative_directory.to_str().is_some()
6057                && root.to_str().is_some()
6058            {
6059                McpWorktreePathDisplayState::Available
6060            } else {
6061                McpWorktreePathDisplayState::Unavailable
6062            },
6063            root: lossless_project_root_display(root),
6064            role: entry.role.into(),
6065        })
6066    }
6067
6068    /// Derive a valid default alias from one selected active worktree root.
6069    fn default_worktree_alias(root: &Path) -> Result<WorktreeAlias, CliError> {
6070        let name = root
6071            .file_name()
6072            .and_then(std::ffi::OsStr::to_str)
6073            .ok_or_else(|| CliError::InvalidInput(MCP_ERROR_WORKTREE_PATH_NON_UTF8.to_string()))?;
6074        WorktreeAlias::parse(&name.to_ascii_lowercase()).map_err(|source| {
6075            CliError::InvalidInput(format!(
6076                "selected worktree directory cannot be used as an alias; provide alias explicitly: {source}"
6077            ))
6078        })
6079    }
6080
6081    /// Open one exact local worktree atlas without creating or migrating it.
6082    fn open_local_worktree_atlas(root: &Path) -> Result<Option<AtlasStore>, CliError> {
6083        let db_path = Self::projectatlas_db_path(root);
6084        if !db_path.exists() {
6085            return Ok(None);
6086        }
6087        Ok(Some(open_atlas_store_read_only_for_project(
6088            &db_path, root,
6089        )?))
6090    }
6091
6092    /// Require the exact project identity from one already-open local atlas.
6093    fn local_worktree_project_instance_id(
6094        store: &AtlasStore,
6095        db_path: &Path,
6096    ) -> Result<ProjectInstanceId, CliError> {
6097        let project_instance_id = store.project_instance_id()?.ok_or_else(|| {
6098            CliError::InvalidInput(format!(
6099                "worktree atlas '{}' has no exact project identity",
6100                normalize_native_path_display(db_path)
6101            ))
6102        })?;
6103        Ok(project_instance_id)
6104    }
6105
6106    /// Export one local usage snapshot bound to an independently read project identity.
6107    fn local_worktree_usage_snapshot(
6108        store: &AtlasStore,
6109        db_path: &Path,
6110        project_instance_id: ProjectInstanceId,
6111    ) -> Result<WorktreeUsageSnapshot, CliError> {
6112        let snapshot = store.export_worktree_usage_snapshot()?;
6113        if snapshot.project_instance_id() != project_instance_id {
6114            return Err(CliError::InvalidInput(format!(
6115                "worktree atlas '{}' telemetry identity does not match its project identity",
6116                normalize_native_path_display(db_path)
6117            )));
6118        }
6119        Ok(snapshot)
6120    }
6121
6122    /// Read one exact local worktree atlas and its bounded telemetry snapshot without migration.
6123    fn local_worktree_atlas(root: &Path) -> Result<Option<LocalWorktreeAtlas>, CliError> {
6124        let db_path = Self::projectatlas_db_path(root);
6125        let Some(store) = Self::open_local_worktree_atlas(root)? else {
6126            return Ok(None);
6127        };
6128        let project_instance_id = Self::local_worktree_project_instance_id(&store, &db_path)?;
6129        let snapshot = Self::local_worktree_usage_snapshot(&store, &db_path, project_instance_id)?;
6130        Ok(Some(LocalWorktreeAtlas {
6131            project_instance_id,
6132            snapshot,
6133        }))
6134    }
6135
6136    /// Reopen an identity-bound local atlas before its registration is committed.
6137    fn revalidate_local_worktree_atlas_identity(
6138        root: &Path,
6139        expected: Option<ProjectInstanceId>,
6140    ) -> Result<(), CliError> {
6141        let Some(expected) = expected else {
6142            return Ok(());
6143        };
6144        let db_path = Self::projectatlas_db_path(root);
6145        let Some(store) = Self::open_local_worktree_atlas(root)? else {
6146            return Err(CliError::InvalidInput(
6147                MCP_ERROR_WORKTREE_IDENTITY_CONFLICT.to_string(),
6148            ));
6149        };
6150        if Self::local_worktree_project_instance_id(&store, &db_path)? != expected {
6151            return Err(CliError::InvalidInput(
6152                MCP_ERROR_WORKTREE_IDENTITY_CONFLICT.to_string(),
6153            ));
6154        }
6155        Ok(())
6156    }
6157
6158    /// Finalize one retirement under control-first writer exclusion.
6159    fn retire_registered_worktree(
6160        control: &AtlasStore,
6161        registration: &WorktreeRegistration,
6162        root: Option<&Path>,
6163        retired_at_epoch: u64,
6164        initial_blocker: Option<String>,
6165    ) -> Result<
6166        (
6167            WorktreeRegistration,
6168            Option<WorktreeUsageSyncState>,
6169            Option<String>,
6170        ),
6171        CliError,
6172    > {
6173        Self::retire_registered_worktree_with_pre_open(
6174            control,
6175            registration,
6176            root,
6177            retired_at_epoch,
6178            initial_blocker,
6179            || Ok(()),
6180        )
6181    }
6182
6183    /// Finalize retirement with one deterministic seam before the local atlas is opened.
6184    fn retire_registered_worktree_with_pre_open<F>(
6185        control: &AtlasStore,
6186        registration: &WorktreeRegistration,
6187        root: Option<&Path>,
6188        retired_at_epoch: u64,
6189        initial_blocker: Option<String>,
6190        pre_open: F,
6191    ) -> Result<
6192        (
6193            WorktreeRegistration,
6194            Option<WorktreeUsageSyncState>,
6195            Option<String>,
6196        ),
6197        CliError,
6198    >
6199    where
6200        F: FnOnce() -> Result<(), CliError>,
6201    {
6202        control.with_active_worktree_registration(
6203            registration.registration_id,
6204            &registration.alias,
6205            |guard| {
6206                let Some(root) = root else {
6207                    let retired = guard.retire(retired_at_epoch)?;
6208                    return Ok(Ok((retired, None, initial_blocker)));
6209                };
6210                if require_registered_worktree_lifecycle(guard.registration(), root).is_err() {
6211                    return Self::retire_changed_worktree_lifecycle(guard, retired_at_epoch)
6212                        .map(Ok);
6213                }
6214                if let Err(error) = pre_open() {
6215                    return Ok(Err(error));
6216                }
6217                let local = match Self::open_local_worktree_atlas(root) {
6218                    Ok(local) => local,
6219                    Err(error) => {
6220                        return Self::classify_retirement_failure(
6221                            guard,
6222                            root,
6223                            retired_at_epoch,
6224                            error,
6225                        );
6226                    }
6227                };
6228                let Some(local) = local else {
6229                    if require_registered_worktree_lifecycle(guard.registration(), root).is_err() {
6230                        return Self::retire_changed_worktree_lifecycle(guard, retired_at_epoch)
6231                            .map(Ok);
6232                    }
6233                    if guard.registration().project_instance_id.is_some() {
6234                        return Ok(Err(CliError::InvalidInput(
6235                            MCP_ERROR_BOUND_WORKTREE_ATLAS_MISSING.to_string(),
6236                        )));
6237                    }
6238                    let retired = guard.retire(retired_at_epoch)?;
6239                    return Ok(Ok((retired, None, initial_blocker)));
6240                };
6241                let db_path = Self::projectatlas_db_path(root);
6242                let project_instance_id =
6243                    match Self::local_worktree_project_instance_id(&local, &db_path) {
6244                        Ok(project_instance_id) => project_instance_id,
6245                        Err(error) => {
6246                            return Self::classify_retirement_failure(
6247                                guard,
6248                                root,
6249                                retired_at_epoch,
6250                                error,
6251                            );
6252                        }
6253                    };
6254                let finalization = match local.with_exclusive_worktree_usage_snapshot(|snapshot| {
6255                    if require_registered_worktree_lifecycle(guard.registration(), root).is_err() {
6256                        return Ok(None);
6257                    }
6258                    guard
6259                        .retire_with_usage_snapshot(
6260                            root,
6261                            project_instance_id,
6262                            snapshot,
6263                            retired_at_epoch,
6264                        )
6265                        .map(Some)
6266                }) {
6267                    Ok(finalization) => finalization,
6268                    Err(error) => {
6269                        return Self::classify_retirement_failure(
6270                            guard,
6271                            root,
6272                            retired_at_epoch,
6273                            error.into(),
6274                        );
6275                    }
6276                };
6277                if let Some((retired, synchronized)) = finalization {
6278                    return Ok(Ok((retired, Some(synchronized), initial_blocker)));
6279                }
6280                Self::retire_changed_worktree_lifecycle(guard, retired_at_epoch).map(Ok)
6281            },
6282        )?
6283    }
6284
6285    /// Preserve an unchanged database error or retire a replaced Git lifecycle.
6286    fn classify_retirement_failure(
6287        guard: &mut ActiveWorktreeRegistrationGuard<'_>,
6288        root: &Path,
6289        retired_at_epoch: u64,
6290        error: CliError,
6291    ) -> Result<
6292        Result<
6293            (
6294                WorktreeRegistration,
6295                Option<WorktreeUsageSyncState>,
6296                Option<String>,
6297            ),
6298            CliError,
6299        >,
6300        DbError,
6301    > {
6302        if require_registered_worktree_lifecycle(guard.registration(), root).is_err() {
6303            Self::retire_changed_worktree_lifecycle(guard, retired_at_epoch).map(Ok)
6304        } else {
6305            Ok(Err(error))
6306        }
6307    }
6308
6309    /// Retire a stale registration without binding, importing, or modifying replacement state.
6310    fn retire_changed_worktree_lifecycle(
6311        guard: &mut ActiveWorktreeRegistrationGuard<'_>,
6312        retired_at_epoch: u64,
6313    ) -> Result<
6314        (
6315            WorktreeRegistration,
6316            Option<WorktreeUsageSyncState>,
6317            Option<String>,
6318        ),
6319        DbError,
6320    > {
6321        let retired = guard.retire(retired_at_epoch)?;
6322        Ok((
6323            retired,
6324            None,
6325            Some(MCP_ERROR_WORKTREE_LIFECYCLE_CHANGED.to_string()),
6326        ))
6327    }
6328
6329    /// Reset one unbound alias while excluding concurrent bind or retirement publication.
6330    fn reset_registered_worktree_index(
6331        &self,
6332        state: &McpProjectState,
6333        selection: &McpWorktreeSelection,
6334        include_mcp_config: bool,
6335    ) -> Result<ResetIndexReport, CliError> {
6336        self.reset_registered_worktree_index_with_post_validation(
6337            state,
6338            selection,
6339            include_mcp_config,
6340            || Ok(()),
6341        )
6342    }
6343
6344    /// Reset one unbound alias with a deterministic post-validation seam.
6345    fn reset_registered_worktree_index_with_post_validation<F>(
6346        &self,
6347        state: &McpProjectState,
6348        selection: &McpWorktreeSelection,
6349        include_mcp_config: bool,
6350        post_validation: F,
6351    ) -> Result<ResetIndexReport, CliError>
6352    where
6353        F: FnOnce() -> Result<(), CliError>,
6354    {
6355        let alias = WorktreeAlias::parse(&selection.alias)?;
6356        let registration_id =
6357            selection
6358                .registration_id
6359                .ok_or_else(|| DbError::WorktreeRegistrationNotFound {
6360                    alias: selection.alias.clone(),
6361                })?;
6362        let control = Self::open_existing_mut_store(&self.control_state, &self.control_state)?;
6363        Self::require_captured_control_identity(Some(selection), &control)?;
6364        let admission = crate::runtime::preflight_managed_reset(&state.db_path, &state.root)?;
6365        let database = crate::runtime::admitted_database_path(&state.db_path, admission.as_ref());
6366        match control.with_unbound_worktree_registration(registration_id, &alias, |registration| {
6367            require_registered_worktree_lifecycle(registration, &state.root)?;
6368            post_validation()?;
6369            reset_index_files_with_revalidation(
6370                database,
6371                include_mcp_config,
6372                admission.as_ref(),
6373                || require_registered_worktree_lifecycle(registration, &state.root),
6374            )
6375        }) {
6376            Ok(result) => result,
6377            Err(DbError::WorktreeRegistrationConflict { .. }) => Err(CliError::InvalidInput(
6378                MCP_ERROR_BOUND_WORKTREE_RESET_UNSUPPORTED.to_string(),
6379            )),
6380            Err(error) => Err(error.into()),
6381        }
6382    }
6383
6384    /// Match invalid evidence by its retained path without following a replacement target.
6385    fn worktree_administrative_path_identity(
6386        entry: &GitWorktreeEntry,
6387    ) -> Option<CanonicalProjectRoot> {
6388        match &entry.state {
6389            GitWorktreeState::Invalid { .. } => {
6390                CanonicalProjectRoot::from_persisted_path(entry.administrative_directory.clone())
6391                    .ok()
6392            }
6393            _ => CanonicalProjectRoot::from_path(&entry.administrative_directory).ok(),
6394        }
6395    }
6396
6397    /// Join one structural worktree entry to registry, atlas, and telemetry state.
6398    fn worktree_list_row(
6399        &self,
6400        common_directory: &Path,
6401        entry: &GitWorktreeEntry,
6402        registrations: &[WorktreeRegistration],
6403    ) -> McpWorktreeRow {
6404        let administrative_directory =
6405            lossless_native_path_display(&entry.administrative_directory);
6406        let common_identity = CanonicalProjectRoot::from_path(common_directory).ok();
6407        let administrative_identity = Self::worktree_administrative_path_identity(entry);
6408        let registration = registrations.iter().find(|registration| {
6409            registration.state == WorktreeRegistrationState::Active
6410                && common_identity
6411                    .as_ref()
6412                    .is_some_and(|identity| *identity == registration.git_common_directory_identity)
6413                && administrative_identity.as_ref().is_some_and(|identity| {
6414                    *identity == registration.git_administrative_directory_identity
6415                })
6416        });
6417        let administrative_identity = git_administrative_identity(&entry.administrative_directory);
6418        let lifecycle_matches = registration.is_none_or(|registration| {
6419            administrative_identity
6420                .as_ref()
6421                .is_ok_and(|identity| identity == &registration.git_administrative_identity)
6422        });
6423        let root = Self::active_worktree_root(entry);
6424        let display_root = match &entry.state {
6425            GitWorktreeState::Missing { .. } => {
6426                registration.map(|registration| registration.last_root_identity.as_path())
6427            }
6428            _ => root,
6429        };
6430        let path_display = if common_directory.to_str().is_some()
6431            && entry.administrative_directory.to_str().is_some()
6432            && display_root.is_none_or(|root| root.to_str().is_some())
6433        {
6434            McpWorktreePathDisplayState::Available
6435        } else {
6436            McpWorktreePathDisplayState::Unavailable
6437        };
6438        let control = root.is_some_and(|root| root == self.control_state.root);
6439        let alias = if control {
6440            Some(MCP_MAIN_WORKTREE_ALIAS.to_string())
6441        } else {
6442            registration.map(|registration| registration.alias.to_string())
6443        };
6444        let registration_state = if control {
6445            McpWorktreeRegistrationState::Control
6446        } else if registration.is_some() {
6447            McpWorktreeRegistrationState::Registered
6448        } else {
6449            McpWorktreeRegistrationState::Unregistered
6450        };
6451        let mut atlas_state = McpWorktreeAtlasState::Unavailable;
6452        let mut telemetry_state = McpWorktreeTelemetryState::Unavailable;
6453        let mut local_telemetry_revision = None;
6454        let mut project_instance_id = None;
6455        let mut blocker = (!lifecycle_matches)
6456            .then(|| MCP_ERROR_WORKTREE_LIFECYCLE_CHANGED.to_string())
6457            .or_else(|| administrative_identity.err().map(|error| error.to_string()));
6458        if matches!(path_display, McpWorktreePathDisplayState::Unavailable) && blocker.is_none() {
6459            blocker = Some(MCP_ERROR_WORKTREE_PATH_NON_UTF8.to_string());
6460        }
6461        let git_state = match &entry.state {
6462            GitWorktreeState::Active { root, .. } => {
6463                if lifecycle_matches {
6464                    match Self::local_worktree_atlas(root) {
6465                        Ok(Some(local)) => {
6466                            let identity_matches = registration
6467                                .and_then(|registration| registration.project_instance_id)
6468                                .is_none_or(|expected| expected == local.project_instance_id);
6469                            if identity_matches {
6470                                atlas_state = McpWorktreeAtlasState::Initialized;
6471                                local_telemetry_revision = Some(local.snapshot.revision());
6472                                project_instance_id = Some(local.project_instance_id.to_string());
6473                                telemetry_state = if control {
6474                                    McpWorktreeTelemetryState::Control
6475                                } else if let Some(registration) = registration {
6476                                    if local.snapshot.revision()
6477                                        > registration.accepted_telemetry_revision
6478                                    {
6479                                        McpWorktreeTelemetryState::Pending
6480                                    } else {
6481                                        McpWorktreeTelemetryState::Current
6482                                    }
6483                                } else {
6484                                    McpWorktreeTelemetryState::Unregistered
6485                                };
6486                            } else {
6487                                atlas_state = McpWorktreeAtlasState::Invalid;
6488                                blocker = Some(MCP_ERROR_WORKTREE_IDENTITY_CONFLICT.to_string());
6489                            }
6490                        }
6491                        Ok(None) => {
6492                            atlas_state = McpWorktreeAtlasState::Missing;
6493                            telemetry_state = if registration.is_some() {
6494                                McpWorktreeTelemetryState::MissingAtlas
6495                            } else {
6496                                McpWorktreeTelemetryState::Unregistered
6497                            };
6498                        }
6499                        Err(error) => {
6500                            atlas_state = McpWorktreeAtlasState::Invalid;
6501                            blocker = Some(error.to_string());
6502                        }
6503                    }
6504                } else {
6505                    atlas_state = McpWorktreeAtlasState::Invalid;
6506                    telemetry_state = McpWorktreeTelemetryState::Unavailable;
6507                }
6508                McpGitWorktreeState::Active
6509            }
6510            GitWorktreeState::Missing { git_control_path } => {
6511                blocker = Some(format!(
6512                    "Git registration target is missing at '{}'",
6513                    normalize_native_path_display(git_control_path)
6514                ));
6515                McpGitWorktreeState::Missing
6516            }
6517            GitWorktreeState::Invalid { issue } => {
6518                blocker = Some(format!(
6519                    "invalid Git evidence at '{}': {:?}",
6520                    normalize_native_path_display(&issue.path),
6521                    issue.kind
6522                ));
6523                McpGitWorktreeState::Invalid
6524            }
6525        };
6526        McpWorktreeRow {
6527            selector: Some(Self::worktree_candidate_selector(entry)),
6528            alias,
6529            role: entry.role.into(),
6530            path_display,
6531            git_state,
6532            registration: registration_state,
6533            administrative_directory,
6534            root: display_root.and_then(lossless_project_root_display),
6535            atlas_state,
6536            telemetry_state,
6537            accepted_telemetry_revision: registration
6538                .map(|registration| registration.accepted_telemetry_revision),
6539            local_telemetry_revision,
6540            project_instance_id,
6541            blocker,
6542        }
6543    }
6544
6545    /// Preserve one active alias when Git no longer reports its worktree registration.
6546    fn missing_registered_worktree_row(registration: &WorktreeRegistration) -> McpWorktreeRow {
6547        let path_display = if registration
6548            .git_common_directory_identity
6549            .display_string()
6550            .is_ok()
6551            && registration.last_root_identity.display_string().is_ok()
6552            && registration
6553                .git_administrative_directory_identity
6554                .display_string()
6555                .is_ok()
6556        {
6557            McpWorktreePathDisplayState::Available
6558        } else {
6559            McpWorktreePathDisplayState::Unavailable
6560        };
6561        McpWorktreeRow {
6562            selector: Some(Self::worktree_candidate_selector_from_canonical_identity(
6563                &registration.git_administrative_directory_identity,
6564            )),
6565            alias: Some(registration.alias.to_string()),
6566            role: McpGitWorktreeRole::Linked,
6567            path_display,
6568            git_state: McpGitWorktreeState::Missing,
6569            registration: McpWorktreeRegistrationState::Registered,
6570            administrative_directory: registration
6571                .git_administrative_directory_identity
6572                .display_string()
6573                .ok(),
6574            root: registration.last_root_identity.display_string().ok(),
6575            atlas_state: McpWorktreeAtlasState::Unavailable,
6576            telemetry_state: McpWorktreeTelemetryState::Unavailable,
6577            accepted_telemetry_revision: Some(registration.accepted_telemetry_revision),
6578            local_telemetry_revision: None,
6579            project_instance_id: registration
6580                .project_instance_id
6581                .map(|identity| identity.to_string()),
6582            blocker: Some(MCP_WORKTREE_MISSING_RETENTION_REASON.to_string()),
6583        }
6584    }
6585
6586    /// Return the current Unix epoch seconds within the persisted `SQLite` domain.
6587    fn current_epoch_seconds() -> Result<u64, CliError> {
6588        u64::try_from(mcp_unix_time_ms().saturating_div(1_000)).map_err(|source| {
6589            CliError::InvalidInput(format!(
6590                "current Unix epoch exceeds the supported worktree registry range: {source}"
6591            ))
6592        })
6593    }
6594
6595    /// Return active/legacy state or one captured registered alias target.
6596    fn state_for_target(
6597        &self,
6598        project_path: Option<String>,
6599        worktree: Option<String>,
6600    ) -> Result<McpProjectState, CliError> {
6601        let state = self.state_for_target_with_config_validation(
6602            project_path,
6603            worktree,
6604            McpConfigValidation::Immediate,
6605        )?;
6606        Self::require_initialized_worktree_target(&state)?;
6607        Ok(state)
6608    }
6609
6610    /// Resolve an ordered alias federation into exact initialized roots and identities.
6611    fn federated_worktree_roots(
6612        &self,
6613        worktrees: &[String],
6614    ) -> Result<(Vec<PathBuf>, Vec<McpWorktreeSelection>), CliError> {
6615        validate_federated_root_count(worktrees.len()).map_err(CliError::Service)?;
6616        let mut roots = Vec::with_capacity(worktrees.len());
6617        let mut selections = Vec::with_capacity(worktrees.len());
6618        for worktree in worktrees {
6619            let state = self.state_for_target(None, Some(worktree.clone()))?;
6620            let selection = state.worktree.ok_or_else(|| {
6621                CliError::InvalidInput(MCP_ERROR_FEDERATED_ALIAS_MISSING.to_string())
6622            })?;
6623            if selections
6624                .iter()
6625                .any(|captured: &McpWorktreeSelection| captured.alias == selection.alias)
6626                || roots.contains(&state.root)
6627            {
6628                return Err(CliError::Service(ServiceError::InvalidInput(
6629                    MCP_ERROR_FEDERATED_TARGET_DUPLICATE.to_string(),
6630                )));
6631            }
6632            selections.push(selection);
6633            roots.push(state.root);
6634        }
6635        Ok((roots, selections))
6636    }
6637
6638    /// Resolve one mutually exclusive target under the selected config-validation timing.
6639    fn state_for_target_with_config_validation(
6640        &self,
6641        project_path: Option<String>,
6642        worktree: Option<String>,
6643        validation: McpConfigValidation,
6644    ) -> Result<McpProjectState, CliError> {
6645        let project_path = Self::normalized_optional_path(project_path);
6646        let worktree = worktree.map(|alias| alias.trim().to_string());
6647        if project_path.is_some() && worktree.is_some() {
6648            return Err(CliError::InvalidInput(
6649                MCP_WORKTREE_PROJECT_PATH_CONFLICT.to_string(),
6650            ));
6651        }
6652        let Some(alias) = worktree else {
6653            return self.state_for_project_path_with_config_validation(project_path, validation);
6654        };
6655        if alias.is_empty() {
6656            return Err(CliError::InvalidInput(
6657                MCP_ERROR_WORKTREE_SELECTOR_EMPTY.to_string(),
6658            ));
6659        }
6660        if alias == MCP_MAIN_WORKTREE_ALIAS {
6661            let mut state = self.control_state.clone();
6662            let project_instance_id = if state.db_path.is_file() {
6663                Self::open_read_store(&state)?.project_instance_id()?
6664            } else {
6665                None
6666            };
6667            state.worktree = Some(McpWorktreeSelection {
6668                alias,
6669                registration_id: None,
6670                project_instance_id,
6671                control_project_instance_id: project_instance_id,
6672            });
6673            return Ok(state);
6674        }
6675        let alias = WorktreeAlias::parse(&alias)?;
6676        self.resolve_registered_worktree(&alias, validation)
6677    }
6678
6679    /// Resolve one active registration through current reciprocal Git structure.
6680    fn resolve_registered_worktree(
6681        &self,
6682        alias: &WorktreeAlias,
6683        validation: McpConfigValidation,
6684    ) -> Result<McpProjectState, CliError> {
6685        let control = Self::open_existing_mut_store(&self.control_state, &self.control_state)?;
6686        let control_project_instance_id = control.captured_project_binding()?.project_instance_id;
6687        let registration = control.worktree_registration(alias)?;
6688        let repository = self.control_git_repository()?;
6689        let common_identity = CanonicalProjectRoot::from_path(&repository.common_directory)
6690            .map_err(|source| CliError::InvalidInput(source.to_string()))?;
6691        if common_identity != registration.git_common_directory_identity {
6692            return Err(CliError::InvalidInput(format!(
6693                "registered worktree '{}' belongs to a different Git common directory",
6694                alias.as_str()
6695            )));
6696        }
6697        let entry = repository
6698            .worktrees
6699            .iter()
6700            .find(|entry| {
6701                Self::worktree_administrative_path_identity(entry).is_some_and(|identity| {
6702                    identity == registration.git_administrative_directory_identity
6703                })
6704            })
6705            .ok_or_else(|| {
6706                CliError::InvalidInput(format!(
6707                    "registered worktree '{}' is no longer present in the bounded Git inventory",
6708                    alias.as_str()
6709                ))
6710            })?;
6711        let administrative_identity = git_administrative_identity(&entry.administrative_directory)?;
6712        if administrative_identity != registration.git_administrative_identity {
6713            return Err(CliError::InvalidInput(
6714                MCP_ERROR_WORKTREE_LIFECYCLE_CHANGED.to_string(),
6715            ));
6716        }
6717        let root = match &entry.state {
6718            GitWorktreeState::Active { root, .. } => root.clone(),
6719            GitWorktreeState::Missing { .. } => {
6720                return Err(CliError::InvalidInput(format!(
6721                    "registered worktree '{}' is missing; restore it through Git or unregister it",
6722                    alias.as_str()
6723                )));
6724            }
6725            GitWorktreeState::Invalid { issue } => {
6726                return Err(CliError::InvalidInput(format!(
6727                    "registered worktree '{}' has invalid Git evidence at '{}': {:?}",
6728                    alias.as_str(),
6729                    normalize_native_path_display(&issue.path),
6730                    issue.kind
6731                )));
6732            }
6733        };
6734        if root == self.control_state.root {
6735            return Err(CliError::InvalidInput(
6736                MCP_ERROR_CONTROL_ALIAS_REQUIRED.to_string(),
6737            ));
6738        }
6739        CanonicalProjectRoot::from_path(&root).map_err(|source| {
6740            let mut message = MCP_ERROR_REGISTERED_WORKTREE_ROOT_INVALID_PREFIX.to_string();
6741            message.push_str(&source.to_string());
6742            CliError::InvalidInput(message)
6743        })?;
6744        let current_registry_root = CanonicalProjectRoot::from_path(&root)
6745            .map_err(|source| CliError::InvalidInput(source.to_string()))?;
6746        // Validate a bound local atlas before any control-catalog refresh. A Git
6747        // move alone is not enough evidence to rewrite `last_root`; the local
6748        // atlas must first be opened through its native admission boundary and
6749        // retain the captured project identity.
6750        if let Some(expected) = registration.project_instance_id {
6751            let store = Self::open_local_worktree_atlas(&root)?.ok_or_else(|| {
6752                CliError::InvalidInput(MCP_ERROR_BOUND_WORKTREE_ATLAS_MISSING.to_string())
6753            })?;
6754            let db_path = Self::projectatlas_db_path(&root);
6755            let observed = Self::local_worktree_project_instance_id(&store, &db_path)?;
6756            if observed != expected {
6757                return Err(CliError::InvalidInput(
6758                    MCP_ERROR_WORKTREE_IDENTITY_CONFLICT.to_string(),
6759                ));
6760            }
6761        }
6762        let registration_root_matches = current_registry_root == registration.last_root_identity;
6763        let registration = if registration_root_matches {
6764            registration
6765        } else {
6766            control.refresh_worktree_root(&registration, &root)?
6767        };
6768        let mut state = Self::project_state_from_root_with_config_validation(&root, validation)?;
6769        state.worktree = Some(McpWorktreeSelection {
6770            alias: alias.to_string(),
6771            registration_id: Some(registration.registration_id),
6772            project_instance_id: registration.project_instance_id,
6773            control_project_instance_id: Some(control_project_instance_id),
6774        });
6775        Ok(state)
6776    }
6777
6778    /// Return project state under the requested configuration-validation timing.
6779    fn state_for_project_path_with_config_validation(
6780        &self,
6781        project_path: Option<String>,
6782        validation: McpConfigValidation,
6783    ) -> Result<McpProjectState, CliError> {
6784        let project_path = Self::normalized_optional_path(project_path);
6785        project_path.map_or_else(
6786            || self.active_project_state(),
6787            |path| {
6788                Self::project_state_from_root_with_config_validation(Path::new(&path), validation)
6789            },
6790        )
6791    }
6792
6793    /// Return the nearest-project policy for one call, honoring explicit overrides.
6794    fn nearest_project_enabled(&self, override_value: Option<bool>) -> bool {
6795        override_value.unwrap_or(self.allow_nearest_project)
6796    }
6797
6798    /// Return selected state and validate an optional root assertion.
6799    fn state_and_root_path(
6800        &self,
6801        project_path: Option<String>,
6802        worktree: Option<String>,
6803        path: Option<String>,
6804        nearest_project: bool,
6805    ) -> Result<(McpProjectState, PathBuf), CliError> {
6806        self.state_and_root_path_with_config_validation(
6807            project_path,
6808            worktree,
6809            path,
6810            nearest_project,
6811            McpConfigValidation::Immediate,
6812        )
6813    }
6814
6815    /// Select a background project without reading configuration before task admission.
6816    fn background_state_and_root_path(
6817        &self,
6818        project_path: Option<String>,
6819        worktree: Option<String>,
6820        path: Option<String>,
6821        nearest_project: bool,
6822    ) -> Result<(McpProjectState, PathBuf), CliError> {
6823        self.state_and_root_path_with_config_validation(
6824            project_path,
6825            worktree,
6826            path,
6827            nearest_project,
6828            McpConfigValidation::Deferred,
6829        )
6830    }
6831
6832    /// Select project state and validate a root assertion under one config timing policy.
6833    fn state_and_root_path_with_config_validation(
6834        &self,
6835        project_path: Option<String>,
6836        worktree: Option<String>,
6837        path: Option<String>,
6838        nearest_project: bool,
6839        validation: McpConfigValidation,
6840    ) -> Result<(McpProjectState, PathBuf), CliError> {
6841        let explicit_target = project_path.is_some() || worktree.is_some();
6842        let state = self.state_for_target_with_config_validation(
6843            project_path.clone(),
6844            worktree,
6845            validation,
6846        )?;
6847        Self::require_initialized_worktree_target(&state)?;
6848        let root = match (
6849            Self::normalized_optional_path(project_path),
6850            Self::normalized_optional_path(path),
6851        ) {
6852            (None, Some(path)) if !explicit_target => {
6853                match Self::path_or_project_root(&state, Some(path.clone())) {
6854                    Ok(root) => root,
6855                    Err(active_error) => {
6856                        if !nearest_project {
6857                            return Err(active_error);
6858                        }
6859                        if Self::absolute_path_inside_selected_root(&state, &path)? {
6860                            return Err(active_error);
6861                        }
6862                        let Some(indexed_state) =
6863                            Self::nearest_root_state_for_root_argument_with_config_validation(
6864                                Path::new(&path),
6865                                validation,
6866                            )?
6867                        else {
6868                            return Err(active_error);
6869                        };
6870                        let root = indexed_state.root.clone();
6871                        return Ok((indexed_state, root));
6872                    }
6873                }
6874            }
6875            (_, path) => Self::path_or_project_root(&state, path)?,
6876        };
6877        Ok((state, root))
6878    }
6879
6880    /// Return whether a root assertion path is inside the selected root but not root-equivalent.
6881    fn absolute_path_inside_selected_root(
6882        state: &McpProjectState,
6883        path: &str,
6884    ) -> Result<bool, CliError> {
6885        let candidate = PathBuf::from(path);
6886        if !candidate.is_absolute() {
6887            return Ok(false);
6888        }
6889        let resolved = canonical_project_root(&candidate)?;
6890        Ok(resolved != state.root && resolved.starts_with(&state.root))
6891    }
6892
6893    /// Return nearest indexed root under one configuration-validation timing policy.
6894    fn nearest_root_state_for_root_argument_with_config_validation(
6895        path: &Path,
6896        validation: McpConfigValidation,
6897    ) -> Result<Option<McpProjectState>, CliError> {
6898        let Ok(addressed_root) = canonical_project_root(path) else {
6899            return Ok(None);
6900        };
6901        let Some(indexed_state) =
6902            Self::project_state_from_nearest_indexed_path_with_config_validation(path, validation)?
6903        else {
6904            return Ok(None);
6905        };
6906        if addressed_root == indexed_state.root {
6907            Ok(Some(indexed_state))
6908        } else {
6909            Ok(None)
6910        }
6911    }
6912
6913    /// Return state and a repository-relative file key for an MCP file argument.
6914    fn state_and_file_key(
6915        &self,
6916        project_path: Option<&str>,
6917        worktree: Option<&str>,
6918        file: &str,
6919        nearest_project: bool,
6920    ) -> Result<McpResolvedRepoPath, CliError> {
6921        let state = self.state_for_target(
6922            project_path.map(ToString::to_string),
6923            worktree.map(ToString::to_string),
6924        )?;
6925        let file_path = PathBuf::from(&file);
6926        if !file_path.is_absolute() {
6927            let file_key = validated_repo_file_key(&file_path)
6928                .map_err(|source| CliError::InvalidInput(source.to_string()))?;
6929            return Ok(McpResolvedRepoPath {
6930                state,
6931                key: file_key,
6932                routed_project: false,
6933            });
6934        }
6935        if nearest_project && project_path.is_none() && worktree.is_none() {
6936            let resolved = Self::nearest_state_and_repo_key(&state, file)?.ok_or_else(|| {
6937                Self::selected_project_path_error(PATH_NOT_INSIDE_INDEXED_PROJECT_ERROR)
6938            })?;
6939            let file_key = validated_repo_file_key(Path::new(&resolved.key))
6940                .map_err(|source| CliError::InvalidInput(source.to_string()))?;
6941            return Ok(McpResolvedRepoPath {
6942                key: file_key,
6943                ..resolved
6944            });
6945        }
6946        if let Some(file_key) = Self::absolute_path_key_in_selected_project(&state, &file_path)? {
6947            let file_key = validated_repo_file_key(Path::new(&file_key))
6948                .map_err(|source| CliError::InvalidInput(source.to_string()))?;
6949            return Ok(McpResolvedRepoPath {
6950                state,
6951                key: file_key,
6952                routed_project: false,
6953            });
6954        }
6955        if project_path.is_some() || worktree.is_some() {
6956            return Err(Self::selected_project_path_error(
6957                PATH_NOT_INSIDE_INDEXED_PROJECT_ERROR,
6958            ));
6959        }
6960        if !nearest_project {
6961            return Err(Self::selected_project_path_error(
6962                PATH_NOT_INSIDE_INDEXED_PROJECT_ERROR,
6963            ));
6964        }
6965        Err(Self::selected_project_path_error(
6966            PATH_NOT_INSIDE_INDEXED_PROJECT_ERROR,
6967        ))
6968    }
6969
6970    /// Return state and an optional repository-relative file key for MCP symbol arguments.
6971    fn state_and_optional_file_key(
6972        &self,
6973        project_path: Option<&str>,
6974        worktree: Option<&str>,
6975        file: Option<&str>,
6976        nearest_project: bool,
6977    ) -> Result<(McpProjectState, Option<String>, bool), CliError> {
6978        let Some(file) = file else {
6979            return self
6980                .state_for_target(
6981                    project_path.map(ToString::to_string),
6982                    worktree.map(ToString::to_string),
6983                )
6984                .map(|state| (state, None, false));
6985        };
6986        let resolved = self.state_and_file_key(project_path, worktree, file, nearest_project)?;
6987        Ok((resolved.state, Some(resolved.key), resolved.routed_project))
6988    }
6989
6990    /// Return state and an optional folder filter for MCP file-ranking arguments.
6991    fn state_and_optional_folder_filter(
6992        &self,
6993        project_path: Option<&str>,
6994        worktree: Option<&str>,
6995        folder: Option<&str>,
6996        nearest_project: bool,
6997    ) -> Result<(McpProjectState, Option<String>, bool), CliError> {
6998        let state = self.state_for_target(
6999            project_path.map(ToString::to_string),
7000            worktree.map(ToString::to_string),
7001        )?;
7002        let Some(folder) = folder.map(str::trim).filter(|folder| !folder.is_empty()) else {
7003            return Ok((state, None, false));
7004        };
7005        let folder_path = PathBuf::from(&folder);
7006        if !folder_path.is_absolute() {
7007            let folder_filter = normalized_folder_filter(folder)?;
7008            return Ok((state, Some(folder_filter), false));
7009        }
7010        if nearest_project && project_path.is_none() && worktree.is_none() {
7011            let resolved = Self::nearest_state_and_repo_key(&state, folder)?.ok_or_else(|| {
7012                Self::selected_project_path_error(FOLDER_NOT_INSIDE_INDEXED_PROJECT_ERROR)
7013            })?;
7014            let folder_filter = normalized_folder_filter(&resolved.key)?;
7015            return Ok((resolved.state, Some(folder_filter), resolved.routed_project));
7016        }
7017        if let Some(folder_filter) =
7018            Self::absolute_path_key_in_selected_project(&state, &folder_path)?
7019        {
7020            let folder_filter = normalized_folder_filter(&folder_filter)?;
7021            return Ok((state, Some(folder_filter), false));
7022        }
7023        if project_path.is_some() || worktree.is_some() {
7024            return Err(Self::selected_project_path_error(
7025                FOLDER_NOT_INSIDE_INDEXED_PROJECT_ERROR,
7026            ));
7027        }
7028        if !nearest_project {
7029            return Err(Self::selected_project_path_error(
7030                FOLDER_NOT_INSIDE_INDEXED_PROJECT_ERROR,
7031            ));
7032        }
7033        Err(Self::selected_project_path_error(
7034            FOLDER_NOT_INSIDE_INDEXED_PROJECT_ERROR,
7035        ))
7036    }
7037
7038    /// Resolve an absolute addressed path to the nearest indexed project and repo key.
7039    fn nearest_state_and_repo_key(
7040        active_state: &McpProjectState,
7041        path: &str,
7042    ) -> Result<Option<McpResolvedRepoPath>, CliError> {
7043        let path = Path::new(path);
7044        let absolute_path = McpAbsolutePath::canonicalize(path)?;
7045        let lexical_state = Self::project_state_from_nearest_lexical_indexed_path(path)?;
7046        let canonical_state = Self::project_state_from_nearest_indexed_path(path)?;
7047        Self::reject_ambiguous_nearest_project_path(
7048            path,
7049            lexical_state.as_ref(),
7050            canonical_state.as_ref(),
7051            &absolute_path,
7052        )?;
7053        let Some(state) = canonical_state else {
7054            return Ok(None);
7055        };
7056        if state.root == active_state.root {
7057            let key = McpSelectedRoot::from_state(active_state)
7058                .repo_key_for(&absolute_path)?
7059                .ok_or_else(|| {
7060                    Self::selected_project_path_error(PATH_NOT_INSIDE_INDEXED_PROJECT_ERROR)
7061                })?;
7062            return Ok(Some(McpResolvedRepoPath {
7063                state: active_state.clone(),
7064                key: key.into_string(),
7065                routed_project: false,
7066            }));
7067        }
7068        let key = McpSelectedRoot::from_state(&state)
7069            .repo_key_for(&absolute_path)?
7070            .ok_or_else(|| {
7071                Self::selected_project_path_error(PATH_NOT_INSIDE_INDEXED_PROJECT_ERROR)
7072            })?;
7073        Ok(Some(McpResolvedRepoPath {
7074            state,
7075            key: key.into_string(),
7076            routed_project: true,
7077        }))
7078    }
7079
7080    /// Return a selected-project repository key for an absolute path inside the active root.
7081    fn absolute_path_key_in_selected_project(
7082        state: &McpProjectState,
7083        path: &Path,
7084    ) -> Result<Option<String>, CliError> {
7085        let absolute_path = McpAbsolutePath::canonicalize(path)?;
7086        McpSelectedRoot::from_state(state)
7087            .repo_key_for(&absolute_path)
7088            .map(|key| key.map(McpRepoKey::into_string))
7089    }
7090
7091    /// Normalize optional project/root path text from MCP payloads.
7092    fn normalized_optional_path(path: Option<String>) -> Option<String> {
7093        path.map(|path| path.trim().to_string())
7094            .filter(|path| !path.is_empty())
7095    }
7096
7097    /// Build `ProjectAtlas` state for one project root.
7098    fn project_state_from_root(root: &Path) -> Result<McpProjectState, CliError> {
7099        Self::project_state_from_root_with_config_validation(root, McpConfigValidation::Immediate)
7100    }
7101
7102    /// Build project state while controlling when configuration content is validated.
7103    fn project_state_from_root_with_config_validation(
7104        root: &Path,
7105        validation: McpConfigValidation,
7106    ) -> Result<McpProjectState, CliError> {
7107        let root = canonical_source_project_root(root)?;
7108        if !root.is_dir() {
7109            return Err(CliError::InvalidInput(format!(
7110                "project path '{}' is not a directory",
7111                root.display()
7112            )));
7113        }
7114        let db_path = Self::projectatlas_db_path(&root);
7115        let config_path = Self::config_path_for_project_root(&root, validation)?;
7116        Ok(McpProjectState {
7117            root,
7118            db_path,
7119            config_path,
7120            worktree: None,
7121        })
7122    }
7123
7124    /// Build project state from the nearest indexed ancestor of an addressed path.
7125    fn project_state_from_nearest_indexed_path(
7126        path: &Path,
7127    ) -> Result<Option<McpProjectState>, CliError> {
7128        Self::project_state_from_nearest_indexed_path_with_config_validation(
7129            path,
7130            McpConfigValidation::Immediate,
7131        )
7132    }
7133
7134    /// Build nearest canonical project state under one config-validation policy.
7135    fn project_state_from_nearest_indexed_path_with_config_validation(
7136        path: &Path,
7137        validation: McpConfigValidation,
7138    ) -> Result<Option<McpProjectState>, CliError> {
7139        let Ok(absolute_path) = McpAbsolutePath::canonicalize(path) else {
7140            return Ok(None);
7141        };
7142        let mut candidate = absolute_path.nearest_search_start();
7143        loop {
7144            if let Some(indexed_root) = Self::indexed_root_from_candidate(candidate) {
7145                let config_path =
7146                    Self::config_path_for_project_root(&indexed_root.root, validation)?;
7147                return Ok(Some(McpProjectState {
7148                    root: indexed_root.root,
7149                    db_path: indexed_root.db_path,
7150                    config_path,
7151                    worktree: None,
7152                }));
7153            }
7154            let Some(parent) = candidate.parent() else {
7155                return Ok(None);
7156            };
7157            candidate = parent;
7158        }
7159    }
7160
7161    /// Build project state from the nearest lexical indexed ancestor of an addressed path.
7162    fn project_state_from_nearest_lexical_indexed_path(
7163        path: &Path,
7164    ) -> Result<Option<McpProjectState>, CliError> {
7165        Self::project_state_from_nearest_lexical_indexed_path_with_config_validation(
7166            path,
7167            McpConfigValidation::Immediate,
7168        )
7169    }
7170
7171    /// Build nearest lexical project state under one config-validation policy.
7172    fn project_state_from_nearest_lexical_indexed_path_with_config_validation(
7173        path: &Path,
7174        validation: McpConfigValidation,
7175    ) -> Result<Option<McpProjectState>, CliError> {
7176        if !path.is_absolute() {
7177            return Ok(None);
7178        }
7179        let lexical_path = Self::lexically_normalized_absolute_path(path);
7180        let mut candidate = if lexical_path.is_dir() {
7181            lexical_path
7182        } else {
7183            lexical_path
7184                .parent()
7185                .unwrap_or(lexical_path.as_path())
7186                .to_path_buf()
7187        };
7188        loop {
7189            if let Some(indexed_root) = Self::indexed_root_from_lexical_candidate(&candidate) {
7190                let config_path =
7191                    Self::config_path_for_project_root(&indexed_root.root, validation)?;
7192                return Ok(Some(McpProjectState {
7193                    root: indexed_root.root,
7194                    db_path: indexed_root.db_path,
7195                    config_path,
7196                    worktree: None,
7197                }));
7198            }
7199            let Some(parent) = candidate.parent() else {
7200                return Ok(None);
7201            };
7202            candidate = parent.to_path_buf();
7203        }
7204    }
7205
7206    /// Return a role-typed indexed root when a candidate folder has a matching DB.
7207    fn indexed_root_from_candidate(candidate: &Path) -> Option<McpIndexedRoot> {
7208        let Ok(root) = canonical_project_root(candidate) else {
7209            return None;
7210        };
7211        let Ok(root_identity) = CanonicalProjectRoot::from_path(&root) else {
7212            return None;
7213        };
7214        let db_path = Self::projectatlas_db_path(&root);
7215        if !db_path.is_file() || !Self::nearest_indexed_db_matches_root(&db_path, &root_identity) {
7216            return None;
7217        }
7218        Some(McpIndexedRoot { root, db_path })
7219    }
7220
7221    /// Return an indexed lexical root without treating symlinked descendants as the lexical owner.
7222    fn indexed_root_from_lexical_candidate(candidate: &Path) -> Option<McpIndexedRoot> {
7223        if Self::path_has_symlink_component(candidate) {
7224            return None;
7225        }
7226        let Ok(root) = canonical_project_root(candidate) else {
7227            return None;
7228        };
7229        let Ok(candidate_identity) = CanonicalProjectRoot::from_path(candidate) else {
7230            return None;
7231        };
7232        let Ok(root_identity) = CanonicalProjectRoot::from_path(&root) else {
7233            return None;
7234        };
7235        if candidate_identity != root_identity {
7236            return None;
7237        }
7238        let db_path = Self::projectatlas_db_path(&root);
7239        if !db_path.is_file() || !Self::nearest_indexed_db_matches_root(&db_path, &root_identity) {
7240            return None;
7241        }
7242        Some(McpIndexedRoot { root, db_path })
7243    }
7244
7245    /// Return a normalized absolute path without resolving symlinks or junctions.
7246    fn lexically_normalized_absolute_path(path: &Path) -> PathBuf {
7247        let mut normalized = PathBuf::new();
7248        for component in path.components() {
7249            match component {
7250                Component::Prefix(prefix) => normalized.push(prefix.as_os_str()),
7251                Component::RootDir => normalized.push(component.as_os_str()),
7252                Component::CurDir => {}
7253                Component::ParentDir => {
7254                    normalized.pop();
7255                }
7256                Component::Normal(segment) => normalized.push(segment),
7257            }
7258        }
7259        normalized
7260    }
7261
7262    /// Return whether a path contains a symlink component in its lexical ancestry.
7263    fn path_has_symlink_component(path: &Path) -> bool {
7264        let mut current = PathBuf::new();
7265        for component in path.components() {
7266            match component {
7267                Component::Prefix(prefix) => current.push(prefix.as_os_str()),
7268                Component::RootDir => current.push(component.as_os_str()),
7269                Component::CurDir => {}
7270                Component::ParentDir => {
7271                    current.pop();
7272                }
7273                Component::Normal(segment) => {
7274                    current.push(segment);
7275                    if fs::symlink_metadata(&current)
7276                        .is_ok_and(|metadata| Self::metadata_is_symlink_or_reparse_point(&metadata))
7277                    {
7278                        return true;
7279                    }
7280                }
7281            }
7282        }
7283        false
7284    }
7285
7286    /// Return whether metadata represents a symlink or Windows reparse point.
7287    fn metadata_is_symlink_or_reparse_point(metadata: &fs::Metadata) -> bool {
7288        if metadata.file_type().is_symlink() {
7289            return true;
7290        }
7291        #[cfg(windows)]
7292        {
7293            use std::os::windows::fs::MetadataExt;
7294            const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x400;
7295            metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0
7296        }
7297        #[cfg(not(windows))]
7298        {
7299            false
7300        }
7301    }
7302
7303    /// Reject nearest-project routing when lexical and canonical roots disagree.
7304    fn reject_ambiguous_nearest_project_path(
7305        path: &Path,
7306        lexical_state: Option<&McpProjectState>,
7307        canonical_state: Option<&McpProjectState>,
7308        canonical_path: &McpAbsolutePath,
7309    ) -> Result<(), CliError> {
7310        if let (Some(lexical_state), Some(canonical_state)) = (lexical_state, canonical_state) {
7311            if lexical_state.root != canonical_state.root {
7312                return Err(Self::ambiguous_nearest_project_path_error(
7313                    path,
7314                    Some(lexical_state),
7315                    Some(canonical_state),
7316                ));
7317            }
7318            return Ok(());
7319        }
7320        let lexical_path = Self::lexically_normalized_absolute_path(path);
7321        if Self::path_has_symlink_component(&lexical_path)
7322            || lexical_state.is_some_and(|state| !canonical_path.as_path().starts_with(&state.root))
7323        {
7324            return Err(Self::ambiguous_nearest_project_path_error(
7325                path,
7326                lexical_state,
7327                canonical_state,
7328            ));
7329        }
7330        Ok(())
7331    }
7332
7333    /// Build a clear MCP error for ambiguous symlink or junction routing.
7334    fn ambiguous_nearest_project_path_error(
7335        path: &Path,
7336        lexical_state: Option<&McpProjectState>,
7337        canonical_state: Option<&McpProjectState>,
7338    ) -> CliError {
7339        let lexical_root = lexical_state.map_or_else(
7340            || MCP_NO_ROOT_PLACEHOLDER.to_string(),
7341            |state| normalize_native_path_display(&state.root),
7342        );
7343        let resolved_root = canonical_state.map_or_else(
7344            || MCP_NO_ROOT_PLACEHOLDER.to_string(),
7345            |state| normalize_native_path_display(&state.root),
7346        );
7347        let path_display = normalize_native_path_display(path);
7348        let mut message = String::new();
7349        message.push_str(AMBIGUOUS_NEAREST_PROJECT_PATH_ERROR);
7350        message.push_str(MCP_ERROR_FOR_PATH_FRAGMENT);
7351        message.push_str(&path_display);
7352        message.push_str(MCP_ERROR_LEXICAL_ROOT_FRAGMENT);
7353        message.push_str(&lexical_root);
7354        message.push_str(MCP_ERROR_RESOLVED_ROOT_FRAGMENT);
7355        message.push_str(&resolved_root);
7356        message.push_str(MCP_ERROR_GUIDANCE_FRAGMENT);
7357        message.push_str(OUTSIDE_SELECTED_PROJECT_GUIDANCE);
7358        CliError::InvalidInput(message)
7359    }
7360
7361    /// Return whether nearest routing can safely associate one DB with a live root.
7362    fn nearest_indexed_db_matches_root(db_path: &Path, root: &CanonicalProjectRoot) -> bool {
7363        let Ok(admission) = crate::runtime::preflight_managed_database(db_path, root.as_path())
7364        else {
7365            return false;
7366        };
7367        let db_path = crate::runtime::admitted_database_path(db_path, admission.as_ref());
7368        match read_project_root_identity_read_only(db_path) {
7369            Ok(Some(stored_root)) => Self::nearest_existing_roots_match(&stored_root, root),
7370            Ok(None) => {
7371                let Ok(Some(legacy_root)) = read_legacy_project_root_candidate_read_only(db_path)
7372                else {
7373                    return false;
7374                };
7375                let Ok(legacy_root) = CanonicalProjectRoot::from_path(Path::new(&legacy_root))
7376                else {
7377                    return false;
7378                };
7379                Self::nearest_existing_roots_match(&legacy_root, root)
7380            }
7381            Err(_) => false,
7382        }
7383    }
7384
7385    /// Re-resolve both existing roots before nearest-routing comparison.
7386    fn nearest_existing_roots_match(
7387        persisted_root: &CanonicalProjectRoot,
7388        selected_root: &CanonicalProjectRoot,
7389    ) -> bool {
7390        let Ok(persisted_root) = CanonicalProjectRoot::from_path(persisted_root.as_path()) else {
7391            return false;
7392        };
7393        let Ok(selected_root) = CanonicalProjectRoot::from_path(selected_root.as_path()) else {
7394            return false;
7395        };
7396        persisted_root == selected_root
7397    }
7398
7399    /// Return the standard `ProjectAtlas` DB path for one project root.
7400    fn projectatlas_db_path(root: &Path) -> PathBuf {
7401        root.join(PROJECTATLAS_DIR_NAME)
7402            .join(PROJECTATLAS_DB_FILE_NAME)
7403    }
7404
7405    /// Return the standard nested `ProjectAtlas` config path for one project root.
7406    fn projectatlas_nested_config_path(root: &Path) -> PathBuf {
7407        root.join(PROJECTATLAS_DIR_NAME)
7408            .join(PROJECTATLAS_CONFIG_FILE_NAME)
7409    }
7410
7411    /// Return the flat `ProjectAtlas` config path for one project root.
7412    fn projectatlas_flat_config_path(root: &Path) -> PathBuf {
7413        root.join(PROJECTATLAS_FLAT_CONFIG_FILE_NAME)
7414    }
7415
7416    /// Find a project-local config and optionally reject one pointing at another root.
7417    fn config_path_for_project_root(
7418        root: &Path,
7419        validation: McpConfigValidation,
7420    ) -> Result<Option<PathBuf>, CliError> {
7421        for config_path in [
7422            Self::projectatlas_nested_config_path(root),
7423            Self::projectatlas_flat_config_path(root),
7424        ] {
7425            if config_path.exists() {
7426                if validation == McpConfigValidation::Immediate {
7427                    Self::validate_project_config_root(root, &config_path)?;
7428                }
7429                return Ok(Some(config_path));
7430            }
7431        }
7432        Ok(None)
7433    }
7434
7435    /// Ensure a selected project's config cannot redirect the MCP root.
7436    fn validate_project_config_root(root: &Path, config_path: &Path) -> Result<(), CliError> {
7437        let config = load_atlas_config(Some(config_path))?;
7438        let config_root = canonical_project_root(&config.root)?;
7439        if config_root != root {
7440            return Err(config_root_mismatch_error(config_path, &config_root, root));
7441        }
7442        Ok(())
7443    }
7444
7445    /// Return whether a startup config belongs to the selected root.
7446    fn config_matches_project_root(root: &Path, config_path: &Path) -> bool {
7447        Self::validate_project_config_root(root, config_path).is_ok()
7448    }
7449
7450    /// Render active project state for agents.
7451    fn render_project_state(state: &McpProjectState) -> Result<String, CliError> {
7452        let payload = McpProjectStateResponse {
7453            project: Self::project_state_payload(state),
7454        };
7455        Self::encode_serialized_payload(payload)
7456    }
7457
7458    /// Build selected-project payload fields.
7459    fn project_state_payload(state: &McpProjectState) -> McpProjectStatePayload {
7460        McpProjectStatePayload {
7461            worktree: state
7462                .worktree
7463                .as_ref()
7464                .map(|selection| selection.alias.clone()),
7465            registration_id: state
7466                .worktree
7467                .as_ref()
7468                .and_then(|selection| selection.registration_id),
7469            root: lossless_project_root_display(&state.root),
7470            db: lossless_native_path_display(&state.db_path),
7471            config: state
7472                .config_path
7473                .as_ref()
7474                .and_then(|path| lossless_native_path_display(path)),
7475            status: McpProjectStatus::Active,
7476        }
7477    }
7478
7479    /// Prefix routed cross-project read payloads with selected root/DB metadata.
7480    fn with_selected_project_audit(
7481        state: &McpProjectState,
7482        routed_project: bool,
7483        toon: String,
7484    ) -> Result<String, CliError> {
7485        if !routed_project {
7486            return Ok(toon);
7487        }
7488        let prefix = Self::encode_named_payload(
7489            MCP_PAYLOAD_SELECTED_PROJECT,
7490            &Self::project_state_payload(state),
7491        )?;
7492        let mut audited = String::with_capacity(prefix.len() + 1 + toon.len());
7493        audited.push_str(&prefix);
7494        audited.push('\n');
7495        audited.push_str(&toon);
7496        Ok(audited)
7497    }
7498
7499    /// Prefix one controlled analysis payload with selected root/DB metadata.
7500    fn with_selected_project_audit_controlled(
7501        state: &McpProjectState,
7502        routed_project: bool,
7503        toon: String,
7504        control: &IndexWorkControl,
7505    ) -> Result<String, CliError> {
7506        control.check(projectatlas_core::IndexWorkStage::RepositoryTraversal)?;
7507        if !routed_project {
7508            return Ok(toon);
7509        }
7510        let prefix = controlled_named_output(
7511            OutputFormat::Toon,
7512            MCP_PAYLOAD_SELECTED_PROJECT,
7513            &Self::project_state_payload(state),
7514            control,
7515        )?;
7516        let mut audited = String::with_capacity(prefix.len() + 1 + toon.len());
7517        audited.push_str(&prefix);
7518        audited.push('\n');
7519        audited.push_str(&toon);
7520        control.check(projectatlas_core::IndexWorkStage::RepositoryTraversal)?;
7521        Ok(audited)
7522    }
7523
7524    /// Return a path parameter or the selected project root.
7525    fn path_or_project_root(
7526        state: &McpProjectState,
7527        path: Option<String>,
7528    ) -> Result<PathBuf, CliError> {
7529        let Some(value) = path else {
7530            return Ok(state.root.clone());
7531        };
7532        if value.is_empty() {
7533            return Ok(state.root.clone());
7534        }
7535        let original = value.clone();
7536        let candidate = PathBuf::from(value);
7537        let resolved = if candidate.is_absolute() {
7538            candidate
7539        } else {
7540            state.root.join(candidate)
7541        };
7542        let resolved = canonical_project_root(&resolved)?;
7543        if resolved == state.root {
7544            Ok(resolved)
7545        } else if resolved.starts_with(&state.root) {
7546            let resolved_display = resolved.display();
7547            let project_root_display = state.root.display();
7548            Err(CliError::InvalidInput(format!(
7549                "MCP path '{original}' resolves to '{resolved_display}', not the selected project root '{project_root_display}'; {SELECTED_ROOT_ASSERTION_GUIDANCE}"
7550            )))
7551        } else {
7552            let resolved_display = resolved.display();
7553            let project_root_display = state.root.display();
7554            Err(CliError::InvalidInput(format!(
7555                "MCP path '{original}' resolves to '{resolved_display}', outside the selected project root '{project_root_display}'; {OUTSIDE_SELECTED_PROJECT_GUIDANCE}"
7556            )))
7557        }
7558    }
7559
7560    /// Reject invalid purpose targets before admission can supersede another source witness.
7561    fn preflight_purpose_path(state: &McpProjectState, path: &str) -> Result<String, CliError> {
7562        let node_key = validated_repo_node_key(std::path::Path::new(path))
7563            .map_err(Self::selected_project_path_error)?;
7564        let store = Self::open_read_store(state)?;
7565        let indexed = store.load_node_by_path(&node_key)?.is_some();
7566        store.finish_index_read_snapshot()?;
7567        let source = state.root.join(&node_key);
7568        if !indexed
7569            && !source.try_exists().map_err(|source_error| CliError::Io {
7570                path: source,
7571                source: source_error,
7572            })?
7573        {
7574            return Err(CliError::InvalidInput(format!(
7575                "path {node_key:?} is not indexed in the MCP-bound project"
7576            )));
7577        }
7578        Ok(node_key)
7579    }
7580
7581    /// Require a purpose target in the captured index, including inside its write transaction.
7582    fn require_indexed_purpose_path(store: &AtlasStore, node_key: &str) -> Result<(), CliError> {
7583        if store.load_node_by_path(node_key)?.is_none() {
7584            return Err(CliError::InvalidInput(format!(
7585                "path {node_key:?} is not indexed in the MCP-bound project"
7586            )));
7587        }
7588        Ok(())
7589    }
7590
7591    /// Add selected-project guidance to repository-relative path errors.
7592    fn selected_project_path_error(message: impl std::fmt::Display) -> CliError {
7593        CliError::InvalidInput(format!("{message}; {OUTSIDE_SELECTED_PROJECT_GUIDANCE}"))
7594    }
7595
7596    /// Return a query parameter with a stable default.
7597    fn query_or_empty(query: Option<String>) -> String {
7598        query.unwrap_or_default()
7599    }
7600
7601    /// Encode one serializable payload as agent-readable TOON text.
7602    fn encode_serialized_payload<T>(payload: T) -> Result<String, CliError>
7603    where
7604        T: Serialize,
7605    {
7606        Ok(encode_agent_payload(&serde_json::to_value(payload)?))
7607    }
7608
7609    /// Encode a dynamic top-level payload key without relying on `json!` key syntax.
7610    fn encode_named_payload<T>(key: &str, payload: &T) -> Result<String, CliError>
7611    where
7612        T: Serialize,
7613    {
7614        let mut object = serde_json::Map::new();
7615        object.insert(key.to_string(), serde_json::to_value(payload)?);
7616        Ok(encode_agent_payload(&serde_json::Value::Object(object)))
7617    }
7618
7619    /// Encode two dynamic top-level payload keys without relying on `json!` key syntax.
7620    fn encode_two_named_payloads<T, U>(
7621        first_key: &str,
7622        first_payload: &T,
7623        second_key: &str,
7624        second_payload: &U,
7625    ) -> Result<String, CliError>
7626    where
7627        T: Serialize,
7628        U: Serialize,
7629    {
7630        let mut object = serde_json::Map::new();
7631        object.insert(first_key.to_string(), serde_json::to_value(first_payload)?);
7632        object.insert(
7633            second_key.to_string(),
7634            serde_json::to_value(second_payload)?,
7635        );
7636        Ok(encode_agent_payload(&serde_json::Value::Object(object)))
7637    }
7638
7639    /// Encode an MCP error as a structured agent-readable payload.
7640    fn encode_error_payload(error: &CliError) -> String {
7641        if let Some(response) = crate::legacy_relation_error_response(error) {
7642            return encode_agent_payload(&response);
7643        }
7644        let schema_version_mismatch = schema_version_mismatch_payload(error);
7645        let schema_migration_required = schema_migration_required_payload(error);
7646        let message = schema_migration_required.as_ref().map_or_else(
7647            || error.to_string(),
7648            SchemaMigrationRequiredPayload::message,
7649        );
7650        let (
7651            kind,
7652            refresh_required,
7653            init_required,
7654            worktree_required,
7655            verification_incomplete,
7656            project_mismatch,
7657            database_filesystem,
7658            search_capability,
7659            next,
7660        ) = match error {
7661            CliError::AtlasMap(crate::atlas_map::AtlasMapError::ResponseLimitExceeded {
7662                ..
7663            }) => (
7664                AgentErrorKind::ResponseLimitExceeded,
7665                None,
7666                None,
7667                None,
7668                None,
7669                None,
7670                None,
7671                None,
7672                None,
7673            ),
7674            CliError::InitRequired(report) => (
7675                AgentErrorKind::InitRequired,
7676                None,
7677                Some(report.as_ref().clone()),
7678                None,
7679                None,
7680                None,
7681                None,
7682                None,
7683                Some(McpNextCall {
7684                    tool: MCP_TOOL_ATLAS_INIT,
7685                    project_path: report
7686                        .worktree
7687                        .is_none()
7688                        .then(|| report.project_root.clone())
7689                        .flatten(),
7690                    worktree: report.worktree.clone(),
7691                }),
7692            ),
7693            CliError::WorktreeRequired(report) => (
7694                AgentErrorKind::WorktreeRequired,
7695                None,
7696                None,
7697                Some(report.as_ref().clone()),
7698                None,
7699                None,
7700                None,
7701                None,
7702                None,
7703            ),
7704            CliError::RefreshRequired(report) => (
7705                AgentErrorKind::RefreshRequired,
7706                Some(report.as_ref().clone()),
7707                None,
7708                None,
7709                None,
7710                None,
7711                None,
7712                None,
7713                Some(McpNextCall {
7714                    tool: MCP_TOOL_ATLAS_WATCH_ONCE,
7715                    project_path: report
7716                        .worktree
7717                        .is_none()
7718                        .then(|| report.project_root.clone())
7719                        .flatten(),
7720                    worktree: report.worktree.clone(),
7721                }),
7722            ),
7723            CliError::VerificationIncomplete(report) => (
7724                AgentErrorKind::VerificationIncomplete,
7725                None,
7726                None,
7727                None,
7728                Some(report.as_ref().clone()),
7729                None,
7730                None,
7731                None,
7732                None,
7733            ),
7734            CliError::ProjectMismatch(report) => (
7735                AgentErrorKind::ProjectMismatch,
7736                None,
7737                None,
7738                None,
7739                None,
7740                Some(report.as_ref().clone()),
7741                None,
7742                None,
7743                None,
7744            ),
7745            CliError::Service(ServiceError::SearchCapabilityUnavailable {
7746                requested_mode,
7747                state,
7748                guidance,
7749            }) => (
7750                AgentErrorKind::SearchCapabilityUnavailable,
7751                None,
7752                None,
7753                None,
7754                None,
7755                None,
7756                None,
7757                Some(crate::SearchCapabilityErrorPayload {
7758                    requested_mode: *requested_mode,
7759                    state,
7760                    recovery: guidance,
7761                }),
7762                None,
7763            ),
7764            _ if schema_version_mismatch.is_some() => (
7765                AgentErrorKind::SchemaVersionMismatch,
7766                None,
7767                None,
7768                None,
7769                None,
7770                None,
7771                None,
7772                None,
7773                None,
7774            ),
7775            _ if schema_migration_required.is_some() => (
7776                AgentErrorKind::SchemaMigrationRequired,
7777                None,
7778                None,
7779                None,
7780                None,
7781                None,
7782                None,
7783                None,
7784                None,
7785            ),
7786            _ => database_filesystem_error_payload(error).map_or(
7787                (
7788                    AgentErrorKind::Error,
7789                    None,
7790                    None,
7791                    None,
7792                    None,
7793                    None,
7794                    None,
7795                    None,
7796                    None,
7797                ),
7798                |(kind, database_filesystem)| {
7799                    (
7800                        kind,
7801                        None,
7802                        None,
7803                        None,
7804                        None,
7805                        None,
7806                        Some(database_filesystem),
7807                        None,
7808                        None,
7809                    )
7810                },
7811            ),
7812        };
7813        let payload = McpErrorResponse {
7814            error: McpErrorPayload {
7815                kind,
7816                message,
7817                refresh_required,
7818                init_required,
7819                worktree_required,
7820                verification_incomplete,
7821                project_mismatch,
7822                database_filesystem,
7823                schema_version_mismatch,
7824                schema_migration_required,
7825                search_capability,
7826                next,
7827            },
7828        };
7829        serde_json::to_value(payload).map_or_else(
7830            |source| {
7831                let mut message = MCP_ERROR_SERIALIZATION_FALLBACK_PREFIX.to_string();
7832                message.push_str(&source.to_string());
7833                message
7834            },
7835            |value| encode_agent_payload(&value),
7836        )
7837    }
7838
7839    /// Convert a command result into an agent-readable TOON MCP text payload.
7840    fn as_mcp_text(result: Result<String, CliError>) -> McpToolTextResult {
7841        match result {
7842            Ok(text) => McpToolTextResult(Ok(text)),
7843            Err(error) => {
7844                if let Some(response) = crate::legacy_relation_error_response(&error) {
7845                    return McpToolTextResult(Err(encode_agent_payload(&response)));
7846                }
7847                let payload = Self::encode_error_payload(&error);
7848                if schema_version_mismatch_payload(&error).is_some() {
7849                    McpToolTextResult(Err(payload))
7850                } else {
7851                    McpToolTextResult(Ok(payload))
7852                }
7853            }
7854        }
7855    }
7856}
7857
7858/// Convert MCP health parameters into a DB health query.
7859fn health_query_from_params(
7860    params: &AtlasHealthParams,
7861    scope: HealthScope,
7862) -> Result<HealthQuery, CliError> {
7863    Ok(HealthQuery {
7864        start_index: params.start_index.unwrap_or(0),
7865        limit: params
7866            .limit
7867            .filter(|value| *value > 0)
7868            .unwrap_or(DEFAULT_HEALTH_LIMIT)
7869            .min(MAX_HEALTH_LIMIT),
7870        category: trimmed_filter(params.category.as_deref()),
7871        severity: trimmed_filter(params.severity.as_deref())
7872            .as_deref()
7873            .map(parse_health_severity)
7874            .transpose()?,
7875        path_prefix: trimmed_filter(params.path_prefix.as_deref())
7876            .map(|value| normalize_repo_path_prefix(&value)),
7877        summary_only: params.summary_only.unwrap_or(false),
7878        scope,
7879    })
7880}
7881
7882/// Return whether MCP parameters contain coverage-only filters.
7883fn has_coverage_filters(params: &AtlasHealthParams) -> bool {
7884    params.parser.is_some()
7885        || params.provider.is_some()
7886        || params.relation.is_some()
7887        || params.coverage_state.is_some()
7888        || params.reason.is_some()
7889}
7890
7891/// Convert explicit MCP coverage parameters into one typed bounded DB query.
7892fn coverage_query_from_params(
7893    params: &AtlasHealthParams,
7894) -> Result<RepositoryCoverageQuery, CliError> {
7895    let limit = params
7896        .limit
7897        .filter(|value| *value > 0)
7898        .unwrap_or(DEFAULT_HEALTH_LIMIT)
7899        .min(COVERAGE_PAGE_MAX_LIMIT as usize);
7900    Ok(RepositoryCoverageQuery {
7901        start_index: u32::try_from(params.start_index.unwrap_or(0)).map_err(|error| {
7902            let mut message = String::from(MCP_ERROR_COVERAGE_START_INDEX_TOO_LARGE_PREFIX);
7903            message.push_str(&error.to_string());
7904            CliError::InvalidInput(message)
7905        })?,
7906        limit: u32::try_from(limit).map_err(|error| {
7907            let mut message = String::from(MCP_ERROR_COVERAGE_LIMIT_TOO_LARGE_PREFIX);
7908            message.push_str(&error.to_string());
7909            CliError::InvalidInput(message)
7910        })?,
7911        path_prefix: trimmed_filter(params.path_prefix.as_deref())
7912            .map(|value| normalize_repo_path_prefix(&value)),
7913        parser: trimmed_filter(params.parser.as_deref())
7914            .as_deref()
7915            .map(parse_coverage_parser)
7916            .transpose()?,
7917        provider: trimmed_filter(params.provider.as_deref())
7918            .as_deref()
7919            .map(parse_coverage_parser)
7920            .transpose()?,
7921        relation: trimmed_filter(params.relation.as_deref())
7922            .as_deref()
7923            .map(parse_coverage_relation)
7924            .transpose()?,
7925        state: trimmed_filter(params.coverage_state.as_deref())
7926            .as_deref()
7927            .map(parse_coverage_state)
7928            .transpose()?,
7929        reason: trimmed_filter(params.reason.as_deref()),
7930    })
7931}
7932
7933/// Return the DB scope for MCP purpose queue parameters.
7934fn purpose_queue_scope(params: &AtlasHealthParams) -> HealthScope {
7935    match (
7936        params.include_assets.unwrap_or(false),
7937        params.include_low_priority_files.unwrap_or(false),
7938    ) {
7939        (false, false) => HealthScope::purpose_default(),
7940        (true, false) => HealthScope::purpose_with_assets(),
7941        (false, true) => HealthScope::purpose_with_source_files(),
7942        (true, true) => HealthScope::all(),
7943    }
7944}
7945
7946/// Return a trimmed non-empty string parameter.
7947fn trimmed_filter(value: Option<&str>) -> Option<String> {
7948    value
7949        .map(str::trim)
7950        .filter(|value| !value.is_empty())
7951        .map(ToString::to_string)
7952}
7953
7954/// Parse an MCP health severity filter.
7955fn parse_health_severity(value: &str) -> Result<Severity, CliError> {
7956    let trimmed = value.trim();
7957    trimmed.parse::<Severity>().map_err(|_source| {
7958        let expected = expected_health_severity_names();
7959        CliError::InvalidInput(format!(
7960            "invalid health severity '{trimmed}'; expected {expected}"
7961        ))
7962    })
7963}
7964
7965/// Render accepted health severity names for diagnostics.
7966fn expected_health_severity_names() -> String {
7967    let mut expected = Severity::Info.as_str().to_string();
7968    expected.push_str(SEVERITY_EXPECTED_SEPARATOR);
7969    expected.push_str(Severity::Warning.as_str());
7970    expected.push_str(SEVERITY_EXPECTED_FINAL_SEPARATOR);
7971    expected.push_str(Severity::Error.as_str());
7972    expected
7973}
7974
7975#[tool_router(router = tool_router)]
7976impl ProjectAtlasMcpServer {
7977    /// Select the active project root for subsequent MCP calls.
7978    #[tool(
7979        name = "atlas_set_project_path",
7980        description = "Select the active ProjectAtlas project root for later MCP calls that omit project_path."
7981    )]
7982    fn atlas_set_project_path(
7983        &self,
7984        Parameters(params): Parameters<AtlasSetProjectPathParams>,
7985    ) -> McpToolTextResult {
7986        Self::as_mcp_text((|| {
7987            let state = Self::project_state_from_root(Path::new(&params.project_path))?;
7988            self.set_active_project_state(state.clone())?;
7989            Self::render_project_state(&state)
7990        })())
7991    }
7992
7993    /// Return bounded structural and `ProjectAtlas` worktree state without mutation.
7994    #[tool(
7995        name = "atlas_worktree_list",
7996        description = "List structurally discovered Git worktrees, short ProjectAtlas aliases, atlas availability, and telemetry synchronization state without changing Git or files."
7997    )]
7998    fn atlas_worktree_list(
7999        &self,
8000        Parameters(params): Parameters<AtlasWorktreeListParams>,
8001    ) -> McpToolTextResult {
8002        Self::as_mcp_text((|| {
8003            let repository = self.control_git_repository()?;
8004            let store = open_atlas_store_read_only_for_project(
8005                &self.control_state.db_path,
8006                &self.control_state.root,
8007            )?;
8008            let include_retired = params.include_retired.unwrap_or(false);
8009            let registrations = store.worktree_registrations(include_retired)?;
8010            let structural_identities = repository
8011                .worktrees
8012                .iter()
8013                .filter_map(Self::worktree_administrative_path_identity)
8014                .collect::<HashSet<_>>();
8015            let (mut worktrees, unregistered): (Vec<_>, Vec<_>) = repository
8016                .worktrees
8017                .iter()
8018                .map(|entry| {
8019                    self.worktree_list_row(&repository.common_directory, entry, &registrations)
8020                })
8021                .partition(|row| {
8022                    !matches!(row.registration, McpWorktreeRegistrationState::Unregistered)
8023                });
8024            worktrees.extend(
8025                registrations
8026                    .iter()
8027                    .filter(|registration| {
8028                        registration.state == WorktreeRegistrationState::Active
8029                            && !structural_identities
8030                                .contains(&registration.git_administrative_directory_identity)
8031                    })
8032                    .map(Self::missing_registered_worktree_row),
8033            );
8034            let total_worktrees = worktrees.len() + unregistered.len();
8035            worktrees.extend(
8036                unregistered
8037                    .into_iter()
8038                    .take(MCP_WORKTREE_LIST_MAX_ROWS.saturating_sub(worktrees.len())),
8039            );
8040            let truncated = total_worktrees > worktrees.len();
8041            let retired = registrations
8042                .iter()
8043                .filter(|registration| registration.state == WorktreeRegistrationState::Retired)
8044                .map(|registration| McpRetiredWorktreeRow {
8045                    alias: registration.alias.to_string(),
8046                    path_display: if registration.last_root_identity.display_string().is_ok() {
8047                        McpWorktreePathDisplayState::Available
8048                    } else {
8049                        McpWorktreePathDisplayState::Unavailable
8050                    },
8051                    last_root: registration.last_root_identity.display_string().ok(),
8052                    project_instance_id: registration
8053                        .project_instance_id
8054                        .map(|identity| identity.to_string()),
8055                    accepted_telemetry_revision: registration.accepted_telemetry_revision,
8056                })
8057                .collect();
8058            Self::encode_named_payload(
8059                MCP_PAYLOAD_WORKTREES,
8060                &McpWorktreeListReport {
8061                    control_alias: MCP_MAIN_WORKTREE_ALIAS,
8062                    control_root: lossless_project_root_display(&self.control_state.root),
8063                    common_directory: lossless_native_path_display(&repository.common_directory),
8064                    worktrees,
8065                    retired,
8066                    total_worktrees,
8067                    truncated,
8068                },
8069            )
8070        })())
8071    }
8072
8073    /// Register one active structural worktree in the immutable control atlas.
8074    #[tool(
8075        name = "atlas_worktree_add",
8076        description = "Register one structurally discovered Git worktree under a short ProjectAtlas alias without creating, moving, or switching Git worktrees."
8077    )]
8078    fn atlas_worktree_add(
8079        &self,
8080        Parameters(params): Parameters<AtlasWorktreeAddParams>,
8081    ) -> McpToolTextResult {
8082        Self::as_mcp_text((|| {
8083            let requested = params.worktree.trim();
8084            if requested.is_empty() {
8085                return Err(CliError::InvalidInput(
8086                    MCP_ERROR_WORKTREE_SELECTOR_EMPTY.to_string(),
8087                ));
8088            }
8089            let repository = self.control_git_repository()?;
8090            let candidates = self.matching_worktree_candidates(&repository, requested);
8091            if candidates.len() != 1 {
8092                let ambiguous = !candidates.is_empty();
8093                let candidate_rows: Vec<McpWorktreeCandidate> = if candidates.is_empty() {
8094                    repository
8095                        .worktrees
8096                        .iter()
8097                        .filter(|entry| {
8098                            Self::active_worktree_root(entry)
8099                                .is_some_and(|root| root != self.control_state.root)
8100                        })
8101                        .filter_map(|entry| {
8102                            Self::worktree_candidate(&repository.common_directory, entry)
8103                        })
8104                        .take(MCP_WORKTREE_LIST_MAX_ROWS)
8105                        .collect()
8106                } else {
8107                    candidates
8108                        .into_iter()
8109                        .filter_map(|entry| {
8110                            Self::worktree_candidate(&repository.common_directory, entry)
8111                        })
8112                        .take(MCP_WORKTREE_LIST_MAX_ROWS)
8113                        .collect()
8114                };
8115                return Self::encode_named_payload(
8116                    MCP_PAYLOAD_WORKTREE,
8117                    &McpWorktreeMutationReport {
8118                        operation: McpWorktreeMutationOperation::Add,
8119                        status: if ambiguous {
8120                            McpWorktreeMutationStatus::Ambiguous
8121                        } else {
8122                            McpWorktreeMutationStatus::NotFound
8123                        },
8124                        selector: None,
8125                        alias: None,
8126                        root: None,
8127                        path_display: None,
8128                        registration_id: None,
8129                        telemetry_sync: None,
8130                        candidates: candidate_rows,
8131                        blocker: Some(format!(
8132                            "selector {requested:?} did not identify exactly one active non-control worktree"
8133                        )),
8134                        git_unchanged: true,
8135                        files_unchanged: true,
8136                    },
8137                );
8138            }
8139            let entry = candidates[0];
8140            let root = Self::active_worktree_root(entry).ok_or_else(|| {
8141                CliError::InvalidInput(MCP_ERROR_WORKTREE_NO_LONGER_ACTIVE.to_string())
8142            })?;
8143            let administrative_identity =
8144                git_administrative_identity(&entry.administrative_directory)?;
8145            let alias = match params.alias.as_deref() {
8146                Some(alias) => WorktreeAlias::parse(alias.trim())?,
8147                None => Self::default_worktree_alias(root)?,
8148            };
8149            let db_path = Self::projectatlas_db_path(root);
8150            let mut project_instance_id = None;
8151            let local = (|| {
8152                let Some(store) = Self::open_local_worktree_atlas(root)? else {
8153                    return Ok(None);
8154                };
8155                let identity = Self::local_worktree_project_instance_id(&store, &db_path)?;
8156                project_instance_id = Some(identity);
8157                let snapshot = Self::local_worktree_usage_snapshot(&store, &db_path, identity)?;
8158                Ok::<_, CliError>(Some(LocalWorktreeAtlas {
8159                    project_instance_id: identity,
8160                    snapshot,
8161                }))
8162            })();
8163            let (local, blocker) = match local {
8164                Ok(local) => (local, None),
8165                Err(error) if project_instance_id.is_some() => return Err(error),
8166                Err(error) => (
8167                    None,
8168                    Some(format!(
8169                        "registration committed without local telemetry import: {error}"
8170                    )),
8171                ),
8172            };
8173            let (repository, entry) =
8174                self.revalidate_worktree_candidate(&repository, entry, &administrative_identity)?;
8175            let root = Self::active_worktree_root(&entry).ok_or_else(|| {
8176                CliError::InvalidInput(MCP_ERROR_WORKTREE_NO_LONGER_ACTIVE.to_string())
8177            })?;
8178            Self::revalidate_local_worktree_atlas_identity(root, project_instance_id)?;
8179            if let Some(local) = local.as_ref() {
8180                require_current_worktree_usage_snapshot(&db_path, root, &local.snapshot)?;
8181            }
8182            let control = Self::open_existing_mut_store(&self.control_state, &self.control_state)?;
8183            let created_at_epoch = Self::current_epoch_seconds()?;
8184            let (registration, telemetry_sync) = if let Some(local) = local.as_ref() {
8185                match control.register_worktree_with_usage_snapshot(
8186                    &alias,
8187                    &repository.common_directory,
8188                    &entry.administrative_directory,
8189                    &administrative_identity,
8190                    root,
8191                    local.project_instance_id,
8192                    &local.snapshot,
8193                    created_at_epoch,
8194                ) {
8195                    Ok((registration, state)) => (registration, Some(state)),
8196                    Err(error) => return Err(error.into()),
8197                }
8198            } else {
8199                (
8200                    control.register_worktree(
8201                        &alias,
8202                        &repository.common_directory,
8203                        &entry.administrative_directory,
8204                        &administrative_identity,
8205                        root,
8206                        project_instance_id,
8207                        created_at_epoch,
8208                    )?,
8209                    None,
8210                )
8211            };
8212            Self::encode_named_payload(
8213                MCP_PAYLOAD_WORKTREE,
8214                &McpWorktreeMutationReport {
8215                    operation: McpWorktreeMutationOperation::Add,
8216                    status: McpWorktreeMutationStatus::Registered,
8217                    selector: Some(Self::worktree_candidate_selector(&entry)),
8218                    alias: Some(alias.to_string()),
8219                    root: lossless_project_root_display(root),
8220                    path_display: Some(
8221                        if root.to_str().is_some()
8222                            && repository.common_directory.to_str().is_some()
8223                            && entry.administrative_directory.to_str().is_some()
8224                        {
8225                            McpWorktreePathDisplayState::Available
8226                        } else {
8227                            McpWorktreePathDisplayState::Unavailable
8228                        },
8229                    ),
8230                    registration_id: Some(registration.registration_id),
8231                    telemetry_sync,
8232                    candidates: Vec::new(),
8233                    blocker,
8234                    git_unchanged: true,
8235                    files_unchanged: true,
8236                },
8237            )
8238        })())
8239    }
8240
8241    /// Final-sync and retire one `ProjectAtlas` alias without touching Git or files.
8242    #[tool(
8243        name = "atlas_worktree_remove",
8244        description = "Final-sync and retire one ProjectAtlas worktree alias while preserving retained token totals and leaving Git, source files, .projectatlas, and SQLite files untouched."
8245    )]
8246    fn atlas_worktree_remove(
8247        &self,
8248        Parameters(params): Parameters<AtlasWorktreeRemoveParams>,
8249    ) -> McpToolTextResult {
8250        Self::as_mcp_text((|| {
8251            let alias = WorktreeAlias::parse(params.worktree.trim())?;
8252            let repository = self.control_git_repository()?;
8253            let control = Self::open_existing_mut_store(&self.control_state, &self.control_state)?;
8254            let registration = control.worktree_registration(&alias)?;
8255            let mut blocker = None;
8256            let entry = repository.worktrees.iter().find(|entry| {
8257                Self::worktree_administrative_path_identity(entry).is_some_and(|identity| {
8258                    identity == registration.git_administrative_directory_identity
8259                })
8260            });
8261            let entry = match entry {
8262                Some(entry) if matches!(entry.state, GitWorktreeState::Invalid { .. }) => {
8263                    Some(entry)
8264                }
8265                Some(entry) => match git_administrative_identity(&entry.administrative_directory) {
8266                    Ok(identity) if identity == registration.git_administrative_identity => {
8267                        Some(entry)
8268                    }
8269                    Ok(_) => {
8270                        blocker = Some(MCP_ERROR_WORKTREE_LIFECYCLE_CHANGED.to_string());
8271                        None
8272                    }
8273                    Err(error) => {
8274                        blocker = Some(error.to_string());
8275                        None
8276                    }
8277                },
8278                None => None,
8279            };
8280            let retired_at_epoch = Self::current_epoch_seconds()?;
8281            let retained_path_display =
8282                Self::missing_registered_worktree_row(&registration).path_display;
8283            let (root_display, active_root) = match entry.map(|entry| &entry.state) {
8284                Some(GitWorktreeState::Active { root, .. }) => {
8285                    (lossless_project_root_display(root), Some(root.as_path()))
8286                }
8287                Some(GitWorktreeState::Invalid { issue }) => {
8288                    return Err(CliError::InvalidInput(format!(
8289                        "cannot retire worktree '{}' while its Git evidence is invalid at '{}': {:?}",
8290                        alias,
8291                        normalize_native_path_display(&issue.path),
8292                        issue.kind
8293                    )));
8294                }
8295                Some(GitWorktreeState::Missing { .. }) | None => {
8296                    blocker
8297                        .get_or_insert_with(|| MCP_WORKTREE_MISSING_RETENTION_REASON.to_string());
8298                    (registration.last_root_identity.display_string().ok(), None)
8299                }
8300            };
8301            let (retired, telemetry_sync, final_blocker) = Self::retire_registered_worktree(
8302                &control,
8303                &registration,
8304                active_root,
8305                retired_at_epoch,
8306                blocker,
8307            )?;
8308            Self::encode_named_payload(
8309                MCP_PAYLOAD_WORKTREE,
8310                &McpWorktreeMutationReport {
8311                    operation: McpWorktreeMutationOperation::Remove,
8312                    status: McpWorktreeMutationStatus::Retired,
8313                    selector: entry.map(Self::worktree_candidate_selector),
8314                    alias: Some(alias.to_string()),
8315                    root: root_display,
8316                    path_display: Some(entry.map_or_else(
8317                        || retained_path_display,
8318                        |entry| match &entry.state {
8319                            GitWorktreeState::Active { root, .. }
8320                                if root.to_str().is_some()
8321                                    && repository.common_directory.to_str().is_some()
8322                                    && entry.administrative_directory.to_str().is_some() =>
8323                            {
8324                                McpWorktreePathDisplayState::Available
8325                            }
8326                            GitWorktreeState::Missing { .. } => retained_path_display,
8327                            GitWorktreeState::Active { .. } | GitWorktreeState::Invalid { .. } => {
8328                                McpWorktreePathDisplayState::Unavailable
8329                            }
8330                        },
8331                    )),
8332                    registration_id: Some(retired.registration_id),
8333                    telemetry_sync,
8334                    candidates: Vec::new(),
8335                    blocker: final_blocker,
8336                    git_unchanged: true,
8337                    files_unchanged: true,
8338                },
8339            )
8340        })())
8341    }
8342
8343    /// Initialize a `ProjectAtlas` project-local config surface.
8344    #[tool(
8345        name = "atlas_init",
8346        description = "Initialize ProjectAtlas project-local config, database, host MCP configs, scan/index, and purpose handoff."
8347    )]
8348    fn atlas_init(&self, Parameters(params): Parameters<AtlasInitParams>) -> McpToolTextResult {
8349        Self::as_mcp_text((|| {
8350            let state = self.init_project_root(params.project_path, params.worktree)?;
8351            let config_path = init_config_path(&state.root, state.config_path.as_deref());
8352            let mut report = self.run_registered_worktree_init(
8353                &state,
8354                &config_path,
8355                &InitBootstrapOptions {
8356                    no_scan: params.no_scan.unwrap_or(false),
8357                    force_rescan: params.force_rescan.unwrap_or(false),
8358                    text_index_max_bytes: params.text_index_max_bytes,
8359                },
8360            )?;
8361            crate::write_init_mcp_config_files(
8362                &mut report,
8363                &state.root.join(PROJECTATLAS_DIR_NAME),
8364                &state.db_path,
8365                &config_path,
8366                false,
8367            );
8368            Self::encode_named_payload(MCP_PAYLOAD_INIT, &report)
8369        })())
8370    }
8371
8372    /// Return the current map and optionally write a JSON sidecar.
8373    #[tool(
8374        name = "atlas_map",
8375        description = "Return the current ProjectAtlas map; optionally write a JSON sidecar."
8376    )]
8377    fn atlas_map(&self, Parameters(params): Parameters<AtlasMapParams>) -> McpToolTextResult {
8378        Self::as_mcp_text((|| {
8379            let state = self.admin_project_root(params.project_path, params.worktree)?;
8380            Self::build_map_report(
8381                &state,
8382                params.json.unwrap_or(false),
8383                params.force.unwrap_or(false),
8384            )
8385        })())
8386    }
8387
8388    /// Return root/DB/config diagnostics.
8389    #[tool(
8390        name = "atlas_root",
8391        description = "Show or verify ProjectAtlas root, DB, config, and runtime identity."
8392    )]
8393    fn atlas_root(&self, Parameters(params): Parameters<AtlasRootParams>) -> McpToolTextResult {
8394        Self::as_mcp_text((|| {
8395            if let Some(control_root) = params.control_root.as_deref() {
8396                if params.project_path.is_some()
8397                    || params.worktree.is_some()
8398                    || params.verify.unwrap_or(false)
8399                {
8400                    return Err(CliError::InvalidInput(
8401                        MCP_ERROR_ROOT_CONTROL_CONFLICT.to_owned(),
8402                    ));
8403                }
8404                let report = build_repository_control_report(Path::new(control_root))?;
8405                return Ok(render_repository_control_report(&report));
8406            }
8407            let state = self.admin_project_root(params.project_path, params.worktree)?;
8408            let admission =
8409                crate::runtime::preflight_managed_database(&state.db_path, &state.root)?;
8410            let database =
8411                crate::runtime::admitted_database_path(&state.db_path, admission.as_ref());
8412            let report = build_root_report(database, state.config_path.as_deref())?;
8413            if params.verify.unwrap_or(false) && report.verified {
8414                let _admission =
8415                    crate::runtime::preflight_managed_database(&state.db_path, &state.root)?;
8416                verify_project_database(database, &state.root)?;
8417            }
8418            Self::with_selected_project_audit(
8419                &state,
8420                state.worktree.is_some(),
8421                render_root_report(&report),
8422            )
8423        })())
8424    }
8425
8426    /// Bind, move, or detach a project root and then make it active.
8427    #[tool(
8428        name = "atlas_root_set",
8429        description = "Bind a repository root, generate project-local MCP configs, and make it active for later MCP calls."
8430    )]
8431    fn atlas_root_set(
8432        &self,
8433        Parameters(params): Parameters<AtlasRootSetParams>,
8434    ) -> McpToolTextResult {
8435        Self::as_mcp_text((|| {
8436            let root = canonical_project_root(Path::new(&params.root))?;
8437            let report = crate::bind_project_root(
8438                &root,
8439                params.transition.unwrap_or(RootTransition::Bind),
8440                params.nearest_project.unwrap_or(false),
8441            )?;
8442            let state = Self::project_state_from_root(&root)?;
8443            self.set_active_project_state(state)?;
8444            Ok(render_root_report(&report))
8445        })())
8446    }
8447
8448    /// Return the effective `ProjectAtlas` config.
8449    #[tool(
8450        name = "atlas_config",
8451        description = "Return the effective ProjectAtlas scan, purpose, and output configuration."
8452    )]
8453    fn atlas_config(
8454        &self,
8455        Parameters(params): Parameters<AtlasProjectParams>,
8456    ) -> McpToolTextResult {
8457        Self::as_mcp_text((|| {
8458            let state = self.admin_project_root(params.project_path, params.worktree)?;
8459            let report = effective_config_report(&Self::load_config_for_state(&state)?);
8460            Self::encode_named_payload(MCP_PAYLOAD_CONFIG, &report)
8461        })())
8462    }
8463
8464    /// Return the effective `ProjectAtlas` ignore policy.
8465    #[tool(
8466        name = "atlas_ignore_list",
8467        description = "List effective ProjectAtlas manual ignore policy and inherited .gitignore status."
8468    )]
8469    fn atlas_ignore_list(
8470        &self,
8471        Parameters(params): Parameters<AtlasProjectParams>,
8472    ) -> McpToolTextResult {
8473        Self::as_mcp_text((|| {
8474            let state = self.admin_project_root(params.project_path, params.worktree)?;
8475            let report = list_ignore_entries(state.config_path.as_deref(), &state.root)?;
8476            Self::encode_named_payload(MCP_PAYLOAD_IGNORE, &report)
8477        })())
8478    }
8479
8480    /// Create a project-root `.gitignore` when it is absent.
8481    #[tool(
8482        name = "atlas_ignore_init_gitignore",
8483        description = "Create a project-root .gitignore when it is missing."
8484    )]
8485    fn atlas_ignore_init_gitignore(
8486        &self,
8487        Parameters(params): Parameters<AtlasProjectParams>,
8488    ) -> McpToolTextResult {
8489        Self::as_mcp_text((|| {
8490            let state = self.admin_project_root(params.project_path, params.worktree)?;
8491            let report = init_gitignore(state.config_path.as_deref(), &state.root)?;
8492            Self::encode_named_payload(MCP_PAYLOAD_GITIGNORE, &report)
8493        })())
8494    }
8495
8496    /// Add a manual `ProjectAtlas` ignore entry.
8497    #[tool(
8498        name = "atlas_ignore_add",
8499        description = "Add one manual ProjectAtlas ignore entry to the selected project's config."
8500    )]
8501    fn atlas_ignore_add(
8502        &self,
8503        Parameters(params): Parameters<AtlasIgnoreMutationParams>,
8504    ) -> McpToolTextResult {
8505        Self::as_mcp_text((|| {
8506            let state = self.admin_project_root(params.project_path, params.worktree)?;
8507            let kind = Self::parse_ignore_kind(params.kind.as_deref(), true)?.ok_or_else(|| {
8508                CliError::InvalidInput(MCP_ERROR_IGNORE_KIND_REQUIRED_FOR_ADD.to_owned())
8509            })?;
8510            let report = add_ignore_entry(
8511                state.config_path.as_deref(),
8512                &state.root,
8513                kind,
8514                &params.value,
8515            )?;
8516            Self::encode_named_payload(MCP_PAYLOAD_IGNORE, &report)
8517        })())
8518    }
8519
8520    /// Remove a manual `ProjectAtlas` ignore entry.
8521    #[tool(
8522        name = "atlas_ignore_remove",
8523        description = "Remove one manual ProjectAtlas ignore entry from the selected project's config."
8524    )]
8525    fn atlas_ignore_remove(
8526        &self,
8527        Parameters(params): Parameters<AtlasIgnoreMutationParams>,
8528    ) -> McpToolTextResult {
8529        Self::as_mcp_text((|| {
8530            let state = self.admin_project_root(params.project_path, params.worktree)?;
8531            let kind = Self::parse_ignore_kind(params.kind.as_deref(), false)?;
8532            let report = remove_ignore_entry(
8533                state.config_path.as_deref(),
8534                &state.root,
8535                kind,
8536                &params.value,
8537            )?;
8538            Self::encode_named_payload(MCP_PAYLOAD_IGNORE, &report)
8539        })())
8540    }
8541
8542    /// Scan a repository, import purpose metadata, rebuild symbols, and return an overview.
8543    #[tool(
8544        name = "atlas_scan",
8545        description = "Scan repository structure, import ProjectAtlas purpose metadata, rebuild symbols, and return a TOON overview."
8546    )]
8547    fn atlas_scan(&self, Parameters(params): Parameters<AtlasScanParams>) -> McpToolTextResult {
8548        Self::as_mcp_text((|| {
8549            let nearest_project = self.nearest_project_enabled(params.nearest_project);
8550            let background = params.background.unwrap_or(false);
8551            let (state, path) = if background {
8552                self.background_state_and_root_path(
8553                    params.project_path,
8554                    params.worktree,
8555                    params.path,
8556                    nearest_project,
8557                )?
8558            } else {
8559                self.state_and_root_path(
8560                    params.project_path,
8561                    params.worktree,
8562                    params.path,
8563                    nearest_project,
8564                )?
8565            };
8566            let symbol_options = SymbolBuildOptions::new(
8567                params.max_bytes.unwrap_or(MAX_SYMBOL_FILE_BYTES),
8568                params.max_workers,
8569                params.timeout_seconds,
8570            );
8571            let text_index_max_bytes = params.text_index_max_bytes;
8572            if background {
8573                let control_state = self.control_state.clone();
8574                let task = self.start_index_task(
8575                    McpTaskOperation::Scan,
8576                    symbol_options,
8577                    MCP_TOOL_ATLAS_OVERVIEW,
8578                    move |control, symbol_options| {
8579                        let plan = ScanRuntimePlan::for_path_controlled(
8580                            state.config_path.as_deref(),
8581                            &path,
8582                            text_index_max_bytes,
8583                            control,
8584                        )?;
8585                        let mut store = Self::open_mut_store(&state, &control_state)?;
8586                        run_scan_pipeline_controlled(&mut store, &plan, &symbol_options, control)?;
8587                        Ok(())
8588                    },
8589                )?;
8590                return Self::encode_named_payload(MCP_PAYLOAD_TASK_START, &task);
8591            }
8592            let control = index_work_control(&symbol_options);
8593            let plan = ScanRuntimePlan::for_path_controlled(
8594                state.config_path.as_deref(),
8595                &path,
8596                text_index_max_bytes,
8597                &control,
8598            )?;
8599            let mut store = Self::open_mut_store(&state, &self.control_state)?;
8600            let report =
8601                run_scan_pipeline_controlled(&mut store, &plan, &symbol_options, &control)?;
8602            Self::encode_named_payload(MCP_PAYLOAD_SCAN, &report)
8603        })())
8604    }
8605
8606    /// Render one verified overview response with optional request cancellation.
8607    fn atlas_overview_response(
8608        &self,
8609        params: AtlasProjectParams,
8610        context: Option<RequestContext<RoleServer>>,
8611    ) -> McpToolTextResult {
8612        Self::as_mcp_text((|| {
8613            let state = self.state_for_target(params.project_path, params.worktree)?;
8614            self.with_fresh_string_and_usage_for_request(&state, context, |store, stamp| {
8615                let overview = store.overview()?;
8616                let toon = render_overview(&overview);
8617                let usage = Self::telemetry_enabled()
8618                    .then(|| self.estimated_source_tokens_cached(&state, store, &stamp, None, None))
8619                    .and_then(Result::ok)
8620                    .map(|baseline_tokens| {
8621                        McpUsageIntent::directory_walk(
8622                            MCP_EVENT_ATLAS_OVERVIEW,
8623                            None,
8624                            None,
8625                            baseline_tokens,
8626                        )
8627                    });
8628                Ok((toon, usage))
8629            })
8630        })())
8631    }
8632
8633    /// Return the indexed repository overview.
8634    #[tool(
8635        name = "atlas_overview",
8636        description = "Return a compact TOON overview of indexed files, folders, and purpose coverage."
8637    )]
8638    fn atlas_overview(
8639        &self,
8640        Parameters(params): Parameters<AtlasProjectParams>,
8641        context: RequestContext<RoleServer>,
8642    ) -> McpToolTextResult {
8643        self.atlas_overview_response(params, Some(context))
8644    }
8645
8646    /// Rank folders before an agent chooses a work area.
8647    #[tool(
8648        name = "atlas_folders",
8649        description = "Rank repository folders by query and purpose so agents choose a work area before opening files."
8650    )]
8651    fn atlas_folders(
8652        &self,
8653        Parameters(params): Parameters<AtlasQueryParams>,
8654        context: RequestContext<RoleServer>,
8655    ) -> McpToolTextResult {
8656        self.atlas_folders_response(params, Some(context))
8657    }
8658
8659    /// Build the folders response with optional request telemetry context.
8660    fn atlas_folders_response(
8661        &self,
8662        params: AtlasQueryParams,
8663        context: Option<RequestContext<RoleServer>>,
8664    ) -> McpToolTextResult {
8665        Self::as_mcp_text((|| {
8666            let state = self.state_for_target(params.project_path, params.worktree)?;
8667            let query = Self::query_or_empty(params.query);
8668            self.with_fresh_string_and_usage_for_request(&state, context, |store, stamp| {
8669                let selected =
8670                    ranked_folder_nodes_with_reasons(store, &query, params.limit.unwrap_or(10))?;
8671                let toon = render_ranked_nodes(NODE_LABEL_FOLDERS, &selected);
8672                let usage = Self::telemetry_enabled()
8673                    .then(|| self.estimated_source_tokens_cached(&state, store, &stamp, None, None))
8674                    .and_then(Result::ok)
8675                    .map(|baseline_tokens| {
8676                        McpUsageIntent::directory_walk(
8677                            MCP_EVENT_ATLAS_FOLDERS,
8678                            None,
8679                            Some(query.clone()),
8680                            baseline_tokens,
8681                        )
8682                    });
8683                Ok((toon, usage))
8684            })
8685        })())
8686    }
8687
8688    /// Rank files after an agent has chosen a folder or query.
8689    #[tool(
8690        name = "atlas_files",
8691        description = "Rank repository files by query, purpose, optional folder, and optional indexed text fallback before an agent opens source."
8692    )]
8693    fn atlas_files(
8694        &self,
8695        Parameters(params): Parameters<AtlasFilesParams>,
8696        context: RequestContext<RoleServer>,
8697    ) -> McpToolTextResult {
8698        self.atlas_files_response(params, Some(context))
8699    }
8700
8701    /// Build the files response with optional request telemetry context.
8702    fn atlas_files_response(
8703        &self,
8704        params: AtlasFilesParams,
8705        context: Option<RequestContext<RoleServer>>,
8706    ) -> McpToolTextResult {
8707        Self::as_mcp_text((|| {
8708            let content_selection = parse_content_selection(params.content_selection.as_deref())?;
8709            let nearest_project = self.nearest_project_enabled(params.nearest_project);
8710            let (state, folder_filter, routed_project) = self.state_and_optional_folder_filter(
8711                params.project_path.as_deref(),
8712                params.worktree.as_deref(),
8713                params.folder.as_deref(),
8714                nearest_project,
8715            )?;
8716            let query = Self::query_or_empty(params.query);
8717            self.with_fresh_string_and_usage_for_request(&state, context, |store, stamp| {
8718                let selected = classified_ranked_file_nodes_with_reasons(
8719                    store,
8720                    &query,
8721                    folder_filter.as_deref(),
8722                    params.file_pattern.as_deref(),
8723                    params.limit.unwrap_or(10),
8724                    params.include_content.unwrap_or(false),
8725                    content_selection,
8726                )?;
8727                let toon = Self::with_selected_project_audit(
8728                    &state,
8729                    routed_project,
8730                    encode_agent_payload(&serde_json::json!({
8731                        NODE_LABEL_FILES: render_classified_ranked_file_rows(&selected),
8732                    })),
8733                )?;
8734                let usage = Self::telemetry_enabled()
8735                    .then(|| {
8736                        self.estimated_source_tokens_cached(
8737                            &state,
8738                            store,
8739                            &stamp,
8740                            folder_filter.as_deref(),
8741                            params.file_pattern.as_deref(),
8742                        )
8743                    })
8744                    .and_then(Result::ok)
8745                    .map(|baseline_tokens| {
8746                        McpUsageIntent::estimate(
8747                            MCP_EVENT_ATLAS_FILES,
8748                            params
8749                                .file_pattern
8750                                .clone()
8751                                .or_else(|| folder_filter.clone()),
8752                            Some(query.clone()),
8753                            baseline_tokens,
8754                        )
8755                    });
8756                Ok((toon, usage))
8757            })
8758        })())
8759    }
8760
8761    /// Recommend the next indexed folders, files, and inspection commands.
8762    #[tool(
8763        name = "atlas_next",
8764        description = "Recommend top indexed folders/files with reasons and deterministic follow-up commands for a task query."
8765    )]
8766    fn atlas_next(
8767        &self,
8768        Parameters(params): Parameters<AtlasNextParams>,
8769        context: RequestContext<RoleServer>,
8770    ) -> McpToolTextResult {
8771        Self::as_mcp_text((|| {
8772            let content_selection = parse_content_selection(params.content_selection.as_deref())?;
8773            let state = self.state_for_target(params.project_path, params.worktree)?;
8774            let query = Self::query_or_empty(params.query);
8775            self.with_fresh_string_and_usage_for_request(&state, Some(context), |store, stamp| {
8776                let report = next_step_report_with_selection(
8777                    store,
8778                    &query,
8779                    params.limit,
8780                    content_selection,
8781                )?;
8782                let payload = next_step_report_payload(&report);
8783                let toon = Self::encode_named_payload(MCP_PAYLOAD_NEXT, &payload)?;
8784                let usage = Self::telemetry_enabled()
8785                    .then(|| self.estimated_source_tokens_cached(&state, store, &stamp, None, None))
8786                    .and_then(Result::ok)
8787                    .map(|baseline_tokens| {
8788                        McpUsageIntent::directory_walk(
8789                            MCP_EVENT_ATLAS_NEXT,
8790                            None,
8791                            Some(query.clone()),
8792                            baseline_tokens,
8793                        )
8794                    });
8795                Ok((toon, usage))
8796            })
8797        })())
8798    }
8799
8800    /// Build a compact file outline.
8801    #[tool(
8802        name = "atlas_outline",
8803        description = "Return compact TOON outline and preview context for a selected file."
8804    )]
8805    fn atlas_outline(
8806        &self,
8807        Parameters(params): Parameters<AtlasOutlineParams>,
8808        context: RequestContext<RoleServer>,
8809    ) -> McpToolTextResult {
8810        Self::as_mcp_text((|| {
8811            let nearest_project = self.nearest_project_enabled(params.nearest_project);
8812            let resolved = self.state_and_file_key(
8813                params.project_path.as_deref(),
8814                params.worktree.as_deref(),
8815                &params.file,
8816                nearest_project,
8817            )?;
8818            let state = resolved.state;
8819            self.with_fresh_string_and_usage_for_request(&state, Some(context), |store, _stamp| {
8820                let file_key = validated_indexed_file_key(store, Path::new(&resolved.key))?;
8821                let content = read_indexed_file_content(store, &file_key)?;
8822                let language = store
8823                    .load_node_by_path(&file_key)?
8824                    .and_then(|node| node.node.language);
8825                let outline =
8826                    build_outline(&file_key, language, &content, params.lines.unwrap_or(12));
8827                let toon = Self::with_selected_project_audit(
8828                    &state,
8829                    resolved.routed_project,
8830                    render_outline(&outline),
8831                )?;
8832                let usage = Some(McpUsageIntent::text(
8833                    MCP_EVENT_ATLAS_OUTLINE,
8834                    Some(file_key),
8835                    content,
8836                ));
8837                Ok((toon, usage))
8838            })
8839        })())
8840    }
8841
8842    /// Render one verified structured file-summary response.
8843    fn atlas_file_summary_response(
8844        &self,
8845        params: &AtlasFileSummaryParams,
8846        context: Option<RequestContext<RoleServer>>,
8847    ) -> McpToolTextResult {
8848        Self::as_mcp_text((|| {
8849            let content_selection = parse_content_selection(params.content_selection.as_deref())?;
8850            let nearest_project = self.nearest_project_enabled(params.nearest_project);
8851            let resolved = self.state_and_file_key(
8852                params.project_path.as_deref(),
8853                params.worktree.as_deref(),
8854                &params.file,
8855                nearest_project,
8856            )?;
8857            let state = resolved.state;
8858            self.with_fresh_string_and_usage_for_request(&state, context, |store, _stamp| {
8859                let file_key = validated_indexed_file_key(store, Path::new(&resolved.key))?;
8860                let content = read_indexed_file_content(store, &file_key)?;
8861                let report = build_file_summary_from_source_with_selection(
8862                    store,
8863                    Path::new(&file_key),
8864                    params.limit.unwrap_or(DEFAULT_FILE_SUMMARY_LIMIT),
8865                    &content,
8866                    content_selection,
8867                )?;
8868                let rendered = if params.compact.unwrap_or(false) {
8869                    encode_agent_payload(&McpFileSummaryPayload {
8870                        file_summary: McpFileSummary::from(&report),
8871                    })
8872                } else {
8873                    render_file_summary(&report)
8874                };
8875                let toon =
8876                    Self::with_selected_project_audit(&state, resolved.routed_project, rendered)?;
8877                let usage = Some(McpUsageIntent::text(
8878                    MCP_EVENT_ATLAS_FILE_SUMMARY,
8879                    Some(report.file_path),
8880                    content,
8881                ));
8882                Ok((toon, usage))
8883            })
8884        })())
8885    }
8886
8887    /// Return deterministic structured file intelligence from the deep index.
8888    #[tool(
8889        name = "atlas_file_summary",
8890        description = "Return structured TOON file intelligence: file purpose, content summary, imports, symbols, line ranges, and calls."
8891    )]
8892    fn atlas_file_summary(
8893        &self,
8894        Parameters(params): Parameters<AtlasFileSummaryParams>,
8895        context: RequestContext<RoleServer>,
8896    ) -> McpToolTextResult {
8897        self.atlas_file_summary_response(&params, Some(context))
8898    }
8899
8900    /// Search selected indexed files with optional context lines.
8901    #[tool(
8902        name = "atlas_search",
8903        description = "Search indexed files with literal, regex, or fuzzy matching, file filters, pagination, and TOON results."
8904    )]
8905    fn atlas_search(
8906        &self,
8907        Parameters(params): Parameters<AtlasSearchParams>,
8908        context: RequestContext<RoleServer>,
8909    ) -> McpToolTextResult {
8910        Self::as_mcp_text((|| {
8911            let content_selection = parse_content_selection(params.content_selection.as_deref())?;
8912            let state =
8913                self.state_for_target(params.project_path.clone(), params.worktree.clone())?;
8914            self.with_fresh_string_and_usage_controlled_for_request(
8915                &state,
8916                Some(context),
8917                |store, _stamp, control| {
8918                    let report = search_indexed_files_with_control(
8919                        store,
8920                        &SearchQuery {
8921                            pattern: &params.pattern,
8922                            regex: params.regex.unwrap_or(false),
8923                            fuzzy: params.fuzzy.unwrap_or(false),
8924                            case_sensitive: params.case_sensitive.unwrap_or(false),
8925                            file_pattern: params.file_pattern.as_deref(),
8926                            context_lines: params.context_lines.unwrap_or(0),
8927                            start_index: params.start_index.unwrap_or(0),
8928                            limit: params.limit.unwrap_or(20),
8929                            content_selection,
8930                            retrieval_mode: params.retrieval_mode.unwrap_or_default().into(),
8931                        },
8932                        Some(control),
8933                    )?;
8934                    let toon = render_search_report(&report);
8935                    let usage = Some(McpUsageIntent::estimate(
8936                        MCP_EVENT_ATLAS_SEARCH,
8937                        params.file_pattern.clone(),
8938                        Some(params.pattern.clone()),
8939                        byte_count_to_tokens(report.searched_bytes),
8940                    ));
8941                    Ok((toon, usage))
8942                },
8943            )
8944        })())
8945    }
8946
8947    /// Return an exact line or symbol slice from a selected file.
8948    #[tool(
8949        name = "atlas_slice",
8950        description = "Return exact source for a selected line range or indexed symbol, after folder/file orientation."
8951    )]
8952    fn atlas_slice(
8953        &self,
8954        Parameters(params): Parameters<AtlasSliceParams>,
8955        context: RequestContext<RoleServer>,
8956    ) -> McpToolTextResult {
8957        Self::as_mcp_text((|| {
8958            let content_selection = parse_content_selection(params.content_selection.as_deref())?;
8959            let nearest_project = self.nearest_project_enabled(params.nearest_project);
8960            let resolved = self.state_and_file_key(
8961                params.project_path.as_deref(),
8962                params.worktree.as_deref(),
8963                &params.file,
8964                nearest_project,
8965            )?;
8966            let state = resolved.state;
8967            self.with_fresh_string_and_usage_for_request(&state, Some(context), |store, _stamp| {
8968                let file_key = validated_indexed_file_key(store, Path::new(&resolved.key))?;
8969                let file = PathBuf::from(&file_key);
8970                let content = read_indexed_file_content(store, &file_key)?;
8971                let output_budget = CodeSliceBudget::new(
8972                    params
8973                        .output_bytes
8974                        .unwrap_or(CodeSliceBudget::DEFAULT_OUTPUT_BYTES),
8975                )?;
8976                let report = if let Some(symbol) = params.symbol.as_ref() {
8977                    read_symbol_slice_from_source_bounded_with_selection(
8978                        store,
8979                        &file,
8980                        &SymbolSliceSelector {
8981                            name: symbol,
8982                            parent: params.symbol_parent.as_deref().and_then(nonempty_str),
8983                            kind: params.symbol_kind.as_deref().and_then(nonempty_str),
8984                            signature: params.symbol_signature.as_deref().and_then(nonempty_str),
8985                            line: params.symbol_line,
8986                        },
8987                        &content,
8988                        output_budget,
8989                        content_selection,
8990                    )?
8991                } else {
8992                    if params
8993                        .symbol_parent
8994                        .as_deref()
8995                        .and_then(nonempty_str)
8996                        .is_some()
8997                        || params
8998                            .symbol_kind
8999                            .as_deref()
9000                            .and_then(nonempty_str)
9001                            .is_some()
9002                        || params
9003                            .symbol_signature
9004                            .as_deref()
9005                            .and_then(nonempty_str)
9006                            .is_some()
9007                        || params.symbol_line.is_some()
9008                    {
9009                        return Err(CliError::InvalidInput(
9010                            SYMBOL_DISAMBIGUATOR_WITHOUT_SYMBOL_ERROR.to_string(),
9011                        ));
9012                    }
9013                    let start_line = params.start_line.ok_or_else(|| {
9014                        CliError::InvalidInput(START_LINE_REQUIRED_ERROR.to_string())
9015                    })?;
9016                    read_indexed_code_slice_from_source_bounded_with_selection(
9017                        store,
9018                        &file,
9019                        start_line,
9020                        params.end_line,
9021                        &content,
9022                        output_budget,
9023                        content_selection,
9024                    )?
9025                };
9026                let toon = report.fit_output(|report| {
9027                    Self::with_selected_project_audit(
9028                        &state,
9029                        resolved.routed_project,
9030                        render_code_slice(report),
9031                    )
9032                })?;
9033                let usage = Some(McpUsageIntent::text(
9034                    MCP_EVENT_ATLAS_SLICE,
9035                    Some(report.slice().path.clone()),
9036                    content,
9037                ));
9038                Ok((toon, usage))
9039            })
9040        })())
9041    }
9042
9043    /// Rebuild symbol graphs for indexed files.
9044    #[tool(
9045        name = "atlas_symbols_build",
9046        description = "Rebuild ProjectAtlas symbol graphs for indexed files and return a TOON build report."
9047    )]
9048    fn atlas_symbols_build(
9049        &self,
9050        Parameters(params): Parameters<AtlasScanParams>,
9051    ) -> McpToolTextResult {
9052        Self::as_mcp_text((|| {
9053            let nearest_project = self.nearest_project_enabled(params.nearest_project);
9054            let background = params.background.unwrap_or(false);
9055            let (state, path) = if background {
9056                self.background_state_and_root_path(
9057                    params.project_path,
9058                    params.worktree,
9059                    params.path,
9060                    nearest_project,
9061                )?
9062            } else {
9063                self.state_and_root_path(
9064                    params.project_path,
9065                    params.worktree,
9066                    params.path,
9067                    nearest_project,
9068                )?
9069            };
9070            let options = SymbolBuildOptions::new(
9071                params.max_bytes.unwrap_or(MAX_SYMBOL_FILE_BYTES),
9072                params.max_workers,
9073                params.timeout_seconds,
9074            );
9075            let text_index_max_bytes = params.text_index_max_bytes;
9076            if background {
9077                let control_state = self.control_state.clone();
9078                let task = self.start_index_task(
9079                    McpTaskOperation::SymbolsBuild,
9080                    options,
9081                    MCP_TOOL_ATLAS_SYMBOLS,
9082                    move |control, options| {
9083                        let plan = ScanRuntimePlan::for_path_controlled(
9084                            state.config_path.as_deref(),
9085                            &path,
9086                            text_index_max_bytes,
9087                            control,
9088                        )?;
9089                        let mut store = Self::open_mut_store(&state, &control_state)?;
9090                        run_symbol_build_pipeline_controlled(
9091                            &mut store, &plan, &options, None, control,
9092                        )?;
9093                        Ok(())
9094                    },
9095                )?;
9096                return Self::encode_named_payload(MCP_PAYLOAD_TASK_START, &task);
9097            }
9098            let control = index_work_control(&options);
9099            let plan = ScanRuntimePlan::for_path_controlled(
9100                state.config_path.as_deref(),
9101                &path,
9102                text_index_max_bytes,
9103                &control,
9104            )?;
9105            let mut store = Self::open_mut_store(&state, &self.control_state)?;
9106            let report =
9107                run_symbol_build_pipeline_controlled(&mut store, &plan, &options, None, &control)?;
9108            Self::encode_named_payload(MCP_PAYLOAD_SYMBOLS_BUILD, &report)
9109        })())
9110    }
9111
9112    /// Render one verified symbol-list response.
9113    fn atlas_symbols_response(
9114        &self,
9115        params: &AtlasSymbolsParams,
9116        context: Option<RequestContext<RoleServer>>,
9117    ) -> McpToolTextResult {
9118        Self::as_mcp_text((|| {
9119            let content_selection = parse_content_selection(params.content_selection.as_deref())?;
9120            let nearest_project = self.nearest_project_enabled(params.nearest_project);
9121            let (state, file, routed_project) = self.state_and_optional_file_key(
9122                params.project_path.as_deref(),
9123                params.worktree.as_deref(),
9124                params.file.as_deref(),
9125                nearest_project,
9126            )?;
9127            self.with_fresh_string_and_usage_for_request(&state, context, |store, _stamp| {
9128                let file = file
9129                    .as_deref()
9130                    .map(|path| validated_indexed_file_key(store, Path::new(path)))
9131                    .transpose()?;
9132                let symbols = store.load_classified_symbols(
9133                    file.as_deref(),
9134                    params.query.as_deref(),
9135                    content_selection,
9136                    params.limit.unwrap_or(50),
9137                )?;
9138                let toon = Self::with_selected_project_audit(
9139                    &state,
9140                    routed_project,
9141                    encode_agent_payload(&serde_json::json!({
9142                        NODE_LABEL_SYMBOLS: render_classified_symbol_rows(&symbols),
9143                    })),
9144                )?;
9145                let usage = Self::telemetry_enabled()
9146                    .then(|| {
9147                        estimated_source_tokens_for_paths(
9148                            store,
9149                            symbols
9150                                .iter()
9151                                .map(|classified| classified.symbol.path.as_str()),
9152                        )
9153                    })
9154                    .and_then(Result::ok)
9155                    .map(|baseline_tokens| {
9156                        McpUsageIntent::estimate(
9157                            MCP_EVENT_ATLAS_SYMBOLS,
9158                            file.clone(),
9159                            params.query.clone(),
9160                            baseline_tokens,
9161                        )
9162                    });
9163                Ok((toon, usage))
9164            })
9165        })())
9166    }
9167
9168    /// List indexed symbols.
9169    #[tool(
9170        name = "atlas_symbols",
9171        description = "List indexed symbols by optional file and query as compact TOON."
9172    )]
9173    fn atlas_symbols(
9174        &self,
9175        Parameters(params): Parameters<AtlasSymbolsParams>,
9176        context: RequestContext<RoleServer>,
9177    ) -> McpToolTextResult {
9178        self.atlas_symbols_response(&params, Some(context))
9179    }
9180
9181    /// Render the shared detailed/analysis contract through one selected store set.
9182    fn detailed_symbol_relations_response(
9183        state: &McpProjectState,
9184        routed_project: bool,
9185        file: &str,
9186        params: &AtlasSymbolRelationsParams,
9187        content_selection: ContentSelection,
9188        analysis: bool,
9189        stores: SymbolRelationStores<'_>,
9190        control: &IndexWorkControl,
9191    ) -> Result<(String, Option<McpUsageIntent>), CliError> {
9192        if params.query.is_some() {
9193            return Err(CliError::Service(ServiceError::InvalidInput(
9194                MCP_ERROR_DETAILED_RELATION_QUERY.to_string(),
9195            )));
9196        }
9197        let primary = stores.primary();
9198        let file = validated_indexed_file_key(primary, Path::new(file))?;
9199        let graph_file = RepositoryFilePath::new(Path::new(&file))
9200            .map_err(|error| CliError::Service(ServiceError::InvalidInput(error.to_string())))?;
9201        let anchor = if let Some(symbol) = params.symbol.as_ref() {
9202            if symbol.is_empty() {
9203                return Err(CliError::Service(ServiceError::InvalidInput(
9204                    MCP_ERROR_DETAILED_RELATION_SYMBOL.to_string(),
9205                )));
9206            }
9207            RelationAnchor::Symbol {
9208                file: graph_file,
9209                name: symbol.clone(),
9210                symbol_kind: params
9211                    .symbol_kind
9212                    .as_deref()
9213                    .and_then(nonempty_str)
9214                    .map(parse_symbol_kind)
9215                    .transpose()?,
9216                parent: params
9217                    .symbol_parent
9218                    .as_deref()
9219                    .and_then(nonempty_str)
9220                    .map(ToString::to_string),
9221                signature: params
9222                    .symbol_signature
9223                    .as_deref()
9224                    .and_then(nonempty_str)
9225                    .map(ToString::to_string),
9226            }
9227        } else {
9228            if params
9229                .symbol_parent
9230                .as_deref()
9231                .and_then(nonempty_str)
9232                .is_some()
9233                || params
9234                    .symbol_kind
9235                    .as_deref()
9236                    .and_then(nonempty_str)
9237                    .is_some()
9238                || params
9239                    .symbol_signature
9240                    .as_deref()
9241                    .and_then(nonempty_str)
9242                    .is_some()
9243            {
9244                return Err(CliError::Service(ServiceError::InvalidInput(
9245                    MCP_ERROR_DETAILED_RELATION_DISAMBIGUATOR.to_string(),
9246                )));
9247            }
9248            RelationAnchor::File { file: graph_file }
9249        };
9250        let policy = if analysis {
9251            projectatlas_service::RelationNavigationView::Analysis
9252        } else {
9253            projectatlas_service::RelationNavigationView::Detailed
9254        };
9255        let rows =
9256            u32::try_from(params.limit.unwrap_or(policy.default_rows())).map_err(|_overflow| {
9257                CliError::Service(ServiceError::InvalidInput(
9258                    MCP_ERROR_DETAILED_RELATION_LIMIT.to_string(),
9259                ))
9260            })?;
9261        let limits = GraphLimits::new(
9262            rows,
9263            params.occurrence_limit.unwrap_or(25),
9264            params.depth.unwrap_or(1),
9265            policy.output_bytes(params.output_bytes)?,
9266        )
9267        .map_err(|error| CliError::Service(ServiceError::InvalidInput(error.to_string())))?;
9268        let relations = DetailedRelationQuery {
9269            anchor,
9270            direction: parse_relation_direction(
9271                params
9272                    .direction
9273                    .as_deref()
9274                    .unwrap_or(MCP_SYMBOL_RELATION_DIRECTION_DEFAULT),
9275            )?,
9276            relation: params
9277                .relation
9278                .as_deref()
9279                .map(parse_coverage_relation)
9280                .transpose()?,
9281            minimum_confidence: parse_relation_confidence(
9282                params
9283                    .minimum_confidence
9284                    .as_deref()
9285                    .unwrap_or(MCP_SYMBOL_RELATION_CONFIDENCE_DEFAULT),
9286            )?,
9287            resolution: parse_relation_resolution(
9288                params
9289                    .resolution
9290                    .as_deref()
9291                    .unwrap_or(MCP_SYMBOL_RELATION_RESOLUTION_DEFAULT),
9292            )?,
9293            content_selection,
9294            include_occurrences: params.include_occurrences.unwrap_or(false),
9295            budget: DetailedRelationBudget::from_graph_limits(limits).with_aggregate_limits(
9296                params.edge_limit,
9297                params.node_limit,
9298                params.visited_limit,
9299                params.occurrence_total_limit,
9300                params.intermediate_bytes,
9301                params.deadline_ms,
9302            )?,
9303            cursor: params.cursor.clone(),
9304        };
9305        let usage = matches!(&stores, SymbolRelationStores::Single(_))
9306            .then(|| {
9307                Self::telemetry_enabled()
9308                    .then(|| {
9309                        estimated_source_tokens_for_paths(primary, std::iter::once(file.as_str()))
9310                    })
9311                    .and_then(Result::ok)
9312                    .map(|baseline_tokens| {
9313                        McpUsageIntent::estimate(
9314                            MCP_EVENT_ATLAS_SYMBOL_RELATIONS,
9315                            Some(file.clone()),
9316                            params.symbol.clone(),
9317                            baseline_tokens,
9318                        )
9319                    })
9320            })
9321            .flatten();
9322
9323        if analysis {
9324            let mode = match params
9325                .analysis_mode
9326                .as_deref()
9327                .unwrap_or(MCP_RELATION_ANALYSIS_MODE_ARCHITECTURE)
9328            {
9329                MCP_RELATION_ANALYSIS_MODE_ARCHITECTURE => RelationAnalysisMode::Architecture,
9330                MCP_RELATION_ANALYSIS_MODE_IMPACT => RelationAnalysisMode::Impact,
9331                MCP_RELATION_ANALYSIS_MODE_TRACE => RelationAnalysisMode::Trace,
9332                MCP_RELATION_ANALYSIS_MODE_ENTRYPOINT => RelationAnalysisMode::Entrypoint,
9333                _unsupported => {
9334                    return Err(CliError::Service(ServiceError::InvalidInput(
9335                        MCP_ERROR_UNSUPPORTED_ANALYSIS_MODE.to_string(),
9336                    )));
9337                }
9338            };
9339            let trace_target = relation_analysis_trace_target(primary, params)?;
9340            let vcs_explicit =
9341                params.vcs.is_some() || params.vcs_base.is_some() || params.vcs_head.is_some();
9342            let vcs = relation_analysis_vcs(params)?;
9343            if mode == RelationAnalysisMode::Entrypoint
9344                && matches!(&stores, SymbolRelationStores::Federated(_))
9345            {
9346                return Err(CliError::Service(ServiceError::InvalidInput(
9347                    MCP_ERROR_ENTRYPOINT_FEDERATED.to_string(),
9348                )));
9349            }
9350            if mode == RelationAnalysisMode::Entrypoint
9351                && params
9352                    .entrypoints
9353                    .as_ref()
9354                    .is_some_and(|items| !items.is_empty())
9355                && (params.symbol.is_some()
9356                    || params.symbol_parent.is_some()
9357                    || params.symbol_kind.is_some()
9358                    || params.symbol_signature.is_some())
9359            {
9360                return Err(CliError::Service(ServiceError::InvalidInput(
9361                    MCP_ERROR_ENTRYPOINT_SYMBOL_SELECTOR.to_string(),
9362                )));
9363            }
9364            let entrypoint_profile = if mode == RelationAnalysisMode::Entrypoint {
9365                let anchors = match params.entrypoints.as_ref() {
9366                    Some(values) if !values.is_empty() => values
9367                        .iter()
9368                        .map(|value| serde_json::from_str::<RelationAnchor>(value))
9369                        .collect::<Result<Vec<_>, _>>()
9370                        .map_err(|error| {
9371                            let mut message = MCP_ERROR_ENTRYPOINT_ANCHORS_PREFIX.to_string();
9372                            message.push_str(&error.to_string());
9373                            CliError::Service(ServiceError::InvalidInput(message))
9374                        })?,
9375                    _ => vec![relations.anchor.clone()],
9376                };
9377                let relation_families = match params.profile_relations.as_ref() {
9378                    Some(values) if !values.is_empty() => values
9379                        .iter()
9380                        .map(|value| parse_coverage_relation(value))
9381                        .collect::<Result<Vec<_>, _>>()?,
9382                    _ => GraphRelationKind::ALL.to_vec(),
9383                };
9384                Some(EntrypointProfile {
9385                    name: params
9386                        .profile_name
9387                        .clone()
9388                        .unwrap_or_else(|| MCP_ENTRYPOINT_PROFILE_DEFAULT_NAME.to_string()),
9389                    anchors,
9390                    relations: relation_families,
9391                })
9392            } else {
9393                if params.profile_name.is_some()
9394                    || params
9395                        .entrypoints
9396                        .as_ref()
9397                        .is_some_and(|items| !items.is_empty())
9398                    || params
9399                        .profile_relations
9400                        .as_ref()
9401                        .is_some_and(|items| !items.is_empty())
9402                {
9403                    return Err(CliError::Service(ServiceError::InvalidInput(
9404                        MCP_ERROR_ENTRYPOINT_CONTROLS_MODE.to_string(),
9405                    )));
9406                }
9407                None
9408            };
9409            let query = RelationAnalysisQuery {
9410                relations,
9411                mode,
9412                trace_target,
9413                vcs: (mode == RelationAnalysisMode::Impact || vcs_explicit).then_some(vcs),
9414                include_communities: params.include_communities.unwrap_or(false),
9415                include_cycles: params.include_cycles.unwrap_or(false),
9416                include_dead_code: params.include_dead_code.unwrap_or(false),
9417                entrypoint_profile,
9418            };
9419            let toon = match stores {
9420                SymbolRelationStores::Single(store) => {
9421                    let draft = load_relation_analysis(store, &query, Some(control))?;
9422                    draft
9423                        .fit_output(|report, control| {
9424                            Self::with_selected_project_audit_controlled(
9425                                state,
9426                                routed_project,
9427                                controlled_named_output(
9428                                    OutputFormat::Toon,
9429                                    MCP_PAYLOAD_SYMBOL_RELATIONS,
9430                                    report,
9431                                    control,
9432                                )?,
9433                                control,
9434                            )
9435                        })?
9436                        .1
9437                }
9438                SymbolRelationStores::Federated(stores) => {
9439                    let draft = load_federated_relation_analysis(stores, &query, Some(control))?;
9440                    draft
9441                        .fit_output(|report, control| {
9442                            Self::with_selected_project_audit_controlled(
9443                                state,
9444                                routed_project,
9445                                controlled_named_output(
9446                                    OutputFormat::Toon,
9447                                    MCP_PAYLOAD_SYMBOL_RELATIONS,
9448                                    report,
9449                                    control,
9450                                )?,
9451                                control,
9452                            )
9453                        })?
9454                        .1
9455                }
9456            };
9457            return Ok((toon, usage));
9458        }
9459
9460        let toon = match stores {
9461            SymbolRelationStores::Single(store) => {
9462                let draft = load_detailed_relation_page(store, &relations, Some(control))?;
9463                draft
9464                    .fit_output(Some(control), |report| {
9465                        let payload = if params.compact.unwrap_or(false) {
9466                            Self::encode_named_payload(
9467                                MCP_PAYLOAD_SYMBOL_RELATIONS,
9468                                &McpCompactDetailedRelationReport::new(report, &file, params),
9469                            )?
9470                        } else {
9471                            Self::encode_named_payload(MCP_PAYLOAD_SYMBOL_RELATIONS, report)?
9472                        };
9473                        Self::with_selected_project_audit(state, routed_project, payload)
9474                    })?
9475                    .1
9476            }
9477            SymbolRelationStores::Federated(stores) => {
9478                let draft = load_federated_detailed_relations(stores, &relations, Some(control))?;
9479                draft
9480                    .fit_output(Some(control), |report| {
9481                        let payload = if params.compact.unwrap_or(false) {
9482                            Self::encode_named_payload(
9483                                MCP_PAYLOAD_SYMBOL_RELATIONS,
9484                                &McpCompactFederatedDetailedRelationReport::new(
9485                                    report, &file, params,
9486                                ),
9487                            )?
9488                        } else {
9489                            Self::encode_named_payload(MCP_PAYLOAD_SYMBOL_RELATIONS, report)?
9490                        };
9491                        Self::with_selected_project_audit(state, routed_project, payload)
9492                    })?
9493                    .1
9494            }
9495        };
9496        Ok((toon, usage))
9497    }
9498
9499    /// Render one verified legacy or detailed symbol-relation response.
9500    fn atlas_symbol_relations_response(
9501        &self,
9502        params: &AtlasSymbolRelationsParams,
9503        context: Option<RequestContext<RoleServer>>,
9504    ) -> McpToolTextResult {
9505        Self::as_mcp_text((|| {
9506            let (detailed, analysis) = match params
9507                .view
9508                .as_deref()
9509                .unwrap_or(MCP_SYMBOL_RELATION_VIEW_DETAILED)
9510            {
9511                MCP_SYMBOL_RELATION_VIEW_LEGACY => (false, false),
9512                MCP_SYMBOL_RELATION_VIEW_DETAILED => (true, false),
9513                MCP_SYMBOL_RELATION_VIEW_ANALYSIS => (true, true),
9514                _unsupported => {
9515                    return Err(CliError::Service(ServiceError::InvalidInput(
9516                        MCP_ERROR_SYMBOL_RELATION_VIEW.to_string(),
9517                    )));
9518                }
9519            };
9520            if params.compact.unwrap_or(false) && (!detailed || analysis) {
9521                return Err(CliError::Service(ServiceError::InvalidInput(
9522                    MCP_ERROR_COMPACT_DETAILED_RELATION_VIEW.to_string(),
9523                )));
9524            }
9525            if !analysis && relation_analysis_controls_present(params) {
9526                return Err(CliError::Service(ServiceError::InvalidInput(
9527                    MCP_ERROR_ANALYSIS_VIEW_REQUIRED.to_string(),
9528                )));
9529            }
9530            let legacy_limits = if detailed {
9531                None
9532            } else {
9533                if params.roots.is_some()
9534                    || params.worktrees.is_some()
9535                    || params.compact.is_some()
9536                    || params.cursor.is_some()
9537                    || params.symbol.is_some()
9538                    || params.symbol_parent.is_some()
9539                    || params.symbol_kind.is_some()
9540                    || params.symbol_signature.is_some()
9541                    || params.direction.is_some()
9542                    || params.relation.is_some()
9543                    || params.minimum_confidence.is_some()
9544                    || params.resolution.is_some()
9545                    || params.depth.is_some()
9546                    || params.include_occurrences.is_some()
9547                    || params.occurrence_limit.is_some()
9548                    || params.edge_limit.is_some()
9549                    || params.node_limit.is_some()
9550                    || params.visited_limit.is_some()
9551                    || params.occurrence_total_limit.is_some()
9552                {
9553                    return Err(CliError::Service(ServiceError::InvalidInput(
9554                        MCP_ERROR_LEGACY_RELATION_CONTROLS.to_string(),
9555                    )));
9556                }
9557                Some((
9558                    projectatlas_db::LegacyRelationReadBudget::new(
9559                        params.limit.unwrap_or(
9560                            projectatlas_service::RelationNavigationView::Legacy.default_rows(),
9561                        ),
9562                        Duration::from_millis(params.deadline_ms.unwrap_or(1_000)),
9563                        params.intermediate_bytes.unwrap_or(
9564                            projectatlas_db::LegacyRelationReadBudget::MAX_INTERMEDIATE_BYTES,
9565                        ),
9566                    )?,
9567                    projectatlas_service::RelationNavigationView::Legacy
9568                        .output_bytes(params.output_bytes)?,
9569                ))
9570            };
9571            if detailed && !analysis && params.file.is_none() {
9572                return Err(CliError::Service(ServiceError::InvalidInput(
9573                    MCP_ERROR_DETAILED_RELATION_FILE.to_string(),
9574                )));
9575            }
9576            let content_selection = parse_content_selection(params.content_selection.as_deref())?;
9577            if !detailed && params.content_selection.is_some() {
9578                return Err(CliError::Service(ServiceError::InvalidInput(
9579                    MCP_ERROR_CONTENT_SELECTION_RELATION_VIEW.to_string(),
9580                )));
9581            }
9582            if params.roots.is_some() && params.worktrees.is_some() {
9583                return Err(CliError::Service(ServiceError::InvalidInput(
9584                    MCP_ERROR_FEDERATED_SELECTOR_CONFLICT.to_string(),
9585                )));
9586            }
9587            let federated_worktrees = params.worktrees.as_deref();
9588            if let Some(worktrees) = federated_worktrees {
9589                validate_federated_root_count(worktrees.len()).map_err(CliError::Service)?;
9590                if params.project_path.is_some() {
9591                    return Err(CliError::Service(ServiceError::InvalidInput(
9592                        MCP_ERROR_FEDERATED_PROJECT_PATH_CONFLICT.to_string(),
9593                    )));
9594                }
9595                if params.worktree.as_deref().is_some_and(|primary| {
9596                    worktrees
9597                        .first()
9598                        .is_none_or(|first| primary.trim() != first.trim())
9599                }) {
9600                    return Err(CliError::Service(ServiceError::InvalidInput(
9601                        MCP_ERROR_FEDERATED_PRIMARY_CONFLICT.to_string(),
9602                    )));
9603                }
9604            }
9605            let nearest_project = self.nearest_project_enabled(params.nearest_project);
9606            let selected_worktree = federated_worktrees
9607                .and_then(|worktrees| worktrees.first())
9608                .map(String::as_str)
9609                .or(params.worktree.as_deref());
9610            let profile_file = if analysis
9611                && params.analysis_mode.as_deref() == Some(MCP_RELATION_ANALYSIS_MODE_ENTRYPOINT)
9612            {
9613                params
9614                    .entrypoints
9615                    .as_ref()
9616                    .and_then(|values| values.first())
9617                    .map(|value| serde_json::from_str::<RelationAnchor>(value))
9618                    .transpose()
9619                    .map_err(|error| {
9620                        let mut message = MCP_ERROR_ENTRYPOINT_ANCHORS_PREFIX.to_string();
9621                        message.push_str(&error.to_string());
9622                        CliError::Service(ServiceError::InvalidInput(message))
9623                    })?
9624                    .map(|anchor| match anchor {
9625                        RelationAnchor::File { file } | RelationAnchor::Symbol { file, .. } => {
9626                            file.as_str().to_string()
9627                        }
9628                    })
9629            } else {
9630                None
9631            };
9632            let (state, file, routed_project) = self.state_and_optional_file_key(
9633                params.project_path.as_deref(),
9634                selected_worktree,
9635                params.file.as_deref().or(profile_file.as_deref()),
9636                nearest_project,
9637            )?;
9638            if params.roots.is_some() || federated_worktrees.is_some() {
9639                if !detailed {
9640                    return Err(CliError::Service(ServiceError::InvalidInput(
9641                        MCP_ERROR_FEDERATED_RELATION_VIEW.to_string(),
9642                    )));
9643                }
9644                let file = file.as_deref().ok_or_else(|| {
9645                    CliError::Service(ServiceError::InvalidInput(
9646                        MCP_ERROR_DETAILED_RELATION_FILE.to_string(),
9647                    ))
9648                })?;
9649                let control =
9650                    index_work_control(&SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, None, None))
9651                        .with_timeout_ceiling(Duration::from_millis(
9652                            params.deadline_ms.unwrap_or(10_000).clamp(1, 60_000),
9653                        ));
9654                let bridge = context
9655                    .map(|context| McpRequestCancellationBridge::start(&context, &control))
9656                    .transpose()?;
9657                let (roots, worktree_selections) = if let Some(worktrees) = federated_worktrees {
9658                    let (roots, selections) = self.federated_worktree_roots(worktrees)?;
9659                    (roots, Some(selections))
9660                } else {
9661                    (
9662                        params
9663                            .roots
9664                            .as_deref()
9665                            .unwrap_or_default()
9666                            .iter()
9667                            .map(PathBuf::from)
9668                            .collect::<Vec<_>>(),
9669                        None,
9670                    )
9671                };
9672                let worktree_labels = worktree_selections.as_ref().map(|selections| {
9673                    selections
9674                        .iter()
9675                        .map(|selection| selection.alias.clone())
9676                        .collect::<Vec<_>>()
9677                });
9678                let stores = open_federated_atlas_stores_for_project(
9679                    &state.db_path,
9680                    &state.root,
9681                    state.config_path.as_deref(),
9682                    &roots,
9683                    worktree_labels.as_deref(),
9684                    &control,
9685                )?;
9686                let stores = if let Some(selections) = worktree_selections.as_deref() {
9687                    Self::require_federated_worktree_identities(stores, selections)?
9688                } else {
9689                    stores
9690                };
9691                let result = Self::detailed_symbol_relations_response(
9692                    &state,
9693                    routed_project,
9694                    file,
9695                    params,
9696                    content_selection,
9697                    analysis,
9698                    SymbolRelationStores::Federated(stores),
9699                    &control,
9700                )
9701                .map(|(toon, _usage)| toon);
9702                if let Some(bridge) = bridge.as_ref() {
9703                    bridge.synchronize(&control);
9704                }
9705                drop(bridge);
9706                return result;
9707            }
9708            self.with_fresh_string_and_usage_controlled_for_request(
9709                &state,
9710                context,
9711                |store, _stamp, control| {
9712                    let file = file
9713                        .as_deref()
9714                        .map(|path| validated_indexed_file_key(store, Path::new(path)))
9715                        .transpose()?;
9716                    if detailed {
9717                        let file = file.as_deref().ok_or_else(|| {
9718                            CliError::Service(ServiceError::InvalidInput(
9719                                MCP_ERROR_DETAILED_RELATION_FILE.to_string(),
9720                            ))
9721                        })?;
9722                        return Self::detailed_symbol_relations_response(
9723                            &state,
9724                            routed_project,
9725                            file,
9726                            params,
9727                            content_selection,
9728                            analysis,
9729                            SymbolRelationStores::Single(store),
9730                            control,
9731                        );
9732                    }
9733
9734                    let (budget, output_limit) = legacy_limits.ok_or_else(|| {
9735                        CliError::Service(ServiceError::InvalidInput(
9736                            MCP_ERROR_LEGACY_RELATION_BUDGET.to_string(),
9737                        ))
9738                    })?;
9739                    let relations = store
9740                        .load_symbol_relations_bounded(
9741                            file.as_deref(),
9742                            params.query.as_deref(),
9743                            budget,
9744                            control,
9745                        )?
9746                        .relations;
9747                    let toon = Self::with_selected_project_audit(
9748                        &state,
9749                        routed_project,
9750                        render_symbol_relations(&relations),
9751                    )?;
9752                    projectatlas_service::check_legacy_relation_output(&toon, output_limit)?;
9753                    let usage = Self::telemetry_enabled()
9754                        .then(|| {
9755                            estimated_source_tokens_for_paths(
9756                                store,
9757                                relations.iter().map(|relation| relation.path.as_str()),
9758                            )
9759                        })
9760                        .and_then(Result::ok)
9761                        .map(|baseline_tokens| {
9762                            McpUsageIntent::estimate(
9763                                MCP_EVENT_ATLAS_SYMBOL_RELATIONS,
9764                                file.clone(),
9765                                params.query.clone(),
9766                                baseline_tokens,
9767                            )
9768                        });
9769                    Ok((toon, usage))
9770                },
9771            )
9772        })())
9773    }
9774
9775    /// List indexed symbol relations.
9776    #[tool(
9777        name = "atlas_symbol_relations",
9778        description = "Navigate anchored detailed relation pages (10 rows, 32 KiB by default); explicit legacy returns only complete small arrays, and analysis remains opt-in."
9779    )]
9780    fn atlas_symbol_relations(
9781        &self,
9782        Parameters(params): Parameters<AtlasSymbolRelationsParams>,
9783        context: RequestContext<RoleServer>,
9784    ) -> McpToolTextResult {
9785        self.atlas_symbol_relations_response(&params, Some(context))
9786    }
9787
9788    /// Return structural health findings.
9789    #[tool(
9790        name = "atlas_health",
9791        description = "Return a bounded ProjectAtlas structural health page with optional category, severity, and path-prefix filters."
9792    )]
9793    fn atlas_health(
9794        &self,
9795        Parameters(params): Parameters<AtlasHealthParams>,
9796        context: RequestContext<RoleServer>,
9797    ) -> McpToolTextResult {
9798        Self::as_mcp_text((|| {
9799            let state =
9800                self.state_for_target(params.project_path.clone(), params.worktree.clone())?;
9801            if params.coverage.unwrap_or(false) {
9802                let query = coverage_query_from_params(&params)?;
9803                return self.with_fresh_string_and_usage_controlled_for_request(
9804                    &state,
9805                    Some(context),
9806                    |store, stamp, control| {
9807                        let mut report =
9808                            load_coverage_discovery_controlled(store, query.clone(), control)?;
9809                        let toon = finalize_coverage_output(OutputFormat::Toon, &mut report)?;
9810                        let usage = Self::telemetry_enabled()
9811                            .then(|| {
9812                                self.estimated_source_tokens_cached(
9813                                    &state, store, &stamp, None, None,
9814                                )
9815                            })
9816                            .and_then(Result::ok)
9817                            .map(|baseline_tokens| {
9818                                McpUsageIntent::directory_walk(
9819                                    MCP_EVENT_ATLAS_HEALTH,
9820                                    None,
9821                                    None,
9822                                    baseline_tokens,
9823                                )
9824                            });
9825                        Ok((toon, usage))
9826                    },
9827                );
9828            }
9829            if has_coverage_filters(&params) {
9830                return Err(CliError::InvalidInput(
9831                    MCP_ERROR_COVERAGE_FILTERS_REQUIRE_COVERAGE.to_string(),
9832                ));
9833            }
9834            let scope = if params.source_only.unwrap_or(false) {
9835                HealthScope::source_only()
9836            } else {
9837                HealthScope::all()
9838            };
9839            let query = health_query_from_params(&params, scope)?;
9840            self.with_fresh_string_and_usage_for_request(&state, Some(context), |store, stamp| {
9841                let page = store.unresolved_health_findings_page_current(&query)?;
9842                let toon = render_health_page(&page, &query);
9843                let usage = Self::telemetry_enabled()
9844                    .then(|| self.estimated_source_tokens_cached(&state, store, &stamp, None, None))
9845                    .and_then(Result::ok)
9846                    .map(|baseline_tokens| {
9847                        McpUsageIntent::directory_walk(
9848                            MCP_EVENT_ATLAS_HEALTH,
9849                            None,
9850                            None,
9851                            baseline_tokens,
9852                        )
9853                    });
9854                Ok((toon, usage))
9855            })
9856        })())
9857    }
9858
9859    /// Mark an intentional deterministic health finding as resolved.
9860    #[tool(
9861        name = "atlas_health_resolve",
9862        description = "Mark a deterministic ProjectAtlas health finding as agent-resolved with rationale."
9863    )]
9864    fn atlas_health_resolve(
9865        &self,
9866        Parameters(params): Parameters<AtlasHealthResolveParams>,
9867    ) -> McpToolTextResult {
9868        Self::as_mcp_text((|| {
9869            let state = self.state_for_target(params.project_path, params.worktree)?;
9870            let store = Self::open_existing_mut_store(&state, &self.control_state)?;
9871            let resolution = HealthResolution {
9872                finding_id: params.finding_id,
9873                category: params.category,
9874                path: params.path,
9875                related_path: params.related_path,
9876                rationale: params.rationale,
9877            };
9878            store.resolve_health_finding(&resolution)?;
9879            Self::encode_named_payload(MCP_PAYLOAD_HEALTH_RESOLUTION, &resolution)
9880        })())
9881    }
9882
9883    /// Run `ProjectAtlas` lint checks without terminating the MCP transport.
9884    #[tool(
9885        name = "atlas_lint",
9886        description = "Run ProjectAtlas lint checks and return an ok flag, CLI-compatible exit code, and report text."
9887    )]
9888    fn atlas_lint(&self, Parameters(params): Parameters<AtlasLintParams>) -> McpToolTextResult {
9889        Self::as_mcp_text((|| {
9890            let state =
9891                self.admin_project_root(params.project_path.clone(), params.worktree.clone())?;
9892            let report = Self::lint_report_for_state(&state, &params)?;
9893            Self::encode_named_payload(MCP_PAYLOAD_LINT, &report)
9894        })())
9895    }
9896
9897    /// Return token savings telemetry.
9898    #[tool(
9899        name = "atlas_token_report",
9900        description = "Return ProjectAtlas token-savings telemetry for the whole index or one session."
9901    )]
9902    fn atlas_token_report(
9903        &self,
9904        Parameters(params): Parameters<AtlasTokenParams>,
9905    ) -> McpToolTextResult {
9906        Self::as_mcp_text((|| {
9907            let state =
9908                self.state_for_target(params.project_path.clone(), params.worktree.clone())?;
9909            let repository_scope = params.session.is_none()
9910                && state.root == self.control_state.root
9911                && state.db_path == self.control_state.db_path;
9912            let load_report = |request: TokenReportRequest<'_>| {
9913                if repository_scope {
9914                    load_synchronized_repository_token_report(
9915                        &state.db_path,
9916                        &state.root,
9917                        state
9918                            .worktree
9919                            .as_ref()
9920                            .and_then(|selection| selection.control_project_instance_id),
9921                        request,
9922                    )
9923                } else {
9924                    let store = Self::open_read_store(&state)?;
9925                    load_token_report(&store, request).map_err(CliError::from)
9926                }
9927            };
9928            let include_chart = params.include_chart.unwrap_or(false);
9929            let chart_theme = Self::parse_token_chart_theme(params.theme.as_deref())?;
9930            if let Some(window) = params.trend_window.as_deref() {
9931                if params.benchmark_results.is_some() {
9932                    return Err(CliError::InvalidInput(
9933                        TOKEN_TREND_BENCHMARK_ERROR.to_string(),
9934                    ));
9935                }
9936                let window = TokenTrendWindow::parse(window).ok_or_else(|| {
9937                    CliError::InvalidInput(format!(
9938                        "unsupported token trend window {window:?}; {TOKEN_TREND_WINDOW_ERROR_SUFFIX}"
9939                    ))
9940                })?;
9941                let request = if repository_scope {
9942                    TokenReportRequest::RepositoryTrends { window }
9943                } else {
9944                    TokenReportRequest::Trends {
9945                        caller_label: params.session.as_deref(),
9946                        window,
9947                    }
9948                };
9949                let report = match load_report(request)? {
9950                    TokenReport::Trends(report) => report,
9951                    TokenReport::Overview(_) => {
9952                        return Err(CliError::InvalidInput(
9953                            TOKEN_TRENDS_RESULT_VARIANT_MISMATCH.to_string(),
9954                        ));
9955                    }
9956                };
9957                if include_chart {
9958                    let chart = render_token_trend_dashboard_plain_with_theme(&report, chart_theme);
9959                    let output = Self::encode_two_named_payloads(
9960                        MCP_PAYLOAD_TOKEN_TRENDS,
9961                        &report,
9962                        MCP_PAYLOAD_CHART,
9963                        &chart,
9964                    )?;
9965                    return Self::with_selected_project_audit(
9966                        &state,
9967                        state.worktree.is_some(),
9968                        output,
9969                    );
9970                }
9971                return Self::with_selected_project_audit(
9972                    &state,
9973                    state.worktree.is_some(),
9974                    render_token_trends(&report),
9975                );
9976            }
9977            let request = if repository_scope {
9978                TokenReportRequest::RepositoryOverview {
9979                    benchmark_results: params.benchmark_results.as_deref().map(Path::new),
9980                }
9981            } else {
9982                TokenReportRequest::Overview {
9983                    caller_label: params.session.as_deref(),
9984                    benchmark_results: params.benchmark_results.as_deref().map(Path::new),
9985                }
9986            };
9987            let overview = match load_report(request)? {
9988                TokenReport::Overview(overview) => overview,
9989                TokenReport::Trends(_) => {
9990                    return Err(CliError::InvalidInput(
9991                        TOKEN_OVERVIEW_RESULT_VARIANT_MISMATCH.to_string(),
9992                    ));
9993                }
9994            };
9995            if include_chart {
9996                let chart = render_token_dashboard_plain_with_theme(
9997                    &overview,
9998                    params.session.as_deref(),
9999                    chart_theme,
10000                );
10001                let output = Self::encode_two_named_payloads(
10002                    MCP_PAYLOAD_TOKEN_SAVINGS,
10003                    &overview,
10004                    MCP_PAYLOAD_CHART,
10005                    &chart,
10006                )?;
10007                return Self::with_selected_project_audit(&state, state.worktree.is_some(), output);
10008            }
10009            Self::with_selected_project_audit(
10010                &state,
10011                state.worktree.is_some(),
10012                render_token_overview(&overview),
10013            )
10014        })())
10015    }
10016
10017    /// Return repository-intelligence parity readiness.
10018    #[tool(
10019        name = "atlas_parity_report",
10020        description = "Return a ProjectAtlas repository-intelligence parity gate report for release and agent-runtime readiness."
10021    )]
10022    fn atlas_parity_report(
10023        &self,
10024        Parameters(params): Parameters<AtlasParityParams>,
10025        context: RequestContext<RoleServer>,
10026    ) -> McpToolTextResult {
10027        Self::as_mcp_text((|| {
10028            let state = self.state_for_target(params.project_path, params.worktree)?;
10029            let profile = params
10030                .profile
10031                .unwrap_or_else(|| crate::REPOSITORY_INTELLIGENCE_PROFILE.to_string());
10032            self.with_fresh_string_for_request(&state, Some(context), |store, _stamp| {
10033                Ok(render_parity_report(&build_parity_report(store, &profile)?))
10034            })
10035        })())
10036    }
10037
10038    /// Return local settings and cache/index locations.
10039    #[tool(
10040        name = "atlas_settings",
10041        description = "Return ProjectAtlas local settings, config, and durable index paths."
10042    )]
10043    fn atlas_settings(
10044        &self,
10045        Parameters(params): Parameters<AtlasProjectParams>,
10046    ) -> McpToolTextResult {
10047        Self::as_mcp_text((|| {
10048            let state = self.state_for_target(params.project_path, params.worktree)?;
10049            self.render_settings_with_capabilities(&state)
10050        })())
10051    }
10052
10053    /// Return watcher availability and operating mode.
10054    #[tool(
10055        name = "atlas_watch_status",
10056        description = "Return ProjectAtlas watcher availability and current operating mode."
10057    )]
10058    fn atlas_watch_status(
10059        &self,
10060        Parameters(params): Parameters<AtlasProjectParams>,
10061    ) -> McpToolTextResult {
10062        let state = match self.state_for_target(params.project_path, params.worktree) {
10063            Ok(state) => state,
10064            Err(error) => return Self::as_mcp_text(Err(error)),
10065        };
10066        let mut report = watcher_status_report(false);
10067        if !state.db_path.exists() {
10068            report
10069                .recommendation
10070                .push_str(WATCH_STATUS_SCAN_RECOMMENDATION);
10071        }
10072        Self::as_mcp_text(Ok(render_watch_status(&report)))
10073    }
10074
10075    /// Run one incremental refresh pass.
10076    #[tool(
10077        name = "atlas_watch_once",
10078        description = "Run one MCP-safe watcher refresh pass over the repository and rebuild changed symbols, with optional worker, timeout, and text-index size controls."
10079    )]
10080    fn atlas_watch_once(
10081        &self,
10082        Parameters(params): Parameters<AtlasWatchOnceParams>,
10083    ) -> McpToolTextResult {
10084        Self::as_mcp_text((|| {
10085            let nearest_project = self.nearest_project_enabled(params.nearest_project);
10086            let background = params.background.unwrap_or(false);
10087            let (state, path) = if background {
10088                self.background_state_and_root_path(
10089                    params.project_path,
10090                    params.worktree,
10091                    params.path,
10092                    nearest_project,
10093                )?
10094            } else {
10095                self.state_and_root_path(
10096                    params.project_path,
10097                    params.worktree,
10098                    params.path,
10099                    nearest_project,
10100                )?
10101            };
10102            let symbol_options = SymbolBuildOptions::new(
10103                MAX_SYMBOL_FILE_BYTES,
10104                params.max_workers,
10105                params.timeout_seconds,
10106            );
10107            let text_index_max_bytes = params.text_index_max_bytes;
10108            if background {
10109                let control_state = self.control_state.clone();
10110                let observations = Arc::clone(&self.source_observations);
10111                let task = self.start_index_task(
10112                    McpTaskOperation::WatchOnce,
10113                    symbol_options,
10114                    MCP_TOOL_ATLAS_OVERVIEW,
10115                    move |control, symbol_options| {
10116                        let plan = ScanRuntimePlan::for_path_controlled(
10117                            state.config_path.as_deref(),
10118                            &path,
10119                            text_index_max_bytes,
10120                            control,
10121                        )?;
10122                        Self::run_observed_watch_refresh(
10123                            &state,
10124                            &control_state,
10125                            &plan,
10126                            &symbol_options,
10127                            control,
10128                            &observations,
10129                            text_index_max_bytes.is_none(),
10130                        )?;
10131                        Ok(())
10132                    },
10133                )?;
10134                return Self::encode_named_payload(MCP_PAYLOAD_TASK_START, &task);
10135            }
10136            let control = index_work_control(&symbol_options);
10137            let plan = ScanRuntimePlan::for_path_controlled(
10138                state.config_path.as_deref(),
10139                &path,
10140                text_index_max_bytes,
10141                &control,
10142            )?;
10143            let report = Self::run_observed_watch_refresh(
10144                &state,
10145                &self.control_state,
10146                &plan,
10147                &symbol_options,
10148                &control,
10149                &self.source_observations,
10150                text_index_max_bytes.is_none(),
10151            )?;
10152            Self::encode_named_payload(MCP_PAYLOAD_WATCH, &report)
10153        })())
10154    }
10155
10156    /// Preview or remove legacy `.purpose` files.
10157    #[tool(
10158        name = "atlas_strip_legacy_purpose",
10159        description = "Preview or remove legacy .purpose files after their metadata has been imported to SQLite."
10160    )]
10161    fn atlas_strip_legacy_purpose(
10162        &self,
10163        Parameters(params): Parameters<AtlasStripLegacyParams>,
10164    ) -> McpToolTextResult {
10165        Self::as_mcp_text((|| {
10166            let nearest_project = self.nearest_project_enabled(params.nearest_project);
10167            let (state, path) = self.state_and_root_path(
10168                params.project_path,
10169                params.worktree,
10170                params.path,
10171                nearest_project,
10172            )?;
10173            let report = strip_legacy_purpose(
10174                &path,
10175                state.config_path.as_deref(),
10176                params.apply.unwrap_or(false),
10177                params.dry_run.unwrap_or(false),
10178                params
10179                    .strip_source_headers
10180                    .unwrap_or_else(|| state.config_path.is_some()),
10181            )?;
10182            Self::encode_named_payload(MCP_PAYLOAD_LEGACY_PURPOSE_MIGRATION, &report)
10183        })())
10184    }
10185
10186    /// Preview or remove local runtime index/cache files.
10187    #[tool(
10188        name = "atlas_reset_index",
10189        description = "Preview or clear ProjectAtlas local SQLite index/cache files for recovery."
10190    )]
10191    fn atlas_reset_index(
10192        &self,
10193        Parameters(params): Parameters<AtlasResetIndexParams>,
10194    ) -> McpToolTextResult {
10195        Self::as_mcp_text((|| {
10196            let state = self.state_for_target(params.project_path, params.worktree)?;
10197            let apply = params.apply.unwrap_or(false);
10198            let dry_run = params.dry_run.unwrap_or(false);
10199            let include_mcp_config = params.include_mcp_config.unwrap_or(false);
10200            let admission = crate::runtime::preflight_managed_reset(&state.db_path, &state.root)?;
10201            let database =
10202                crate::runtime::admitted_database_path(&state.db_path, admission.as_ref());
10203            let report = if apply && !dry_run {
10204                if let Some(selection) = state
10205                    .worktree
10206                    .as_ref()
10207                    .filter(|selection| selection.registration_id.is_some())
10208                {
10209                    self.reset_registered_worktree_index(&state, selection, include_mcp_config)?
10210                } else {
10211                    reset_index_files(
10212                        database,
10213                        true,
10214                        false,
10215                        include_mcp_config,
10216                        admission.as_ref(),
10217                    )?
10218                }
10219            } else {
10220                reset_index_files(
10221                    database,
10222                    apply,
10223                    dry_run,
10224                    include_mcp_config,
10225                    admission.as_ref(),
10226                )?
10227            };
10228            Self::encode_named_payload(MCP_PAYLOAD_RESET_INDEX, &report)
10229        })())
10230    }
10231
10232    /// Generate a project-local MCP config document.
10233    #[tool(
10234        name = "atlas_mcp_config",
10235        description = "Return a generated ProjectAtlas MCP config document for mcp-json, codex, claude-code, or opencode hosts."
10236    )]
10237    fn atlas_mcp_config(
10238        &self,
10239        Parameters(params): Parameters<AtlasMcpConfigParams>,
10240    ) -> McpToolTextResult {
10241        Self::as_mcp_text((|| {
10242            let state = self.admin_project_root(params.project_path, params.worktree)?;
10243            let harness = Self::parse_harness_config(params.harness.as_deref())?;
10244            let server_name = params
10245                .server_name
10246                .unwrap_or_else(|| MCP_DEFAULT_CONFIG_SERVER_NAME.to_string());
10247            let report = build_harness_mcp_config_report(
10248                harness,
10249                &server_name,
10250                &state.db_path,
10251                state.config_path.as_deref(),
10252                params.nearest_project.unwrap_or(false),
10253            )?;
10254            Self::encode_named_payload(MCP_PAYLOAD_MCP_CONFIG, &report)
10255        })())
10256    }
10257
10258    /// Return runtime identity and compiled MCP tool surface.
10259    #[tool(
10260        name = "atlas_runtime_info",
10261        description = "Return ProjectAtlas runtime identity, version, capabilities, and compiled MCP tool names."
10262    )]
10263    fn atlas_runtime_info(
10264        &self,
10265        Parameters(_params): Parameters<AtlasProjectParams>,
10266    ) -> McpToolTextResult {
10267        let _session_scope = self.session.as_str();
10268        Self::as_mcp_text(Ok(render_runtime_info(&build_runtime_info())))
10269    }
10270
10271    /// Return a compact startup brief for agents.
10272    #[tool(
10273        name = "atlas_session_brief",
10274        description = "Return selected project identity, index state, ranked candidates, blockers, and typed next-call recommendations for agent startup."
10275    )]
10276    fn atlas_session_brief(
10277        &self,
10278        Parameters(params): Parameters<AtlasSessionBriefParams>,
10279        context: RequestContext<RoleServer>,
10280    ) -> McpToolTextResult {
10281        Self::as_mcp_text((|| {
10282            if params.compact.unwrap_or(false) {
10283                let brief = self.build_compact_session_brief(params, Some(context))?;
10284                Self::encode_named_payload(MCP_PAYLOAD_SESSION_BRIEF, &brief)
10285            } else {
10286                let brief = self.build_session_brief(params, Some(context))?;
10287                Self::encode_named_payload(MCP_PAYLOAD_SESSION_BRIEF, &brief)
10288            }
10289        })())
10290    }
10291
10292    /// Return typed status for one MCP task-progress record.
10293    #[tool(
10294        name = "atlas_task_status",
10295        description = "Return typed status for a bounded MCP task-progress record."
10296    )]
10297    fn atlas_task_status(
10298        &self,
10299        Parameters(params): Parameters<AtlasTaskParams>,
10300    ) -> McpToolTextResult {
10301        Self::as_mcp_text((|| {
10302            let status = self.task_status(params.task_id)?;
10303            Self::encode_named_payload(MCP_PAYLOAD_TASK_STATUS, &status)
10304        })())
10305    }
10306
10307    /// Request cancellation for one MCP task-progress record.
10308    #[tool(
10309        name = "atlas_task_cancel",
10310        description = "Request cancellation for a bounded MCP task-progress record."
10311    )]
10312    fn atlas_task_cancel(
10313        &self,
10314        Parameters(params): Parameters<AtlasTaskParams>,
10315    ) -> McpToolTextResult {
10316        Self::as_mcp_text((|| {
10317            let cancel = self.task_cancel(params.task_id)?;
10318            Self::encode_named_payload(MCP_PAYLOAD_TASK_CANCEL, &cancel)
10319        })())
10320    }
10321
10322    /// Return a bounded purpose curation queue.
10323    #[tool(
10324        name = "atlas_purpose_queue",
10325        description = "Return a bounded folder-first queue of ProjectAtlas paths that need agent purpose curation."
10326    )]
10327    fn atlas_purpose_queue(
10328        &self,
10329        Parameters(params): Parameters<AtlasPurposeQueueParams>,
10330        context: RequestContext<RoleServer>,
10331    ) -> McpToolTextResult {
10332        Self::as_mcp_text((|| {
10333            let state = self.state_for_target(
10334                params.health.project_path.clone(),
10335                params.health.worktree.clone(),
10336            )?;
10337            let query =
10338                health_query_from_params(&params.health, purpose_queue_scope(&params.health))?;
10339            let task = params
10340                .task
10341                .as_deref()
10342                .unwrap_or(MCP_PURPOSE_TASK_QUEUE)
10343                .to_string();
10344            self.with_fresh_string_and_usage_for_request(&state, Some(context), |store, stamp| {
10345                let page = purpose_curation_page(store, &query, &task)?;
10346                let toon = render_purpose_curation_page(&page);
10347                let usage = Self::telemetry_enabled()
10348                    .then(|| self.estimated_source_tokens_cached(&state, store, &stamp, None, None))
10349                    .and_then(Result::ok)
10350                    .map(|baseline_tokens| {
10351                        McpUsageIntent::directory_walk(
10352                            MCP_EVENT_ATLAS_PURPOSE_QUEUE,
10353                            None,
10354                            None,
10355                            baseline_tokens,
10356                        )
10357                    });
10358                Ok((toon, usage))
10359            })
10360        })())
10361    }
10362
10363    /// Set an agent-approved purpose in the durable index.
10364    #[tool(
10365        name = "atlas_purpose_set",
10366        description = "Set agent-approved ProjectAtlas purpose metadata for one indexed path."
10367    )]
10368    fn atlas_purpose_set(
10369        &self,
10370        Parameters(params): Parameters<AtlasPurposeSetParams>,
10371        context: RequestContext<RoleServer>,
10372    ) -> McpToolTextResult {
10373        Self::as_mcp_text((|| {
10374            let state = self.state_for_target(params.project_path, params.worktree)?;
10375            let node_key = Self::preflight_purpose_path(&state, &params.path)?;
10376            self.with_admitted_purpose_mutation(&state, Some(context), |store| {
10377                Self::require_indexed_purpose_path(store, &node_key)?;
10378                store.set_purpose(&node_key, &params.purpose, PurposeSource::Agent)?;
10379                let classification = if store
10380                    .load_node_by_path(&node_key)?
10381                    .is_some_and(|node| node.node.kind == projectatlas_core::NodeKind::File)
10382                {
10383                    store
10384                        .file_content_classifications_for_paths(std::slice::from_ref(&node_key))?
10385                        .first()
10386                        .map(|row| row.classification)
10387                } else {
10388                    None
10389                };
10390                Self::encode_serialized_payload(McpPurposeSetResponse {
10391                    purpose_set: McpPurposeSetPayload {
10392                        path: node_key,
10393                        classification,
10394                        status: PurposeStatus::Approved,
10395                        source: PurposeSource::Agent,
10396                        agent_reviewed: true,
10397                    },
10398                })
10399            })
10400        })())
10401    }
10402
10403    /// Batch-review existing purpose records through the MCP surface.
10404    #[tool(
10405        name = "atlas_purpose_review",
10406        description = "Preview or apply agent-reviewed ProjectAtlas purpose metadata for multiple indexed paths."
10407    )]
10408    fn atlas_purpose_review(
10409        &self,
10410        Parameters(params): Parameters<AtlasPurposeReviewParams>,
10411        context: RequestContext<RoleServer>,
10412    ) -> McpToolTextResult {
10413        Self::as_mcp_text((|| {
10414            let apply = params.apply.unwrap_or(false);
10415            let requests = params
10416                .items
10417                .into_iter()
10418                .map(|item| PurposeReviewRequest {
10419                    path: item.path,
10420                    purpose: item.purpose,
10421                    confirm_existing: item.confirm_existing.unwrap_or(false),
10422                    task: item.task,
10423                    work_key: item.work_key,
10424                    state_token: item.state_token,
10425                })
10426                .collect::<Vec<_>>();
10427            validate_purpose_review_admission(&requests)?;
10428            let state = self.state_for_target(params.project_path, params.worktree)?;
10429            if apply {
10430                return self.with_admitted_purpose_mutation(&state, Some(context), |store| {
10431                    let report = review_purposes(store, &requests, true)?;
10432                    Ok(render_purpose_review_report(&report))
10433                });
10434            }
10435            self.with_fresh_string_for_request(&state, Some(context), |store, _stamp| {
10436                let report = review_purposes(store, &requests, false)?;
10437                Ok(render_purpose_review_report(&report))
10438            })
10439        })())
10440    }
10441}
10442
10443#[allow(clippy::unused_async_trait_impl)]
10444#[tool_handler(router = self.tool_router)]
10445impl ServerHandler for ProjectAtlasMcpServer {
10446    fn get_info(&self) -> ServerConfig {
10447        ServerConfig::new(ServerCapabilities::builder().enable_tools().build())
10448            .with_server_info(Implementation::new(
10449                MCP_SERVER_NAME,
10450                env!("CARGO_PKG_VERSION"),
10451            ))
10452            .with_instructions(MCP_SERVER_INSTRUCTIONS)
10453    }
10454}
10455
10456/// Return current Unix time in milliseconds for MCP task status records.
10457fn mcp_unix_time_ms() -> u128 {
10458    SystemTime::now()
10459        .duration_since(UNIX_EPOCH)
10460        .map_or(0, |duration| duration.as_millis())
10461}
10462
10463/// Recognize cooperative cancellation through each typed adapter error layer.
10464fn task_error_is_canceled(error: &CliError) -> bool {
10465    std::iter::successors(
10466        Some(error as &(dyn std::error::Error + 'static)),
10467        |source| source.source(),
10468    )
10469    .any(|source| {
10470        matches!(
10471            source.downcast_ref::<IndexWorkFailure>(),
10472            Some(IndexWorkFailure::Cancelled { .. })
10473        )
10474    })
10475}
10476
10477/// Retain one concise task failure without letting diagnostics grow unbounded.
10478fn bounded_task_error(error: &CliError) -> String {
10479    error
10480        .to_string()
10481        .chars()
10482        .take(MCP_TASK_ERROR_MAX_CHARS)
10483        .collect()
10484}
10485
10486#[cfg(test)]
10487mod tests {
10488    use super::*;
10489    use crate::atlas_map::init_project_with_config;
10490    use notify::{Event, EventKind, event::ModifyKind};
10491    use projectatlas_core::graph::{
10492        Completeness, ConfidenceClass, EntitySelector, ExtendedRelationKind, ExternalSelector,
10493        GraphEntity, GraphIdentityText, GraphRelationKind, LogicalRelation, PackageSelector,
10494        RelationResolution, RepositoryFilePath,
10495    };
10496    use projectatlas_core::symbols::RelationKind;
10497    use projectatlas_core::{
10498        CanonicalProjectRoot, IndexCancellation, IndexWorkStage, RankedConnectionDirection,
10499        RankedConnectionKind, RankedConnectionTarget,
10500    };
10501    use projectatlas_db::ProjectRootTransition;
10502    use std::collections::BTreeSet;
10503    use std::fs;
10504    use std::io;
10505    #[cfg(unix)]
10506    use std::os::unix::ffi::OsStringExt;
10507    use std::process::Command as StdCommand;
10508    use std::time::{Duration, Instant};
10509
10510    fn require(condition: bool, message: &str) -> Result<(), Box<dyn std::error::Error>> {
10511        if condition {
10512            Ok(())
10513        } else {
10514            Err(io::Error::other(message.to_string()).into())
10515        }
10516    }
10517
10518    #[test]
10519    fn mcp_map_response_limit_checks_encoded_envelope_and_typed_error()
10520    -> Result<(), Box<dyn std::error::Error>> {
10521        let error = CliError::AtlasMap(crate::atlas_map::AtlasMapError::ResponseLimitExceeded {
10522            observed: 5,
10523            limit: 4,
10524        });
10525        let payload: serde_json::Value =
10526            toon_format::decode_default(&ProjectAtlasMcpServer::encode_error_payload(&error))?;
10527        require(
10528            payload["error"]["kind"] == "response_limit_exceeded"
10529                && payload["error"]["message"]
10530                    .as_str()
10531                    .is_some_and(|message| message.contains("observed 5, limit 4")),
10532            "MCP map response limit lost its typed error",
10533        )?;
10534
10535        let report = McpMapReport {
10536            root: None,
10537            map_path: None,
10538            written: false,
10539            json: false,
10540            format: crate::OutputFormat::Toon,
10541            content: "\"".repeat(2_100_000),
10542            json_written: false,
10543            skipped_reason: None,
10544        };
10545        let encoded = ProjectAtlasMcpServer::encode_named_payload(MCP_PAYLOAD_MAP, &report)?;
10546        require(
10547            report.content.len() < 4 * 1024 * 1024
10548                && matches!(
10549                    enforce_map_response_limit(&encoded, MAX_MAP_RESPONSE_BYTES),
10550                    Err(crate::atlas_map::AtlasMapError::ResponseLimitExceeded { .. })
10551                ),
10552            "MCP map accepted an encoded envelope larger than the response limit",
10553        )?;
10554
10555        let temp = tempfile::tempdir()?;
10556        fs::write(
10557            temp.path().join("source.rs"),
10558            "// Purpose: MCP envelope limit fixture.\n",
10559        )?;
10560        let state = McpProjectState {
10561            root: fs::canonicalize(temp.path())?,
10562            db_path: temp.path().join(".projectatlas").join("projectatlas.db"),
10563            config_path: None,
10564            worktree: None,
10565        };
10566        require(
10567            matches!(
10568                ProjectAtlasMcpServer::build_map_report_with_limit(&state, true, true, 1),
10569                Err(CliError::AtlasMap(
10570                    crate::atlas_map::AtlasMapError::ResponseLimitExceeded { limit: 1, .. }
10571                ))
10572            ) && !temp.path().join(".projectatlas").exists(),
10573            "MCP envelope refusal created a JSON sidecar or legacy TOON file",
10574        )
10575    }
10576
10577    /// Downgrade a current fixture to the released schema-19 worktree shape.
10578    fn drop_native_worktree_identity_schema(
10579        connection: &rusqlite::Connection,
10580    ) -> rusqlite::Result<()> {
10581        connection.execute_batch(
10582            "DROP INDEX IF EXISTS idx_worktree_registrations_active_native_administrative_directory;
10583             DROP INDEX IF EXISTS idx_worktree_registrations_active_native_root;
10584             ALTER TABLE worktree_registrations DROP COLUMN git_common_directory_identity;
10585             ALTER TABLE worktree_registrations DROP COLUMN git_administrative_directory_identity;
10586             ALTER TABLE worktree_registrations DROP COLUMN last_root_identity;",
10587        )
10588    }
10589
10590    /// Execute one test-owned Git fixture command and retain exact diagnostics on failure.
10591    fn run_fixture_command(command: &mut StdCommand) -> Result<String, Box<dyn std::error::Error>> {
10592        let output = command.output()?;
10593        if !output.status.success() {
10594            return Err(io::Error::other(format!(
10595                "fixture command failed: {}{}",
10596                String::from_utf8_lossy(&output.stdout),
10597                String::from_utf8_lossy(&output.stderr)
10598            ))
10599            .into());
10600        }
10601        Ok(String::from_utf8(output.stdout)?)
10602    }
10603
10604    struct RegisteredWorktreeRaceFixture {
10605        _temp: tempfile::TempDir,
10606        primary: PathBuf,
10607        control_root: PathBuf,
10608        linked: PathBuf,
10609        control_db: PathBuf,
10610        target_db: PathBuf,
10611        server: ProjectAtlasMcpServer,
10612        alias: WorktreeAlias,
10613        registration: WorktreeRegistration,
10614        selection: McpWorktreeSelection,
10615        state: McpProjectState,
10616        administrative_directory: PathBuf,
10617        administrative_identity: String,
10618    }
10619
10620    fn registered_worktree_race_fixture(
10621        alias: &str,
10622    ) -> Result<RegisteredWorktreeRaceFixture, Box<dyn std::error::Error>> {
10623        let temp = tempfile::tempdir()?;
10624        let primary = temp.path().join("control");
10625        let linked = temp.path().join("linked");
10626        fs::create_dir_all(primary.join("src"))?;
10627        run_fixture_command(StdCommand::new("git").current_dir(&primary).arg("init"))?;
10628        for (key, value) in [
10629            ("user.name", "ProjectAtlas Test"),
10630            ("user.email", "projectatlas@example.invalid"),
10631            ("commit.gpgsign", "false"),
10632            ("core.autocrlf", "false"),
10633        ] {
10634            run_fixture_command(
10635                StdCommand::new("git")
10636                    .current_dir(&primary)
10637                    .args(["config", key, value]),
10638            )?;
10639        }
10640        fs::write(primary.join("src/lib.rs"), "pub fn control() {}\n")?;
10641        run_fixture_command(
10642            StdCommand::new("git")
10643                .current_dir(&primary)
10644                .args(["add", "."]),
10645        )?;
10646        run_fixture_command(
10647            StdCommand::new("git")
10648                .current_dir(&primary)
10649                .args(["commit", "-m", "fixture"]),
10650        )?;
10651        run_fixture_command(
10652            StdCommand::new("git")
10653                .current_dir(&primary)
10654                .args(["worktree", "add", "-b", "captured"])
10655                .arg(&linked),
10656        )?;
10657
10658        let control_root = primary.canonicalize()?;
10659        let control_config = control_root.join(PROJECTATLAS_DIR_NAME).join("config.toml");
10660        let control_db = control_root
10661            .join(PROJECTATLAS_DIR_NAME)
10662            .join(PROJECTATLAS_DB_FILE_NAME);
10663        init_project_with_config(&control_root, Some(&control_config))?;
10664        let mut control = AtlasStore::open_for_project(&control_db, &control_root)?;
10665        let plan = ScanRuntimePlan::for_path(Some(&control_config), &control_root, None)?;
10666        run_scan_pipeline(
10667            &mut control,
10668            &plan,
10669            &SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, None, None),
10670        )?;
10671        let control_project_instance_id = control
10672            .project_instance_id()?
10673            .ok_or(DbError::ProjectInstanceIdentityMissing)?;
10674        let server = ProjectAtlasMcpServer::new(
10675            control_db.clone(),
10676            Some(control_config),
10677            "worktree-race".to_string(),
10678            false,
10679        );
10680        let repository = server.control_git_repository()?;
10681        let canonical_linked = linked.canonicalize()?;
10682        let entry = repository
10683            .worktrees
10684            .iter()
10685            .find(|entry| {
10686                ProjectAtlasMcpServer::active_worktree_root(entry)
10687                    == Some(canonical_linked.as_path())
10688            })
10689            .ok_or_else(|| io::Error::other("linked worktree was not discovered"))?;
10690        let administrative_directory = entry.administrative_directory.clone();
10691        let administrative_identity = git_administrative_identity(&administrative_directory)?;
10692        let alias = WorktreeAlias::parse(alias)?;
10693        let registration = control.register_worktree(
10694            &alias,
10695            &repository.common_directory,
10696            &administrative_directory,
10697            &administrative_identity,
10698            &canonical_linked,
10699            None,
10700            1,
10701        )?;
10702        drop(control);
10703        let target_db = canonical_linked
10704            .join(PROJECTATLAS_DIR_NAME)
10705            .join(PROJECTATLAS_DB_FILE_NAME);
10706        let selection = McpWorktreeSelection {
10707            alias: alias.to_string(),
10708            registration_id: Some(registration.registration_id),
10709            project_instance_id: None,
10710            control_project_instance_id: Some(control_project_instance_id),
10711        };
10712        let state = McpProjectState {
10713            root: canonical_linked,
10714            db_path: target_db.clone(),
10715            config_path: Some(linked.join(PROJECTATLAS_DIR_NAME).join("config.toml")),
10716            worktree: Some(selection.clone()),
10717        };
10718        Ok(RegisteredWorktreeRaceFixture {
10719            _temp: temp,
10720            primary: control_root.clone(),
10721            control_root,
10722            linked,
10723            control_db,
10724            target_db,
10725            server,
10726            alias,
10727            registration,
10728            selection,
10729            state,
10730            administrative_directory,
10731            administrative_identity,
10732        })
10733    }
10734
10735    fn replace_registered_worktree(
10736        fixture: &RegisteredWorktreeRaceFixture,
10737        branch: &str,
10738    ) -> Result<GitWorktreeEntry, Box<dyn std::error::Error>> {
10739        run_fixture_command(
10740            StdCommand::new("git")
10741                .current_dir(&fixture.primary)
10742                .args(["worktree", "remove", "--force"])
10743                .arg(&fixture.linked),
10744        )?;
10745        run_fixture_command(
10746            StdCommand::new("git")
10747                .current_dir(&fixture.primary)
10748                .args(["worktree", "add", "-b", branch])
10749                .arg(&fixture.linked),
10750        )?;
10751        let repository = fixture.server.control_git_repository()?;
10752        repository
10753            .worktrees
10754            .into_iter()
10755            .find(|entry| {
10756                ProjectAtlasMcpServer::active_worktree_root(entry)
10757                    == Some(fixture.state.root.as_path())
10758            })
10759            .ok_or_else(|| io::Error::other("replacement worktree was not discovered").into())
10760    }
10761
10762    /// Prepare one fully verified hydration candidate without publishing it.
10763    fn prepared_hydration_candidate(
10764        fixture: &RegisteredWorktreeRaceFixture,
10765    ) -> Result<
10766        (
10767            PreparedWorktreeHydrationCandidate,
10768            PathBuf,
10769            IndexWorkControl,
10770        ),
10771        Box<dyn std::error::Error>,
10772    > {
10773        fs::create_dir_all(
10774            fixture
10775                .target_db
10776                .parent()
10777                .ok_or_else(|| io::Error::other("target database has no parent"))?,
10778        )?;
10779        let source =
10780            open_atlas_store_read_only_for_project(&fixture.control_db, &fixture.control_root)?;
10781        let work_control =
10782            index_work_control(&SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, None, None));
10783        let mut candidate = source.prepare_worktree_hydration(
10784            &fixture.state.root,
10785            &fixture.target_db,
10786            &work_control,
10787        )?;
10788        let candidate_path = candidate.path()?.to_path_buf();
10789        candidate.accept_verified_source_state(&work_control)?;
10790        let candidate = candidate.prepare_activation(&work_control)?;
10791        Ok((candidate, candidate_path, work_control))
10792    }
10793
10794    #[test]
10795    fn task_errors_classify_only_typed_cancellation_as_canceled() {
10796        let stage = IndexWorkStage::RepositoryTraversal;
10797        assert!(task_error_is_canceled(&CliError::IndexWork(
10798            IndexWorkFailure::Cancelled { stage },
10799        )));
10800        assert!(task_error_is_canceled(&CliError::Fs(
10801            projectatlas_fs::FsError::IndexWork(IndexWorkFailure::Cancelled { stage }),
10802        )));
10803        assert!(task_error_is_canceled(&CliError::Db(DbError::IndexWork(
10804            IndexWorkFailure::Cancelled { stage },
10805        ))));
10806        assert!(task_error_is_canceled(&CliError::Service(
10807            ServiceError::Db(DbError::IndexWork(IndexWorkFailure::Cancelled { stage })),
10808        )));
10809        assert!(!task_error_is_canceled(&CliError::IndexWork(
10810            IndexWorkFailure::DeadlineExceeded { stage },
10811        )));
10812        assert!(!task_error_is_canceled(&CliError::Db(DbError::IndexWork(
10813            IndexWorkFailure::DeadlineExceeded { stage },
10814        ))));
10815        assert!(!task_error_is_canceled(&CliError::Fs(
10816            projectatlas_fs::FsError::IndexWork(IndexWorkFailure::ResourceLimitExceeded {
10817                stage,
10818                resource: projectatlas_core::IndexWorkResource::Entries,
10819                limit: 1,
10820                observed: 2,
10821            }),
10822        )));
10823    }
10824
10825    fn usage_test_project(
10826        parent: &Path,
10827        name: &str,
10828    ) -> Result<(McpProjectState, AtlasStore), Box<dyn std::error::Error>> {
10829        let root = parent.join(name);
10830        fs::create_dir_all(root.join(".projectatlas"))?;
10831        let db_path = root.join(".projectatlas").join("projectatlas.db");
10832        let store = AtlasStore::open_for_project(&db_path, &root)?;
10833        Ok((
10834            McpProjectState {
10835                root,
10836                db_path,
10837                config_path: None,
10838                worktree: None,
10839            },
10840            store,
10841        ))
10842    }
10843
10844    fn usage_runtime_identity(
10845        server: &ProjectAtlasMcpServer,
10846        state: &McpProjectState,
10847        store: &AtlasStore,
10848    ) -> Result<UsageRuntimeInstance, Box<dyn std::error::Error>> {
10849        let binding = McpUsageProjectBinding::capture(state, store)?;
10850        let project_instance = server
10851            .usage_runtime
10852            .lock()
10853            .map_err(|_poisoned| io::Error::other("usage runtime lock poisoned"))?
10854            .entries
10855            .iter()
10856            .find(|entry| entry.binding == binding)
10857            .map(|entry| Arc::clone(&entry.instance))
10858            .ok_or_else(|| io::Error::other("operating-system entropy was unavailable"))?;
10859        let identity = *project_instance
10860            .lock()
10861            .map_err(|_poisoned| io::Error::other("project usage lock poisoned"))?;
10862        Ok(identity)
10863    }
10864
10865    #[test]
10866    fn mcp_server_clones_share_one_runtime_identity() -> Result<(), Box<dyn std::error::Error>> {
10867        let temp = tempfile::tempdir()?;
10868        let (state, store) = usage_test_project(temp.path(), "selected-project")?;
10869        let first = ProjectAtlasMcpServer::new(
10870            state.db_path.clone(),
10871            None,
10872            "shared-label".to_string(),
10873            false,
10874        );
10875        first.record_usage_for_state(&state, &store, |_usage_instance| Ok(()));
10876        let first_identity = usage_runtime_identity(&first, &state, &store)?;
10877        let cloned = first.clone();
10878        require(
10879            usage_runtime_identity(&first, &state, &store)? == first_identity,
10880            "cloning an MCP server changed the original telemetry identity",
10881        )?;
10882        let restarted = ProjectAtlasMcpServer::new(
10883            state.db_path.clone(),
10884            None,
10885            "shared-label".to_string(),
10886            false,
10887        );
10888        restarted.record_usage_for_state(&state, &store, |_usage_instance| Ok(()));
10889        let restarted_identity = usage_runtime_identity(&restarted, &state, &store)?;
10890
10891        require(
10892            usage_runtime_identity(&cloned, &state, &store)? == first_identity,
10893            "cloning an MCP server changed its process-scoped telemetry identity",
10894        )?;
10895        require(
10896            restarted_identity != first_identity,
10897            "a separately constructed MCP server reused the prior runtime identity",
10898        )
10899    }
10900
10901    #[test]
10902    fn mcp_request_cancellation_bridge_reaches_index_work() -> Result<(), Box<dyn std::error::Error>>
10903    {
10904        let cancellation = Arc::new(std::sync::atomic::AtomicBool::new(false));
10905        let observed = Arc::clone(&cancellation);
10906        let control = IndexWorkControl::new(IndexCancellation::new(), None);
10907        let bridge = McpRequestCancellationBridge::start_with_probe(
10908            move || observed.load(Ordering::Acquire),
10909            &control,
10910        )?;
10911
10912        cancellation.store(true, Ordering::Release);
10913        let mut canceled = false;
10914        for _attempt in 0..100 {
10915            if matches!(
10916                control.check(IndexWorkStage::Publication),
10917                Err(IndexWorkFailure::Cancelled { .. })
10918            ) {
10919                canceled = true;
10920                break;
10921            }
10922            thread::sleep(Duration::from_millis(2));
10923        }
10924        drop(bridge);
10925
10926        require(
10927            canceled,
10928            "RMCP cancellation probe did not reach the shared index work control",
10929        )
10930    }
10931
10932    #[test]
10933    fn purpose_preflight_preserves_admitted_mutations_and_saved_source_repair()
10934    -> Result<(), Box<dyn std::error::Error>> {
10935        let temp = tempfile::tempdir()?;
10936        let root = temp.path();
10937        fs::create_dir(root.join(".projectatlas"))?;
10938        fs::write(root.join("source.rs"), "fn current() {}\n")?;
10939        let db_path = root.join(".projectatlas/projectatlas.db");
10940        let plan = ScanRuntimePlan::for_path(None, root, None)?;
10941        let mut store = open_atlas_store_for_project(&db_path, &plan.root)?;
10942        run_scan_pipeline(
10943            &mut store,
10944            &plan,
10945            &SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, None, None),
10946        )?;
10947        drop(store);
10948        let server =
10949            ProjectAtlasMcpServer::new(db_path, None, "purpose-preflight".to_owned(), false);
10950        let state = server.state_for_target(Some(normalize_native_path_display(root)), None)?;
10951        let control = IndexWorkControl::new(IndexCancellation::new(), None);
10952        let admission = server.source_observations.admit_mutation(
10953            &state.db_path,
10954            &state.root,
10955            state.config_path.as_deref(),
10956            &control,
10957        )?;
10958        let store = ProjectAtlasMcpServer::open_existing_mut_store(&state, &server.control_state)?;
10959        let transaction = store.begin_purpose_mutation()?;
10960        store.set_purpose(".", "Accepted repository purpose", PurposeSource::Agent)?;
10961        let absolute = normalize_native_path_display(root.join("source.rs"));
10962        for path in [absolute.as_str(), "", "missing.rs"] {
10963            require(
10964                matches!(
10965                    ProjectAtlasMcpServer::preflight_purpose_path(&state, path),
10966                    Err(CliError::InvalidInput(_))
10967                ),
10968                "invalid purpose path passed admission preflight",
10969            )?;
10970            admission.verify()?;
10971        }
10972        transaction.commit()?;
10973        require(
10974            store.load_node_by_path(".")?.is_some_and(|node| {
10975                node.purpose.purpose.as_deref() == Some("Accepted repository purpose")
10976            }),
10977            "rejected purpose requests prevented the admitted mutation from committing",
10978        )?;
10979        let identity = store.captured_project_binding()?.project_instance_id;
10980        drop(store);
10981        let saved_path = root.join("added.rs");
10982        let saved_source = "fn added() {}\n";
10983        fs::write(&saved_path, saved_source)?;
10984        let node_key = ProjectAtlasMcpServer::preflight_purpose_path(&state, "added.rs")?;
10985        server.with_admitted_purpose_mutation_controlled(&state, &control, None, |store| {
10986            ProjectAtlasMcpServer::require_indexed_purpose_path(store, &node_key)?;
10987            store.set_purpose(&node_key, "New saved source", PurposeSource::Agent)?;
10988            Ok(())
10989        })?;
10990        let store = ProjectAtlasMcpServer::open_read_store(&state)?;
10991        require(
10992            store.captured_project_binding()?.project_instance_id == identity
10993                && store.load_node_by_path("added.rs")?.is_some_and(|node| {
10994                    node.purpose.purpose.as_deref() == Some("New saved source")
10995                })
10996                && fs::read_to_string(saved_path)? == saved_source,
10997            "purpose preflight prevented exact saved-source repair or changed source identity",
10998        )?;
10999        store.finish_index_read_snapshot()?;
11000        Ok(())
11001    }
11002
11003    #[test]
11004    fn purpose_preflight_preserves_selected_worktree_error_context()
11005    -> Result<(), Box<dyn std::error::Error>> {
11006        let fixture = registered_worktree_race_fixture("purpose-target")?;
11007        fs::create_dir_all(fixture.state.root.join(PROJECTATLAS_DIR_NAME))?;
11008        // The selected alias now points at an index belonging to another root.
11009        drop(AtlasStore::open_for_project(
11010            &fixture.target_db,
11011            &fixture.control_root,
11012        )?);
11013        let before = fs::read(&fixture.target_db)?;
11014        let error = ProjectAtlasMcpServer::preflight_purpose_path(&fixture.state, "src/lib.rs")
11015            .err()
11016            .ok_or_else(|| io::Error::other("mismatched preflight was accepted"))?;
11017        require(
11018            matches!(&error, CliError::ProjectMismatch(report)
11019                if report.worktree.as_deref() == Some("purpose-target")),
11020            "purpose preflight lost the selected worktree context",
11021        )?;
11022        let payload = ProjectAtlasMcpServer::encode_error_payload(&error);
11023        let value: serde_json::Value = toon_format::decode_default(&payload)?;
11024        require(
11025            value
11026                .pointer("/error/kind")
11027                .and_then(serde_json::Value::as_str)
11028                == Some("project_mismatch")
11029                && value
11030                    .pointer("/error/project_mismatch/worktree")
11031                    .and_then(serde_json::Value::as_str)
11032                    == Some("purpose-target"),
11033            "MCP mismatch payload lost the selected worktree alias",
11034        )?;
11035        let mut explicit = fixture.state.clone();
11036        explicit.worktree = None;
11037        require(
11038            matches!(
11039                ProjectAtlasMcpServer::preflight_purpose_path(&explicit, "src/lib.rs"),
11040                Err(CliError::ProjectMismatch(report)) if report.worktree.is_none()
11041            ),
11042            "explicit-project preflight acquired an unrelated worktree alias",
11043        )?;
11044        require(
11045            fs::read(&fixture.target_db)? == before,
11046            "rejected purpose preflight changed the mismatched database",
11047        )?;
11048        Ok(())
11049    }
11050
11051    #[test]
11052    fn purpose_mutation_synchronously_rolls_back_request_cancellation()
11053    -> Result<(), Box<dyn std::error::Error>> {
11054        let temp = tempfile::tempdir()?;
11055        let root = temp.path().join("purpose-cancellation");
11056        fs::create_dir_all(root.join(".projectatlas"))?;
11057        fs::write(root.join("source.rs"), "fn current() {}\n")?;
11058        let db_path = root.join(".projectatlas/projectatlas.db");
11059        let plan = ScanRuntimePlan::for_path(None, &root, None)?;
11060        let mut store = open_atlas_store_for_project(&db_path, &plan.root)?;
11061        run_scan_pipeline(
11062            &mut store,
11063            &plan,
11064            &SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, None, None),
11065        )?;
11066        let before_revision = store.authored_purpose_revision()?;
11067        let before_purpose = store
11068            .load_node_by_path("source.rs")?
11069            .ok_or_else(|| io::Error::other("indexed cancellation source missing"))?
11070            .purpose;
11071        drop(store);
11072
11073        let server = ProjectAtlasMcpServer::new(
11074            db_path.clone(),
11075            None,
11076            "purpose-cancellation".to_string(),
11077            false,
11078        );
11079        let state = server.state_for_target(Some(normalize_native_path_display(&root)), None)?;
11080        let control = IndexWorkControl::new(IndexCancellation::new(), None);
11081        let cancelled = Arc::new(std::sync::atomic::AtomicBool::new(false));
11082        let cancellation_probe = Arc::clone(&cancelled);
11083        let bridge = McpRequestCancellationBridge::start_with_probe(
11084            move || cancellation_probe.load(Ordering::Acquire),
11085            &control,
11086        )?;
11087        let result = server.with_admitted_purpose_mutation_controlled(
11088            &state,
11089            &control,
11090            Some(&bridge),
11091            |store| {
11092                store.set_purpose("source.rs", "Canceled purpose", PurposeSource::Agent)?;
11093                cancelled.store(true, Ordering::Release);
11094                Ok(())
11095            },
11096        );
11097        drop(bridge);
11098
11099        require(
11100            matches!(result, Err(CliError::IndexWork(_))),
11101            "request cancellation did not reject the purpose transaction",
11102        )?;
11103        let store = open_atlas_store_for_project(&db_path, &state.root)?;
11104        require(
11105            store.authored_purpose_revision()? == before_revision,
11106            "request cancellation advanced the authored-purpose revision",
11107        )?;
11108        require(
11109            store
11110                .load_node_by_path("source.rs")?
11111                .is_some_and(|node| node.purpose == before_purpose),
11112            "request cancellation persisted the rejected purpose",
11113        )
11114    }
11115
11116    #[test]
11117    fn mcp_same_path_project_identity_rotation_starts_a_distinct_runtime_entry()
11118    -> Result<(), Box<dyn std::error::Error>> {
11119        if telemetry_disabled() {
11120            return Ok(());
11121        }
11122        let temp = tempfile::tempdir()?;
11123        let (state, store) = usage_test_project(temp.path(), "selected-project")?;
11124        let server = ProjectAtlasMcpServer::new(
11125            state.db_path.clone(),
11126            None,
11127            "shared-label".to_string(),
11128            false,
11129        );
11130        let old_project_identity = store.captured_project_binding()?.project_instance_id;
11131        server.record_usage_for_state(&state, &store, |_usage_instance| Ok(()));
11132        let old_runtime_identity = usage_runtime_identity(&server, &state, &store)?;
11133        drop(store);
11134
11135        AtlasStore::transition_project_root(
11136            &state.db_path,
11137            &state.root,
11138            projectatlas_db::ProjectRootTransition::Detach,
11139        )?;
11140        let detached_store = AtlasStore::open_for_project(&state.db_path, &state.root)?;
11141        let detached_project_identity = detached_store
11142            .captured_project_binding()?
11143            .project_instance_id;
11144        server.record_usage_for_state(&state, &detached_store, |_usage_instance| Ok(()));
11145        let detached_runtime_identity = usage_runtime_identity(&server, &state, &detached_store)?;
11146        let tracked = server
11147            .usage_runtime
11148            .lock()
11149            .map_err(|_poisoned| io::Error::other("usage runtime lock poisoned"))?
11150            .entries
11151            .len();
11152
11153        require(
11154            detached_project_identity != old_project_identity,
11155            "detach did not rotate the captured project identity",
11156        )?;
11157        require(
11158            detached_runtime_identity != old_runtime_identity,
11159            "same-path detach reused the previous project's telemetry identity",
11160        )?;
11161        require(
11162            tracked == 2,
11163            "same-path project identities did not retain distinct bounded runtime entries",
11164        )
11165    }
11166
11167    #[test]
11168    fn mcp_telemetry_rotates_inactive_bindings_without_dropping_later_worktrees()
11169    -> Result<(), Box<dyn std::error::Error>> {
11170        if telemetry_disabled() {
11171            return Ok(());
11172        }
11173        let temp = tempfile::tempdir()?;
11174        let (state, control) = usage_test_project(temp.path(), "control")?;
11175        let server =
11176            ProjectAtlasMcpServer::new(state.db_path, None, "worktree-capacity".to_string(), false);
11177        let common = temp.path().join("common.git");
11178        let event = usage_from_estimates_with_context(
11179            "worktree-capacity",
11180            "atlas_overview",
11181            None,
11182            None,
11183            100,
11184            10,
11185            TOKEN_BUCKET_NAVIGATION_AVOIDANCE,
11186            TOKEN_BASELINE_SELECTED_CANDIDATES,
11187            TOKEN_CONFIDENCE_INFERRED,
11188        );
11189        for index in 0..=MCP_TELEMETRY_PROJECT_BINDING_LIMIT {
11190            let registration = control.register_worktree(
11191                &WorktreeAlias::parse(&format!("worktree-{index:03}"))?,
11192                &common,
11193                &common.join(format!("worktrees/{index:03}")),
11194                &format!("{:064x}", index + 1),
11195                &temp.path().join(format!("worktree-{index:03}")),
11196                Some(ProjectInstanceId::from_bytes(
11197                    [u8::try_from(index + 1)?; 16],
11198                )?),
11199                u64::try_from(index + 1)?,
11200            )?;
11201            server.record_usage_for_origin(
11202                &server.control_state,
11203                &control,
11204                Some(registration.registration_id),
11205                |usage_instance| {
11206                    usage_instance.record_for_worktree(
11207                        &control,
11208                        registration.registration_id,
11209                        &event,
11210                    )
11211                },
11212            );
11213        }
11214
11215        require(
11216            control.repository_token_overview()?.calls == MCP_TELEMETRY_PROJECT_BINDING_LIMIT + 1,
11217            "a worktree beyond the in-memory telemetry bound lost its accepted event",
11218        )?;
11219        let runtime = server
11220            .usage_runtime
11221            .lock()
11222            .map_err(|_poisoned| io::Error::other("usage runtime lock poisoned"))?;
11223        require(
11224            runtime.entries.len() == MCP_TELEMETRY_PROJECT_BINDING_LIMIT,
11225            "telemetry project binding registry exceeded its hard bound",
11226        )?;
11227        drop(runtime);
11228        require(
11229            control.telemetry_retention_state()?.active_instance_rows
11230                == MCP_TELEMETRY_PROJECT_BINDING_LIMIT,
11231            "inactive binding rotation did not seal before replacement",
11232        )?;
11233        server.seal_usage_instances_for_projects();
11234        require(
11235            control.telemetry_retention_state()?.active_instance_rows == 0,
11236            "rotated telemetry bindings were not sealed at MCP shutdown",
11237        )
11238    }
11239
11240    #[test]
11241    fn routed_worktree_telemetry_preserves_session_baseline_identity()
11242    -> Result<(), Box<dyn std::error::Error>> {
11243        if telemetry_disabled() {
11244            return Ok(());
11245        }
11246        let temp = tempfile::tempdir()?;
11247        let (state, control) = usage_test_project(temp.path(), "control")?;
11248        let server =
11249            ProjectAtlasMcpServer::new(state.db_path, None, "worktree-scale".to_string(), false);
11250        let common = temp.path().join("common.git");
11251        let event = usage_from_estimates_with_context(
11252            "worktree-scale",
11253            "atlas_overview",
11254            None,
11255            None,
11256            100,
11257            10,
11258            TOKEN_BUCKET_NAVIGATION_AVOIDANCE,
11259            TOKEN_BASELINE_SELECTED_CANDIDATES,
11260            TOKEN_CONFIDENCE_INFERRED,
11261        );
11262        let registration = control.register_worktree(
11263            &WorktreeAlias::parse("worktree-001")?,
11264            &common,
11265            &common.join("worktrees/001"),
11266            &format!("{:064x}", 1),
11267            &temp.path().join("worktree-001"),
11268            Some(ProjectInstanceId::from_bytes([1; 16])?),
11269            1,
11270        )?;
11271        for _ in 0..2 {
11272            server.record_usage_for_origin(
11273                &server.control_state,
11274                &control,
11275                Some(registration.registration_id),
11276                |usage_instance| {
11277                    usage_instance.record_for_worktree(
11278                        &control,
11279                        registration.registration_id,
11280                        &event,
11281                    )
11282                },
11283            );
11284        }
11285
11286        let overview = control.repository_token_overview()?;
11287
11288        require(
11289            overview.calls == 2,
11290            "alias-routed modeled usage did not retain both accepted calls",
11291        )?;
11292        require(
11293            overview.deduped_modeled_tokens_avoided == 80,
11294            "alias-routed modeled usage did not reuse the session baseline witness",
11295        )?;
11296        require(
11297            overview.repeated_baselines_deduped == 1,
11298            "alias-routed modeled usage did not classify the repeated baseline",
11299        )?;
11300        require(
11301            control.telemetry_retention_state()?.active_instance_rows == 1,
11302            "alias-routed usage did not retain one bounded session identity",
11303        )?;
11304        server.seal_usage_instances_for_projects();
11305        require(
11306            control.telemetry_retention_state()?.active_instance_rows == 0,
11307            "alias-routed session identity was not sealed at MCP shutdown",
11308        )
11309    }
11310
11311    #[test]
11312    fn mcp_telemetry_busy_project_does_not_block_another_project()
11313    -> Result<(), Box<dyn std::error::Error>> {
11314        if telemetry_disabled() {
11315            return Ok(());
11316        }
11317        let server = ProjectAtlasMcpServer::new(
11318            PathBuf::from("startup.db"),
11319            None,
11320            "shared-label".to_string(),
11321            false,
11322        );
11323        let temp = tempfile::tempdir()?;
11324        let (state_a, store_a) = usage_test_project(temp.path(), "project-a")?;
11325        let (state_b, store_b) = usage_test_project(temp.path(), "project-b")?;
11326        let (entered_tx, entered_rx) = std::sync::mpsc::sync_channel(1);
11327        let (release_tx, release_rx) = std::sync::mpsc::sync_channel(1);
11328        let server_a = server.clone();
11329        let a_handle = std::thread::spawn(move || -> Result<(), String> {
11330            server_a.record_usage_for_state(&state_a, &store_a, |_usage_instance| {
11331                entered_tx.send(()).map_err(|error| {
11332                    CliError::InvalidInput(format!("test coordination failed: {error}"))
11333                })?;
11334                release_rx
11335                    .recv_timeout(Duration::from_secs(5))
11336                    .map_err(|error| {
11337                        CliError::InvalidInput(format!("test coordination failed: {error}"))
11338                    })?;
11339                Ok(())
11340            });
11341            Ok(())
11342        });
11343        entered_rx.recv_timeout(Duration::from_secs(2))?;
11344
11345        let (done_tx, done_rx) = std::sync::mpsc::sync_channel(1);
11346        let server_b = server;
11347        let b_handle = std::thread::spawn(move || -> Result<(), String> {
11348            server_b.record_usage_for_state(&state_b, &store_b, |_usage_instance| {
11349                done_tx.send(()).map_err(|error| {
11350                    CliError::InvalidInput(format!("test coordination failed: {error}"))
11351                })?;
11352                Ok(())
11353            });
11354            Ok(())
11355        });
11356        let project_b_completed = done_rx.recv_timeout(Duration::from_secs(1)).is_ok();
11357        release_tx.send(())?;
11358        a_handle
11359            .join()
11360            .map_err(|_panic| io::Error::other("project A telemetry thread panicked"))?
11361            .map_err(io::Error::other)?;
11362        b_handle
11363            .join()
11364            .map_err(|_panic| io::Error::other("project B telemetry thread panicked"))?
11365            .map_err(io::Error::other)?;
11366
11367        require(
11368            project_b_completed,
11369            "one project's blocked telemetry delayed another project",
11370        )
11371    }
11372
11373    #[test]
11374    fn mcp_telemetry_keeps_identity_when_capacity_seal_fails()
11375    -> Result<(), Box<dyn std::error::Error>> {
11376        if telemetry_disabled() {
11377            return Ok(());
11378        }
11379        let temp = tempfile::tempdir()?;
11380        let (state, store) = usage_test_project(temp.path(), "selected-project")?;
11381        let server = ProjectAtlasMcpServer::new(
11382            state.db_path.clone(),
11383            None,
11384            "shared-label".to_string(),
11385            false,
11386        );
11387        server.record_usage_for_state(&state, &store, |usage_instance| {
11388            record_usage_estimate(
11389                &store,
11390                Some(usage_instance),
11391                "seal-failure-test",
11392                MCP_EVENT_ATLAS_OVERVIEW,
11393                None,
11394                None,
11395                8,
11396                "overview:\n  files: 1\n",
11397            )
11398        });
11399        let initial_identity = usage_runtime_identity(&server, &state, &store)?;
11400        let busy_connection = rusqlite::Connection::open(&state.db_path)?;
11401        busy_connection.execute_batch("BEGIN IMMEDIATE")?;
11402        let mut calls = 0usize;
11403
11404        server.record_usage_for_state(&state, &store, |_usage_instance| {
11405            calls += 1;
11406            Err(CliError::Db(DbError::TelemetryBaselineCapacity))
11407        });
11408        busy_connection.execute_batch("ROLLBACK")?;
11409
11410        require(calls == 1, "failed sealing unexpectedly retried the event")?;
11411        require(
11412            usage_runtime_identity(&server, &state, &store)? == initial_identity,
11413            "failed sealing replaced the still-active project identity",
11414        )
11415    }
11416
11417    #[test]
11418    fn mcp_telemetry_rotates_and_retries_once_when_baselines_reach_capacity()
11419    -> Result<(), Box<dyn std::error::Error>> {
11420        if telemetry_disabled() {
11421            return Ok(());
11422        }
11423        let temp = tempfile::tempdir()?;
11424        let root = temp.path().join("selected-project");
11425        fs::create_dir_all(root.join(".projectatlas"))?;
11426        let db_path = root.join(".projectatlas").join("projectatlas.db");
11427        let store = AtlasStore::open_for_project(&db_path, &root)?;
11428        let state = McpProjectState {
11429            root,
11430            db_path: db_path.clone(),
11431            config_path: None,
11432            worktree: None,
11433        };
11434        let other_root = temp.path().join("other-project");
11435        fs::create_dir_all(other_root.join(".projectatlas"))?;
11436        let other_db_path = other_root.join(".projectatlas").join("projectatlas.db");
11437        let other_store = AtlasStore::open_for_project(&other_db_path, &other_root)?;
11438        let other_state = McpProjectState {
11439            root: other_root,
11440            db_path: other_db_path,
11441            config_path: None,
11442            worktree: None,
11443        };
11444        let server = ProjectAtlasMcpServer::new(db_path, None, "shared-label".to_string(), false);
11445        server.record_usage_for_state(&state, &store, |usage_instance| {
11446            record_usage_estimate(
11447                &store,
11448                Some(usage_instance),
11449                "rotation-test",
11450                MCP_EVENT_ATLAS_OVERVIEW,
11451                None,
11452                None,
11453                8,
11454                "overview:\n  files: 1\n",
11455            )
11456        });
11457        server.record_usage_for_state(&other_state, &other_store, |_usage_instance| Ok(()));
11458        let initial_identity = usage_runtime_identity(&server, &state, &store)?;
11459        let other_identity = usage_runtime_identity(&server, &other_state, &other_store)?;
11460        let mut calls = 0usize;
11461
11462        server.record_usage_for_state(&state, &store, |_usage_instance| {
11463            calls += 1;
11464            if calls == 1 {
11465                Err(CliError::Db(DbError::TelemetryBaselineCapacity))
11466            } else {
11467                Ok(())
11468            }
11469        });
11470
11471        let tracked = server
11472            .usage_runtime
11473            .lock()
11474            .map_err(|_poisoned| io::Error::other("usage runtime lock poisoned"))?
11475            .entries
11476            .len();
11477        let rotated_identity = usage_runtime_identity(&server, &state, &store)?;
11478        require(calls == 2, "capacity handling did not retry exactly once")?;
11479        require(
11480            rotated_identity != initial_identity,
11481            "capacity handling did not rotate the bounded runtime identity",
11482        )?;
11483        require(
11484            tracked == 2,
11485            "rotation changed the bounded project binding inventory",
11486        )?;
11487        require(
11488            usage_runtime_identity(&server, &other_state, &other_store)? == other_identity,
11489            "one project's capacity rotation changed another project's identity",
11490        )
11491    }
11492
11493    #[test]
11494    fn navigation_result_survives_telemetry_write_failure() -> Result<(), Box<dyn std::error::Error>>
11495    {
11496        if telemetry_disabled() {
11497            return Ok(());
11498        }
11499        let temp = tempfile::tempdir()?;
11500        let repo = temp.path().join("repo");
11501        fs::create_dir_all(repo.join("src"))?;
11502        fs::write(repo.join("src").join("lib.rs"), "pub fn owner() {}\n")?;
11503        let config_path = repo.join(".projectatlas").join("config.toml");
11504        init_project_with_config(&repo, Some(&config_path))?;
11505        let db_path = repo.join(".projectatlas").join("projectatlas.db");
11506        let plan = ScanRuntimePlan::for_path(Some(&config_path), &repo, None)?;
11507        let mut store = open_atlas_store_for_project(&db_path, &repo)?;
11508        run_scan_pipeline(
11509            &mut store,
11510            &plan,
11511            &SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, Some(1), Some(30)),
11512        )?;
11513        drop(store);
11514
11515        let connection = rusqlite::Connection::open(&db_path)?;
11516        connection.execute_batch("BEGIN IMMEDIATE")?;
11517
11518        let server = ProjectAtlasMcpServer::new(
11519            db_path,
11520            Some(config_path),
11521            "shared-label".to_string(),
11522            false,
11523        );
11524        let result = server.atlas_overview_response(
11525            AtlasProjectParams {
11526                project_path: None,
11527                worktree: None,
11528            },
11529            None,
11530        );
11531        connection.execute_batch("ROLLBACK")?;
11532
11533        if result.contains("overview:") && result.contains("files:") {
11534            Ok(())
11535        } else {
11536            Err(io::Error::other(format!(
11537                "telemetry failure replaced an already-built navigation result: {result}"
11538            ))
11539            .into())
11540        }
11541    }
11542
11543    #[test]
11544    fn mcp_calls_share_server_identity_and_new_server_uses_another()
11545    -> Result<(), Box<dyn std::error::Error>> {
11546        if telemetry_disabled() {
11547            return Ok(());
11548        }
11549        let temp = tempfile::tempdir()?;
11550        let repo = temp.path().join("repo");
11551        fs::create_dir_all(repo.join("src"))?;
11552        fs::write(repo.join("src").join("lib.rs"), "pub fn owner() {}\n")?;
11553        let config_path = repo.join(".projectatlas").join("config.toml");
11554        init_project_with_config(&repo, Some(&config_path))?;
11555        let db_path = repo.join(".projectatlas").join("projectatlas.db");
11556        let plan = ScanRuntimePlan::for_path(Some(&config_path), &repo, None)?;
11557        let mut store = open_atlas_store_for_project(&db_path, &repo)?;
11558        run_scan_pipeline(
11559            &mut store,
11560            &plan,
11561            &SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, Some(1), Some(30)),
11562        )?;
11563        drop(store);
11564
11565        let call_overview = |server: &ProjectAtlasMcpServer| {
11566            server.atlas_overview_response(
11567                AtlasProjectParams {
11568                    project_path: None,
11569                    worktree: None,
11570                },
11571                None,
11572            )
11573        };
11574        let first = ProjectAtlasMcpServer::new(
11575            db_path.clone(),
11576            Some(config_path.clone()),
11577            "shared-label".to_string(),
11578            false,
11579        );
11580        require(
11581            call_overview(&first).contains("overview:"),
11582            "first MCP call failed",
11583        )?;
11584        require(
11585            call_overview(&first).contains("overview:"),
11586            "second MCP call failed",
11587        )?;
11588        let restarted = ProjectAtlasMcpServer::new(
11589            db_path.clone(),
11590            Some(config_path),
11591            "shared-label".to_string(),
11592            false,
11593        );
11594        require(
11595            call_overview(&restarted).contains("overview:"),
11596            "restarted MCP call failed",
11597        )?;
11598
11599        let connection = rusqlite::Connection::open(db_path)?;
11600        let instances: i64 = connection.query_row(
11601            "SELECT COUNT(*) FROM usage_instances WHERE owner = 'mcp_process' AND caller_label = 'shared-label'",
11602            [],
11603            |row| row.get(0),
11604        )?;
11605        let events: i64 =
11606            connection.query_row("SELECT COUNT(*) FROM usage_events", [], |row| row.get(0))?;
11607        require(
11608            instances == 2 && events == 3,
11609            "MCP calls did not reuse one identity per server construction",
11610        )?;
11611        drop(connection);
11612        drop(restarted);
11613        drop(first);
11614        fs::remove_dir_all(&repo)?;
11615        require(!repo.exists(), "MCP teardown left its repository in use")
11616    }
11617
11618    #[test]
11619    fn mcp_database_filesystem_failures_are_typed_and_actionable()
11620    -> Result<(), Box<dyn std::error::Error>> {
11621        let error = CliError::Db(projectatlas_db::DbError::DatabaseFilesystemUnsupported {
11622            path: PathBuf::from("project")
11623                .join(".projectatlas")
11624                .join("projectatlas.db"),
11625            mount_point: Some(PathBuf::from("project")),
11626            filesystem_type: Some("nfs".to_string()),
11627        });
11628        let payload = ProjectAtlasMcpServer::encode_error_payload(&error);
11629        require(
11630            payload.contains("kind: database_filesystem_unsupported")
11631                && payload.contains("filesystem_type: nfs")
11632                && payload.contains("supported local filesystem"),
11633            "MCP TOON lost typed filesystem details or recovery guidance",
11634        )
11635    }
11636
11637    #[test]
11638    fn mcp_project_mismatch_preserves_lossless_store_roots()
11639    -> Result<(), Box<dyn std::error::Error>> {
11640        let temp = tempfile::tempdir()?;
11641        let selected_root = temp.path().join("selected-root");
11642        let indexed_root = temp.path().join("indexed-root");
11643        fs::create_dir_all(&selected_root)?;
11644        fs::create_dir_all(&indexed_root)?;
11645        let database = indexed_root.join(".projectatlas").join("projectatlas.db");
11646        fs::create_dir_all(
11647            database
11648                .parent()
11649                .ok_or_else(|| io::Error::other("indexed database path has no parent"))?,
11650        )?;
11651        drop(AtlasStore::open_for_project(&database, &indexed_root)?);
11652
11653        let Err(error) = open_atlas_store_for_project(&database, &selected_root) else {
11654            return Err(io::Error::other("wrong-root store open unexpectedly succeeded").into());
11655        };
11656        let selected_display = CanonicalProjectRoot::from_path(&selected_root)?.display_string()?;
11657        let indexed_display = CanonicalProjectRoot::from_path(&indexed_root)?.display_string()?;
11658        let payload = ProjectAtlasMcpServer::encode_error_payload(&error);
11659        let value: serde_json::Value = toon_format::decode_default(&payload)?;
11660        require(
11661            value.pointer("/error/project_mismatch/selected_project_root")
11662                == Some(&serde_json::Value::String(selected_display.clone()))
11663                && value.pointer("/error/project_mismatch/indexed_project_root")
11664                    == Some(&serde_json::Value::String(indexed_display.clone())),
11665            "MCP omitted lossless roots from a store mismatch",
11666        )?;
11667        require(
11668            payload.contains(&selected_display) && payload.contains(&indexed_display),
11669            "MCP TOON did not retain lossless store roots",
11670        )
11671    }
11672
11673    #[cfg(unix)]
11674    #[test]
11675    fn mcp_project_mismatch_keeps_native_display_unavailable_typed()
11676    -> Result<(), Box<dyn std::error::Error>> {
11677        let temp = tempfile::tempdir()?;
11678        let raw_root = temp
11679            .path()
11680            .join(std::ffi::OsString::from_vec(b"raw-root-\x80".to_vec()));
11681        let replacement_root = temp.path().join("raw-root-�");
11682        fs::create_dir(&raw_root)?;
11683        fs::create_dir(&replacement_root)?;
11684        let raw_identity = CanonicalProjectRoot::from_path(&raw_root)?;
11685        let replacement_identity = CanonicalProjectRoot::from_path(&replacement_root)?;
11686        let error = CliError::ProjectMismatch(Box::new(IndexProjectMismatch::from_native_roots(
11687            &raw_identity,
11688            &replacement_identity,
11689        )));
11690        let payload = ProjectAtlasMcpServer::encode_error_payload(&error);
11691        require(
11692            payload.contains("selected_project_root: null")
11693                && payload.contains("indexed_project_root:")
11694                && payload.contains("raw-root-�"),
11695            "MCP promoted a lossy native root into an ambiguous structured value",
11696        )?;
11697
11698        let mapped = crate::runtime::project_store_error(DbError::ProjectRootMismatch {
11699            expected: raw_root.to_string_lossy().into_owned(),
11700            found: replacement_root.to_string_lossy().into_owned(),
11701            identities: None,
11702        });
11703        let mapped_payload = ProjectAtlasMcpServer::encode_error_payload(&mapped);
11704        require(
11705            mapped_payload.contains("selected_project_root: null")
11706                && mapped_payload.contains("indexed_project_root: null")
11707                && mapped_payload.contains("does not match"),
11708            "MCP promoted lossy store mismatch text into structured roots",
11709        )
11710    }
11711
11712    #[cfg(unix)]
11713    #[test]
11714    fn mcp_recovery_and_session_reports_omit_unavailable_native_root_selectors()
11715    -> Result<(), Box<dyn std::error::Error>> {
11716        let temp = tempfile::tempdir()?;
11717        let raw_root = temp
11718            .path()
11719            .join(std::ffi::OsString::from_vec(b"repo-\x80".to_vec()));
11720        let replacement_root = temp.path().join("repo-�");
11721        fs::create_dir(&raw_root)?;
11722        fs::create_dir(&replacement_root)?;
11723        let raw_db = raw_root.join(".projectatlas").join("projectatlas.db");
11724        let replacement_db = replacement_root
11725            .join(".projectatlas")
11726            .join("projectatlas.db");
11727        let replacement_display = lossless_project_root_display(&replacement_root)
11728            .ok_or_else(|| io::Error::other("replacement root lost its UTF-8 display"))?;
11729
11730        let init_errors = [
11731            (
11732                crate::runtime::index_init_required(&raw_root, &raw_db),
11733                false,
11734            ),
11735            (
11736                crate::runtime::index_init_required(&replacement_root, &replacement_db),
11737                true,
11738            ),
11739        ];
11740        for (error, displayable) in init_errors {
11741            let payload = ProjectAtlasMcpServer::encode_error_payload(&error);
11742            let value: serde_json::Value = toon_format::decode_default(&payload)?;
11743            if displayable {
11744                let expected = serde_json::Value::String(replacement_display.clone());
11745                require(
11746                    value.pointer("/error/init_required/project_root") == Some(&expected)
11747                        && value.pointer("/error/next/project_path") == Some(&expected),
11748                    "MCP init recovery lost a displayable root selector",
11749                )?;
11750            } else {
11751                require(
11752                    value
11753                        .pointer("/error/init_required/project_root")
11754                        .is_some_and(serde_json::Value::is_null)
11755                        && value.pointer("/error/next/project_path").is_none()
11756                        && !payload.contains("repo-�"),
11757                    "MCP init recovery exposed a lossy raw-root selector",
11758                )?;
11759            }
11760        }
11761
11762        let refresh_errors = [
11763            (
11764                CliError::RefreshRequired(Box::new(IndexRefreshRequired {
11765                    project_root: lossless_project_root_display(&raw_root),
11766                    worktree: None,
11767                    status: IndexReadStatus::RefreshRequired,
11768                    reason: IndexRefreshReason::SourceChanged,
11769                    scope: IndexRefreshScope::Incremental,
11770                    changed: 1,
11771                    added: 0,
11772                    removed: 0,
11773                    modified: 1,
11774                    sample_paths: vec!["src/lib.rs".to_string()],
11775                })),
11776                false,
11777            ),
11778            (
11779                CliError::RefreshRequired(Box::new(IndexRefreshRequired {
11780                    project_root: lossless_project_root_display(&replacement_root),
11781                    worktree: None,
11782                    status: IndexReadStatus::RefreshRequired,
11783                    reason: IndexRefreshReason::SourceChanged,
11784                    scope: IndexRefreshScope::Incremental,
11785                    changed: 1,
11786                    added: 0,
11787                    removed: 0,
11788                    modified: 1,
11789                    sample_paths: vec!["src/lib.rs".to_string()],
11790                })),
11791                true,
11792            ),
11793        ];
11794        for (error, displayable) in refresh_errors {
11795            let payload = ProjectAtlasMcpServer::encode_error_payload(&error);
11796            let value: serde_json::Value = toon_format::decode_default(&payload)?;
11797            if displayable {
11798                let expected = serde_json::Value::String(replacement_display.clone());
11799                require(
11800                    value.pointer("/error/refresh_required/project_root") == Some(&expected)
11801                        && value.pointer("/error/next/project_path") == Some(&expected),
11802                    "MCP refresh recovery lost a displayable root selector",
11803                )?;
11804            } else {
11805                require(
11806                    value
11807                        .pointer("/error/refresh_required/project_root")
11808                        .is_some_and(serde_json::Value::is_null)
11809                        && value.pointer("/error/next/project_path").is_none()
11810                        && !payload.contains("repo-�"),
11811                    "MCP refresh recovery exposed a lossy raw-root selector",
11812                )?;
11813            }
11814        }
11815
11816        let raw_server = ProjectAtlasMcpServer::new(raw_db, None, "raw-session".to_string(), false);
11817        let raw_project = serde_json::to_value(ProjectAtlasMcpServer::project_state_payload(
11818            &raw_server.control_state,
11819        ))?;
11820        let raw_capability = serde_json::to_value(
11821            ProjectAtlasMcpServer::selected_project_capability(&raw_server.control_state),
11822        )?;
11823        require(
11824            raw_project.get("root") == Some(&serde_json::Value::Null)
11825                && raw_project.get("db") == Some(&serde_json::Value::Null)
11826                && raw_capability.get("root") == Some(&serde_json::Value::Null)
11827                && raw_capability.get("db") == Some(&serde_json::Value::Null)
11828                && !raw_project.to_string().contains("repo-�")
11829                && !raw_capability.to_string().contains("repo-�"),
11830            "MCP selected-project reports exposed a lossy raw-root projection",
11831        )?;
11832        let raw_brief = raw_server.build_session_brief(
11833            AtlasSessionBriefParams {
11834                project_path: None,
11835                worktree: None,
11836                query: None,
11837                purpose_task: None,
11838                compact: None,
11839                folder_limit: None,
11840                file_limit: None,
11841                blocker_limit: None,
11842                purpose_limit: None,
11843            },
11844            None,
11845        )?;
11846        let raw_value = serde_json::to_value(&raw_brief)?;
11847        require(
11848            raw_value.pointer("/project/root") == Some(&serde_json::Value::Null)
11849                && raw_value
11850                    .pointer("/recommendations/0/arguments/project_path")
11851                    .is_none()
11852                && raw_value
11853                    .pointer("/recommendations/0/arguments/worktree")
11854                    .is_none()
11855                && !raw_value.to_string().contains("repo-�"),
11856            "MCP session brief offered a lossy sibling selector for a raw root",
11857        )?;
11858
11859        let replacement_server = ProjectAtlasMcpServer::new(
11860            replacement_db,
11861            None,
11862            "replacement-session".to_string(),
11863            false,
11864        );
11865        let replacement_project = serde_json::to_value(
11866            ProjectAtlasMcpServer::project_state_payload(&replacement_server.control_state),
11867        )?;
11868        let replacement_capability = serde_json::to_value(
11869            ProjectAtlasMcpServer::selected_project_capability(&replacement_server.control_state),
11870        )?;
11871        require(
11872            replacement_project
11873                .get("root")
11874                .and_then(|value| value.as_str())
11875                == Some(replacement_display.as_str())
11876                && replacement_capability
11877                    .get("root")
11878                    .and_then(|value| value.as_str())
11879                    == Some(replacement_display.as_str()),
11880            "MCP selected-project reports lost the displayable root",
11881        )?;
11882        let replacement_brief = replacement_server.build_session_brief(
11883            AtlasSessionBriefParams {
11884                project_path: None,
11885                worktree: None,
11886                query: None,
11887                purpose_task: None,
11888                compact: None,
11889                folder_limit: None,
11890                file_limit: None,
11891                blocker_limit: None,
11892                purpose_limit: None,
11893            },
11894            None,
11895        )?;
11896        let replacement_value = serde_json::to_value(&replacement_brief)?;
11897        require(
11898            replacement_value
11899                .pointer("/project/root")
11900                .and_then(|value| value.as_str())
11901                == Some(replacement_display.as_str())
11902                && replacement_value
11903                    .pointer("/recommendations/0/arguments/project_path")
11904                    .and_then(|value| value.as_str())
11905                    == Some(replacement_display.as_str()),
11906            "MCP session brief lost the displayable root recovery selector",
11907        )?;
11908        require(
11909            !raw_root.join(".projectatlas").exists()
11910                && !replacement_root.join(".projectatlas").exists(),
11911            "MCP missing-index recovery mutated a project root",
11912        )
11913    }
11914
11915    #[test]
11916    fn mcp_schema_version_mismatches_are_typed_and_content_free()
11917    -> Result<(), Box<dyn std::error::Error>> {
11918        let supported = projectatlas_db::CURRENT_SCHEMA_VERSION;
11919        let future = supported + 1;
11920        let error = CliError::Db(DbError::SchemaVersion {
11921            found: future,
11922            expected: supported,
11923        });
11924        let payload = ProjectAtlasMcpServer::encode_error_payload(&error);
11925        require(
11926            payload.contains("kind: schema_version_mismatch")
11927                && payload.contains(&format!("found_schema_version: {future}"))
11928                && payload.contains(&format!("supported_schema_version: {supported}"))
11929                && payload.contains(env!("CARGO_PKG_VERSION"))
11930                && payload.contains("do not reset")
11931                && !payload.contains(".projectatlas")
11932                && !payload.contains("session_id")
11933                && !payload.contains("project_root"),
11934            "MCP TOON lost typed schema-version details or exposed database context",
11935        )?;
11936
11937        let predecessor = CliError::Service(ServiceError::Db(DbError::SchemaVersion {
11938            found: 8,
11939            expected: supported,
11940        }));
11941        let McpToolTextResult(predecessor_result) =
11942            ProjectAtlasMcpServer::as_mcp_text(Err(predecessor));
11943        let predecessor_payload = predecessor_result.map_err(std::io::Error::other)?;
11944        require(
11945            predecessor_payload.contains("kind: schema_migration_required")
11946                && predecessor_payload.contains("found_schema_version: 8")
11947                && predecessor_payload.contains(&format!("supported_schema_version: {supported}"))
11948                && predecessor_payload
11949                    .contains(&format!("migration_steps_remaining: {}", supported - 8))
11950                && predecessor_payload.contains("projectatlas init")
11951                && predecessor_payload.contains("atlas_init")
11952                && predecessor_payload.contains("same global `--db`/`--config` selection")
11953                && predecessor_payload.contains("same MCP server/database binding")
11954                && !predecessor_payload.contains("schema_version_mismatch")
11955                && !predecessor_payload.contains(crate::SCHEMA_VERSION_MISMATCH_RECOVERY),
11956            "MCP omitted the supported-predecessor migration handoff",
11957        )
11958    }
11959
11960    #[test]
11961    fn mcp_search_capability_failures_are_typed_and_actionable()
11962    -> Result<(), Box<dyn std::error::Error>> {
11963        let error = CliError::Service(ServiceError::SearchCapabilityUnavailable {
11964            requested_mode: projectatlas_service::SearchRetrievalMode::Hybrid,
11965            state: "not-installed",
11966            guidance: "install and build a compatible semantic generation",
11967        });
11968        let payload = ProjectAtlasMcpServer::encode_error_payload(&error);
11969        require(
11970            payload.contains("kind: search_capability_unavailable")
11971                && payload.contains("requested_mode")
11972                && payload.contains("hybrid")
11973                && payload.contains("state")
11974                && payload.contains("not-installed")
11975                && payload.contains("compatible semantic generation"),
11976            "MCP TOON lost typed search-capability state or recovery guidance",
11977        )
11978    }
11979
11980    #[test]
11981    fn mcp_records_usage_only_for_the_accepted_verified_attempt()
11982    -> Result<(), Box<dyn std::error::Error>> {
11983        if telemetry_disabled() {
11984            return Ok(());
11985        }
11986        let temp = tempfile::tempdir()?;
11987        let repo = temp.path().join("repo");
11988        let source = repo.join("src").join("lib.rs");
11989        fs::create_dir_all(
11990            source
11991                .parent()
11992                .ok_or_else(|| io::Error::other("missing parent"))?,
11993        )?;
11994        let original = "pub fn original() {}\n";
11995        let revised = "pub fn revised() {}\n";
11996        fs::write(&source, original)?;
11997        let config_path = repo.join(".projectatlas").join("config.toml");
11998        init_project_with_config(&repo, Some(&config_path))?;
11999        let db_path = repo.join(".projectatlas").join("projectatlas.db");
12000        let plan = ScanRuntimePlan::for_path(Some(&config_path), &repo, None)?;
12001        let mut store = open_atlas_store_for_project(&db_path, &repo)?;
12002        run_scan_pipeline(
12003            &mut store,
12004            &plan,
12005            &SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, Some(1), Some(30)),
12006        )?;
12007        drop(store);
12008        let state = McpProjectState {
12009            root: repo.clone(),
12010            db_path: db_path.clone(),
12011            config_path: Some(config_path.clone()),
12012            worktree: None,
12013        };
12014        let server = ProjectAtlasMcpServer::new(
12015            db_path.clone(),
12016            Some(config_path.clone()),
12017            "accepted-attempt".to_string(),
12018            false,
12019        );
12020        let mut attempts = 0_u64;
12021
12022        let response =
12023            server.with_fresh_string_and_usage_for_request(&state, None, |store, _stamp| {
12024                attempts = attempts.saturating_add(1);
12025                let hash = store
12026                    .load_node_by_path("src/lib.rs")?
12027                    .and_then(|node| node.node.content_hash)
12028                    .ok_or_else(|| CliError::InvalidInput("source hash missing".to_string()))?;
12029                if attempts == 1 {
12030                    fs::write(&source, revised).map_err(|source_error| CliError::Io {
12031                        path: source.clone(),
12032                        source: source_error,
12033                    })?;
12034                    server.source_observations.inject_test_event(
12035                        &db_path,
12036                        &repo,
12037                        Some(&config_path),
12038                        Event::new(EventKind::Modify(ModifyKind::Any)).add_path(source.clone()),
12039                    )?;
12040                }
12041                Ok((
12042                    hash,
12043                    Some(McpUsageIntent::estimate(
12044                        MCP_EVENT_ATLAS_OVERVIEW,
12045                        None,
12046                        None,
12047                        1,
12048                    )),
12049                ))
12050            })?;
12051
12052        require(
12053            attempts >= 2,
12054            "mid-query source edit did not retry the query",
12055        )?;
12056        require(
12057            response == blake3::hash(revised.as_bytes()).to_hex().to_string(),
12058            "MCP returned the provisional pre-edit result",
12059        )?;
12060        let connection = rusqlite::Connection::open(db_path)?;
12061        let events: i64 =
12062            connection.query_row("SELECT COUNT(*) FROM usage_events", [], |row| row.get(0))?;
12063        require(
12064            events == 1,
12065            "MCP telemetry recorded a discarded provisional attempt",
12066        )
12067    }
12068
12069    /// Wait for one admitted task to reach a terminal state.
12070    fn wait_for_background_task(
12071        server: &ProjectAtlasMcpServer,
12072        task_id: &str,
12073    ) -> Result<McpTaskRecord, Box<dyn std::error::Error>> {
12074        for _attempt in 0..5_000 {
12075            let status = server.task_status(task_id.to_string())?;
12076            if let Some(record) = status.task.filter(McpTaskRecord::is_terminal_state) {
12077                return Ok(record);
12078            }
12079            thread::sleep(Duration::from_millis(1));
12080        }
12081        Err(io::Error::other("background task did not reach a terminal state").into())
12082    }
12083
12084    /// Admit one successful task and wait for completion.
12085    fn run_successful_background_task(
12086        server: &ProjectAtlasMcpServer,
12087    ) -> Result<McpTaskRecord, Box<dyn std::error::Error>> {
12088        let task = server.start_index_task(
12089            McpTaskOperation::Scan,
12090            SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, None, None),
12091            MCP_TOOL_ATLAS_OVERVIEW,
12092            |_control, _options| Ok(()),
12093        )?;
12094        wait_for_background_task(server, &task.task_id)
12095    }
12096
12097    /// Wait for the latest task of one operation to reach a terminal state.
12098    fn wait_for_background_operation(
12099        server: &ProjectAtlasMcpServer,
12100        operation: &McpTaskOperation,
12101    ) -> Result<McpTaskRecord, Box<dyn std::error::Error>> {
12102        let task_id = server
12103            .task_registry
12104            .read()
12105            .map_err(|_poisoned| io::Error::other("task registry lock poisoned"))?
12106            .latest_task_id(operation)
12107            .ok_or_else(|| io::Error::other("background task was not admitted"))?;
12108        wait_for_background_task(server, &task_id)
12109    }
12110
12111    /// Assert that persisted index work is visible through normal agent reads.
12112    fn require_agent_index_reads(
12113        server: &ProjectAtlasMcpServer,
12114        project_path: &str,
12115        expected_symbol: &str,
12116    ) -> Result<(), Box<dyn std::error::Error>> {
12117        let overview = server.atlas_overview_response(
12118            AtlasProjectParams {
12119                project_path: Some(project_path.to_string()),
12120                worktree: None,
12121            },
12122            None,
12123        );
12124        require(
12125            overview.contains("overview:"),
12126            "agent overview did not read the published index",
12127        )?;
12128
12129        let summary = server.atlas_file_summary_response(
12130            &AtlasFileSummaryParams {
12131                project_path: Some(project_path.to_string()),
12132                worktree: None,
12133                file: "src/lib.rs".to_string(),
12134                nearest_project: Some(false),
12135                compact: None,
12136                content_selection: None,
12137                limit: Some(25),
12138            },
12139            None,
12140        );
12141        require(
12142            summary.contains("file_summary:")
12143                && summary.contains("src/lib.rs")
12144                && summary.contains(expected_symbol),
12145            "agent file summary omitted published source facts",
12146        )?;
12147
12148        let symbols = server.atlas_symbols_response(
12149            &AtlasSymbolsParams {
12150                project_path: Some(project_path.to_string()),
12151                worktree: None,
12152                file: Some("src/lib.rs".to_string()),
12153                nearest_project: Some(false),
12154                query: None,
12155                content_selection: None,
12156                limit: Some(50),
12157            },
12158            None,
12159        );
12160        require(
12161            symbols.contains("symbols[") && symbols.contains(expected_symbol),
12162            "agent symbol read omitted published parser output",
12163        )?;
12164
12165        let relations = server.atlas_symbol_relations_response(
12166            &AtlasSymbolRelationsParams {
12167                project_path: Some(project_path.to_string()),
12168                file: Some("src/lib.rs".to_string()),
12169                symbol: Some(expected_symbol.to_string()),
12170                nearest_project: Some(false),
12171                query: None,
12172                limit: Some(50),
12173                ..AtlasSymbolRelationsParams::default()
12174            },
12175            None,
12176        );
12177        require(
12178            relations.contains("symbol_relations:") && relations.contains(expected_symbol),
12179            "agent relation read omitted published graph output",
12180        )?;
12181        let explicit_legacy = server.atlas_symbol_relations_response(
12182            &AtlasSymbolRelationsParams {
12183                project_path: Some(project_path.to_string()),
12184                file: Some("src/lib.rs".to_string()),
12185                nearest_project: Some(false),
12186                view: Some("legacy".to_string()),
12187                query: None,
12188                limit: Some(50),
12189                ..AtlasSymbolRelationsParams::default()
12190            },
12191            None,
12192        );
12193        require(
12194            explicit_legacy.contains("relations[") && explicit_legacy.contains(expected_symbol),
12195            "explicit MCP legacy relation view lost its complete small array",
12196        )?;
12197        let compact_legacy = server.atlas_symbol_relations_response(
12198            &AtlasSymbolRelationsParams {
12199                project_path: Some(project_path.to_string()),
12200                file: Some("src/lib.rs".to_string()),
12201                nearest_project: Some(false),
12202                view: Some("legacy".to_string()),
12203                compact: Some(true),
12204                limit: Some(50),
12205                ..AtlasSymbolRelationsParams::default()
12206            },
12207            None,
12208        );
12209        require(
12210            compact_legacy.contains(MCP_ERROR_COMPACT_DETAILED_RELATION_VIEW),
12211            "compact relation projection did not reject the legacy view",
12212        )?;
12213        let zero_limit_legacy = server.atlas_symbol_relations_response(
12214            &AtlasSymbolRelationsParams {
12215                project_path: Some(project_path.to_string()),
12216                file: Some("src/lib.rs".to_string()),
12217                nearest_project: Some(false),
12218                view: Some("legacy".to_string()),
12219                limit: Some(0),
12220                ..AtlasSymbolRelationsParams::default()
12221            },
12222            None,
12223        );
12224        require(
12225            zero_limit_legacy.contains("1..200"),
12226            "MCP legacy zero row bound was accepted",
12227        )?;
12228        let detailed = server.atlas_symbol_relations_response(
12229            &AtlasSymbolRelationsParams {
12230                project_path: Some(project_path.to_string()),
12231                file: Some("src/lib.rs".to_string()),
12232                nearest_project: Some(false),
12233                view: Some("detailed".to_string()),
12234                direction: Some("outbound".to_string()),
12235                limit: Some(50),
12236                ..AtlasSymbolRelationsParams::default()
12237            },
12238            None,
12239        );
12240        require(
12241            detailed.contains("symbol_relations:") && detailed.contains("anchor:"),
12242            "detailed MCP relation route did not return the bounded graph envelope",
12243        )?;
12244        if expected_symbol == "third" {
12245            let compact_detailed = server.atlas_symbol_relations_response(
12246                &AtlasSymbolRelationsParams {
12247                    project_path: Some(project_path.to_string()),
12248                    file: Some("src/lib.rs".to_string()),
12249                    nearest_project: Some(false),
12250                    view: Some("detailed".to_string()),
12251                    compact: Some(true),
12252                    symbol: Some("first".to_string()),
12253                    direction: Some("outbound".to_string()),
12254                    include_occurrences: Some(true),
12255                    limit: Some(1),
12256                    output_bytes: Some(8 * 1_024),
12257                    ..AtlasSymbolRelationsParams::default()
12258                },
12259                None,
12260            );
12261            require(
12262                compact_detailed.len() <= 8 * 1_024
12263                    && compact_detailed.contains("returned: 1")
12264                    && compact_detailed.contains("status: resolved")
12265                    && compact_detailed.contains("confidence: exact")
12266                    && compact_detailed.contains("completeness: complete")
12267                    && compact_detailed.contains("Own café λ relation navigation")
12268                    && compact_detailed.contains("next_call:")
12269                    && compact_detailed.contains("occurrences[1]:")
12270                    && !compact_detailed.contains("occurrences[1]:\n        - relation:"),
12271                "compact detailed relation omitted trust, purpose, occurrence, next-call, or bounded-output behavior",
12272            )?;
12273            let first_detailed_page = server.atlas_symbol_relations_response(
12274                &AtlasSymbolRelationsParams {
12275                    project_path: Some(project_path.to_string()),
12276                    file: Some("src/lib.rs".to_string()),
12277                    nearest_project: Some(false),
12278                    view: Some("detailed".to_string()),
12279                    symbol: Some("first".to_string()),
12280                    direction: Some("outbound".to_string()),
12281                    depth: Some(2),
12282                    limit: Some(1),
12283                    output_bytes: Some(64 * 1024),
12284                    ..AtlasSymbolRelationsParams::default()
12285                },
12286                None,
12287            );
12288            let first_detailed_value: serde_json::Value =
12289                toon_format::decode_default(&first_detailed_page)?;
12290            let first_detailed_report = first_detailed_value
12291                .get("symbol_relations")
12292                .ok_or_else(|| io::Error::other("first detailed MCP page omitted its envelope"))?;
12293            let continuation = first_detailed_report
12294                .get("continuation")
12295                .and_then(serde_json::Value::as_str)
12296                .ok_or_else(|| io::Error::other("first detailed MCP page omitted its cursor"))?;
12297            require(
12298                first_detailed_report
12299                    .get("returned")
12300                    .and_then(serde_json::Value::as_u64)
12301                    == Some(1)
12302                    && first_detailed_report
12303                        .get("rows")
12304                        .and_then(serde_json::Value::as_array)
12305                        .is_some_and(|rows| rows.len() == 1),
12306                "first detailed MCP page was not a nonempty bounded symbol result",
12307            )?;
12308            let second_detailed_page = server.atlas_symbol_relations_response(
12309                &AtlasSymbolRelationsParams {
12310                    project_path: Some(project_path.to_string()),
12311                    file: Some("src/lib.rs".to_string()),
12312                    nearest_project: Some(false),
12313                    view: Some("detailed".to_string()),
12314                    cursor: Some(continuation.to_string()),
12315                    symbol: Some("first".to_string()),
12316                    direction: Some("outbound".to_string()),
12317                    depth: Some(2),
12318                    limit: Some(1),
12319                    output_bytes: Some(64 * 1024),
12320                    ..AtlasSymbolRelationsParams::default()
12321                },
12322                None,
12323            );
12324            let second_detailed_value: serde_json::Value =
12325                toon_format::decode_default(&second_detailed_page)?;
12326            let second_detailed_report = second_detailed_value
12327                .get("symbol_relations")
12328                .ok_or_else(|| io::Error::other("second detailed MCP page omitted its envelope"))?;
12329            require(
12330                second_detailed_report
12331                    .get("returned")
12332                    .and_then(serde_json::Value::as_u64)
12333                    == Some(1)
12334                    && second_detailed_report.get("rows") != first_detailed_report.get("rows")
12335                    && second_detailed_page.contains("Own café λ relation navigation"),
12336                "detailed MCP cursor did not resume a distinct Unicode-safe symbol row",
12337            )?;
12338            let compact_continuation_page = server.atlas_symbol_relations_response(
12339                &AtlasSymbolRelationsParams {
12340                    project_path: Some(project_path.to_string()),
12341                    file: Some("src/lib.rs".to_string()),
12342                    nearest_project: Some(false),
12343                    view: Some("detailed".to_string()),
12344                    compact: Some(true),
12345                    symbol: Some("first".to_string()),
12346                    symbol_parent: Some(String::new()),
12347                    direction: Some("outbound".to_string()),
12348                    depth: Some(2),
12349                    limit: Some(1),
12350                    output_bytes: Some(64 * 1024),
12351                    ..AtlasSymbolRelationsParams::default()
12352                },
12353                None,
12354            );
12355            let compact_continuation_value: serde_json::Value =
12356                toon_format::decode_default(&compact_continuation_page)?;
12357            let compact_continuation_report = compact_continuation_value
12358                .get("symbol_relations")
12359                .ok_or_else(|| io::Error::other("compact relation page omitted its envelope"))?;
12360            let compact_next_call = compact_continuation_report
12361                .get("next_call")
12362                .ok_or_else(|| io::Error::other("compact relation page omitted its next call"))?;
12363            require(
12364                compact_next_call
12365                    .get("tool")
12366                    .and_then(serde_json::Value::as_str)
12367                    == Some(MCP_TOOL_ATLAS_SYMBOL_RELATIONS),
12368                "compact relation continuation did not name its owning MCP tool",
12369            )?;
12370            let compact_next_arguments = compact_next_call
12371                .get("arguments")
12372                .cloned()
12373                .ok_or_else(|| io::Error::other("compact next call omitted its arguments"))?;
12374            require(
12375                compact_next_arguments.get("cursor").is_some()
12376                    && compact_next_arguments.get("symbol_parent").is_none(),
12377                "compact next call did not preserve its cursor or normalize an empty parent",
12378            )?;
12379            let compact_next_params: AtlasSymbolRelationsParams =
12380                serde_json::from_value(compact_next_arguments)?;
12381            let compact_resumed_page =
12382                server.atlas_symbol_relations_response(&compact_next_params, None);
12383            require(
12384                compact_resumed_page.contains("symbol_relations:")
12385                    && !compact_resumed_page.contains("cursor does not match query")
12386                    && !compact_resumed_page.contains("graph symbol anchor is not available"),
12387                "compact relation next call was not directly reusable",
12388            )?;
12389        }
12390        let bounded_output_bytes = 4 * 1024_u32;
12391        let bounded = server.atlas_symbol_relations_response(
12392            &AtlasSymbolRelationsParams {
12393                project_path: Some(project_path.to_string()),
12394                file: Some("src/lib.rs".to_string()),
12395                nearest_project: Some(false),
12396                view: Some("detailed".to_string()),
12397                direction: Some("outbound".to_string()),
12398                limit: Some(50),
12399                edge_limit: Some(50),
12400                node_limit: Some(50),
12401                visited_limit: Some(50),
12402                occurrence_total_limit: Some(50),
12403                intermediate_bytes: Some(128 * 1024),
12404                deadline_ms: Some(2_000),
12405                output_bytes: Some(bounded_output_bytes),
12406                ..AtlasSymbolRelationsParams::default()
12407            },
12408            None,
12409        );
12410        require(
12411            bounded.contains("symbol_relations:")
12412                && bounded.len() <= bounded_output_bytes as usize
12413                && bounded.contains("Own café λ relation navigation")
12414                && bounded.contains(&format!("rendered_output_bytes: {}", bounded.len())),
12415            "detailed MCP relation output did not enforce or report the exact routed envelope bytes",
12416        )?;
12417
12418        let analysis = server.atlas_symbol_relations_response(
12419            &AtlasSymbolRelationsParams {
12420                project_path: Some(project_path.to_string()),
12421                file: Some("src/lib.rs".to_string()),
12422                nearest_project: Some(false),
12423                view: Some("analysis".to_string()),
12424                symbol: Some("first".to_string()),
12425                direction: Some("outbound".to_string()),
12426                depth: Some(2),
12427                limit: Some(50),
12428                output_bytes: Some(64 * 1024),
12429                include_communities: Some(true),
12430                include_cycles: Some(true),
12431                ..AtlasSymbolRelationsParams::default()
12432            },
12433            None,
12434        );
12435        require(
12436            analysis.contains("symbol_relations:")
12437                && analysis.contains("mode: architecture")
12438                && analysis.contains("findings[")
12439                && analysis.contains("next_call:")
12440                && analysis.contains("work:"),
12441            "MCP relation analysis omitted its closed mode, findings, work, or reusable next call",
12442        )?;
12443        let entrypoint_anchor = serde_json::to_string(&RelationAnchor::File {
12444            file: RepositoryFilePath::new(Path::new("src/lib.rs"))?,
12445        })?;
12446        let entrypoint = server.atlas_symbol_relations_response(
12447            &AtlasSymbolRelationsParams {
12448                project_path: Some(project_path.to_string()),
12449                file: None,
12450                nearest_project: Some(false),
12451                view: Some("analysis".to_string()),
12452                direction: Some("outbound".to_string()),
12453                resolution: Some("any".to_string()),
12454                depth: Some(2),
12455                limit: Some(50),
12456                output_bytes: Some(64 * 1024),
12457                analysis_mode: Some("entrypoint".to_string()),
12458                profile_name: Some("mcp-entrypoint".to_string()),
12459                entrypoints: Some(vec![entrypoint_anchor.clone()]),
12460                profile_relations: Some(vec!["calls".to_string()]),
12461                ..AtlasSymbolRelationsParams::default()
12462            },
12463            None,
12464        );
12465        require(
12466            entrypoint.contains("symbol_relations:")
12467                && entrypoint.contains("mode: entrypoint")
12468                && entrypoint.contains("entrypoint_profile:")
12469                && entrypoint.contains("coverage:"),
12470            "MCP entrypoint analysis did not serialize the shared profile contract",
12471        )?;
12472        let entrypoint_symbol_conflict = server.atlas_symbol_relations_response(
12473            &AtlasSymbolRelationsParams {
12474                project_path: Some(project_path.to_string()),
12475                file: None,
12476                nearest_project: Some(false),
12477                view: Some("analysis".to_string()),
12478                direction: Some("outbound".to_string()),
12479                resolution: Some("any".to_string()),
12480                depth: Some(2),
12481                limit: Some(50),
12482                output_bytes: Some(64 * 1024),
12483                analysis_mode: Some("entrypoint".to_string()),
12484                symbol: Some("first".to_string()),
12485                entrypoints: Some(vec![entrypoint_anchor]),
12486                profile_relations: Some(vec!["calls".to_string()]),
12487                ..AtlasSymbolRelationsParams::default()
12488            },
12489            None,
12490        );
12491        require(
12492            entrypoint_symbol_conflict.contains(MCP_ERROR_ENTRYPOINT_SYMBOL_SELECTOR),
12493            "MCP silently ignored a symbol selector alongside explicit entrypoint anchors",
12494        )?;
12495
12496        let state = ProjectAtlasMcpServer::project_state_from_root(Path::new(project_path))?;
12497        let publication_before = ProjectAtlasMcpServer::open_read_store(&state)?
12498            .index_publication()?
12499            .ok_or_else(|| io::Error::other("MCP impact fixture publication missing"))?;
12500        let task_records_before = server
12501            .task_registry
12502            .read()
12503            .map_err(|_poisoned| io::Error::other("task registry lock poisoned"))?
12504            .len();
12505        let impact_started = Instant::now();
12506        let impact = server.atlas_symbol_relations_response(
12507            &AtlasSymbolRelationsParams {
12508                project_path: Some(project_path.to_string()),
12509                file: Some("src/lib.rs".to_string()),
12510                nearest_project: Some(false),
12511                view: Some("analysis".to_string()),
12512                symbol: Some("first".to_string()),
12513                direction: Some("outbound".to_string()),
12514                depth: Some(2),
12515                limit: Some(8),
12516                edge_limit: Some(8),
12517                node_limit: Some(16),
12518                visited_limit: Some(16),
12519                occurrence_total_limit: Some(16),
12520                intermediate_bytes: Some(128 * 1_024),
12521                deadline_ms: Some(1_000),
12522                output_bytes: Some(64 * 1_024),
12523                analysis_mode: Some("impact".to_string()),
12524                vcs: Some("working_tree".to_string()),
12525                include_dead_code: Some(true),
12526                ..AtlasSymbolRelationsParams::default()
12527            },
12528            None,
12529        );
12530        let impact_elapsed = impact_started.elapsed();
12531        require(
12532            impact_elapsed <= Duration::from_secs(5)
12533                && impact.contains("mode: impact")
12534                && (impact.contains("state: available") || impact.contains("state: unavailable")),
12535            "MCP impact analysis exceeded its elapsed tolerance or omitted typed mode/VCS state",
12536        )?;
12537        let impact_value: serde_json::Value = toon_format::decode_default(&impact)?;
12538        let impact_report = impact_value
12539            .get(MCP_PAYLOAD_SYMBOL_RELATIONS)
12540            .ok_or_else(|| io::Error::other("MCP impact response omitted its envelope"))?;
12541        let bounded_work = [
12542            ("/returned", 8_u64),
12543            ("/work/relations/inspected_edges", 8),
12544            ("/work/relations/active_nodes", 16),
12545            ("/work/relations/visited_nodes", 16),
12546            ("/work/analyzed_nodes", 16),
12547            ("/work/analyzed_edges", 8),
12548            ("/work/peak_intermediate_bytes", 128 * 1_024),
12549            ("/work/rendered_output_bytes", 64 * 1_024),
12550        ];
12551        require(
12552            impact.len() <= 64 * 1_024
12553                && bounded_work.iter().all(|(path, limit)| {
12554                    impact_report
12555                        .pointer(path)
12556                        .and_then(serde_json::Value::as_u64)
12557                        .is_some_and(|observed| observed <= *limit)
12558                }),
12559            "MCP impact analysis crossed or omitted a declared row/node/edge/visited/intermediate/output budget",
12560        )?;
12561        require(
12562            ProjectAtlasMcpServer::open_read_store(&state)?
12563                .index_publication()?
12564                .as_ref()
12565                == Some(&publication_before)
12566                && server
12567                    .task_registry
12568                    .read()
12569                    .map_err(|_poisoned| io::Error::other("task registry lock poisoned"))?
12570                    .len()
12571                    == task_records_before,
12572            "read-only MCP impact analysis changed publication or retained a task record",
12573        )?;
12574        let follow_up_started = Instant::now();
12575        let follow_up = server.atlas_overview_response(
12576            AtlasProjectParams {
12577                project_path: Some(project_path.to_string()),
12578                worktree: None,
12579            },
12580            None,
12581        );
12582        require(
12583            follow_up_started.elapsed() <= Duration::from_secs(2)
12584                && follow_up.contains("overview:"),
12585            "immediate MCP follow-up read was not responsive after bounded impact analysis",
12586        )?;
12587
12588        let trace = server.atlas_symbol_relations_response(
12589            &AtlasSymbolRelationsParams {
12590                project_path: Some(project_path.to_string()),
12591                file: Some("src/lib.rs".to_string()),
12592                nearest_project: Some(false),
12593                view: Some("analysis".to_string()),
12594                symbol: Some("first".to_string()),
12595                direction: Some("outbound".to_string()),
12596                depth: Some(2),
12597                limit: Some(50),
12598                analysis_mode: Some("trace".to_string()),
12599                trace_target: Some("second".to_string()),
12600                trace_target_file: Some("src/lib.rs".to_string()),
12601                trace_target_kind: Some("function".to_string()),
12602                trace_target_signature: Some("fn second ( )".to_string()),
12603                ..AtlasSymbolRelationsParams::default()
12604            },
12605            None,
12606        );
12607        require(
12608            trace.contains("mode: trace")
12609                && trace.contains("kind: static_trace")
12610                && trace.contains("status: confirmed")
12611                && trace.contains("name: second")
12612                && trace.contains("capability: symbol_slice"),
12613            "MCP trace analysis omitted its confirmed path or reusable exact selector",
12614        )?;
12615
12616        let misplaced = server.atlas_symbol_relations_response(
12617            &AtlasSymbolRelationsParams {
12618                project_path: Some(project_path.to_string()),
12619                file: Some("src/lib.rs".to_string()),
12620                nearest_project: Some(false),
12621                view: Some("detailed".to_string()),
12622                analysis_mode: Some("impact".to_string()),
12623                ..AtlasSymbolRelationsParams::default()
12624            },
12625            None,
12626        );
12627        require(
12628            misplaced.contains("analysis controls require view=analysis"),
12629            "MCP detailed relation view accepted analysis-only controls",
12630        )?;
12631
12632        let missing_trace_target = server.atlas_symbol_relations_response(
12633            &AtlasSymbolRelationsParams {
12634                project_path: Some(project_path.to_string()),
12635                file: Some("src/lib.rs".to_string()),
12636                nearest_project: Some(false),
12637                view: Some("analysis".to_string()),
12638                analysis_mode: Some("trace".to_string()),
12639                ..AtlasSymbolRelationsParams::default()
12640            },
12641            None,
12642        );
12643        require(
12644            missing_trace_target.contains("analysis trace requires an exact file or symbol target"),
12645            "MCP trace analysis accepted a missing exact target",
12646        )?;
12647
12648        let misplaced_vcs = server.atlas_symbol_relations_response(
12649            &AtlasSymbolRelationsParams {
12650                project_path: Some(project_path.to_string()),
12651                file: Some("src/lib.rs".to_string()),
12652                nearest_project: Some(false),
12653                view: Some("analysis".to_string()),
12654                analysis_mode: Some("architecture".to_string()),
12655                vcs: Some("working_tree".to_string()),
12656                ..AtlasSymbolRelationsParams::default()
12657            },
12658            None,
12659        );
12660        require(
12661            misplaced_vcs.contains("VCS selection is valid only for impact analysis"),
12662            "MCP silently dropped an explicit VCS selector outside impact mode",
12663        )
12664    }
12665
12666    #[test]
12667    fn current_dir_alias_paths_use_active_mcp_project() -> Result<(), Box<dyn std::error::Error>> {
12668        let temp = tempfile::tempdir()?;
12669        let repo = temp.path().join("repo-a");
12670        fs::create_dir(&repo)?;
12671        let db_path = repo.join(".projectatlas").join("projectatlas.db");
12672        let server = ProjectAtlasMcpServer::new(db_path, None, "mcp-test".to_string(), false);
12673        let expected_root = canonical_project_root(&repo)?;
12674
12675        let (_state, root) =
12676            server.state_and_root_path(None, None, Some("./".to_string()), false)?;
12677        require(
12678            root == expected_root,
12679            "current-dir alias did not use active root",
12680        )?;
12681
12682        #[cfg(windows)]
12683        {
12684            let (_state, root) =
12685                server.state_and_root_path(None, None, Some(".\\".to_string()), false)?;
12686            require(
12687                root == expected_root,
12688                "windows current-dir alias did not use active root",
12689            )?;
12690        }
12691
12692        Ok(())
12693    }
12694
12695    #[test]
12696    fn worktree_list_retains_retired_rows_at_structural_capacity()
12697    -> Result<(), Box<dyn std::error::Error>> {
12698        let temp = tempfile::tempdir()?;
12699        let primary = temp.path().join("control");
12700        fs::create_dir(&primary)?;
12701        run_fixture_command(StdCommand::new("git").current_dir(&primary).arg("init"))?;
12702        let git_directory = primary.join(".git");
12703        let structural_registrations = git_directory.join("worktrees");
12704        fs::create_dir(&structural_registrations)?;
12705        for index in 0..MAX_GIT_WORKTREE_REGISTRATIONS {
12706            let administrative_directory =
12707                structural_registrations.join(format!("missing-{index:04}"));
12708            fs::create_dir(&administrative_directory)?;
12709            fs::write(
12710                administrative_directory.join("gitdir"),
12711                temp.path()
12712                    .join(format!("missing-{index:04}"))
12713                    .join(".git")
12714                    .to_string_lossy()
12715                    .as_bytes(),
12716            )?;
12717        }
12718
12719        let database = primary.join(".projectatlas").join("projectatlas.db");
12720        fs::create_dir_all(
12721            database
12722                .parent()
12723                .ok_or_else(|| io::Error::other("control database has no parent"))?,
12724        )?;
12725        let store = AtlasStore::open_for_project(&database, &primary)?;
12726        let retired_alias = WorktreeAlias::parse("retired-at-capacity")?;
12727        let retired_registration = store.register_worktree(
12728            &retired_alias,
12729            &git_directory,
12730            &structural_registrations.join("retired"),
12731            &"ab".repeat(32),
12732            &temp.path().join("retired"),
12733            None,
12734            1,
12735        )?;
12736        drop(store);
12737
12738        let server =
12739            ProjectAtlasMcpServer::new(database.clone(), None, "worktree-list".to_string(), false);
12740        let active = server.atlas_worktree_list(Parameters(AtlasWorktreeListParams {
12741            include_retired: Some(false),
12742        }));
12743        require(
12744            active.contains("total_worktrees: 1026")
12745                && active.contains("truncated: false")
12746                && active.contains("retired-at-capacity")
12747                && active.contains("missing-1023"),
12748            "combined capacity starved a structural or active missing registration",
12749        )?;
12750
12751        let store = AtlasStore::open_for_project(&database, &primary)?;
12752        store.retire_worktree(retired_registration.registration_id, &retired_alias, 2)?;
12753        drop(store);
12754
12755        let listed = server.atlas_worktree_list(Parameters(AtlasWorktreeListParams {
12756            include_retired: Some(true),
12757        }));
12758        require(
12759            listed.contains("total_worktrees: 1025") && listed.contains("retired-at-capacity"),
12760            "full structural inventory starved the requested retired registration",
12761        )
12762    }
12763
12764    #[test]
12765    fn worktree_remove_retains_display_for_git_known_missing_registration()
12766    -> Result<(), Box<dyn std::error::Error>> {
12767        let fixture = registered_worktree_race_fixture("missing-remove")?;
12768        fs::remove_file(fixture.linked.join(".git"))?;
12769        let repository = fixture.server.control_git_repository()?;
12770        let missing_entry = repository
12771            .worktrees
12772            .iter()
12773            .find(|entry| entry.administrative_directory == fixture.administrative_directory)
12774            .ok_or_else(|| io::Error::other("missing Git worktree entry was not discovered"))?;
12775        require(
12776            matches!(missing_entry.state, GitWorktreeState::Missing { .. }),
12777            "Git-known missing worktree did not retain its structural entry",
12778        )?;
12779        let root_display = normalize_native_path_display(&fixture.linked.canonicalize()?);
12780        let registrations =
12781            AtlasStore::open_for_project(&fixture.control_db, &fixture.server.control_state.root)?
12782                .worktree_registrations(false)?;
12783        let listed = fixture.server.worktree_list_row(
12784            &repository.common_directory,
12785            missing_entry,
12786            &registrations,
12787        );
12788        require(
12789            listed.root.as_deref() == Some(root_display.as_str())
12790                && matches!(listed.path_display, McpWorktreePathDisplayState::Available)
12791                && matches!(listed.git_state, McpGitWorktreeState::Missing)
12792                && matches!(
12793                    listed.registration,
12794                    McpWorktreeRegistrationState::Registered
12795                ),
12796            "Git-known missing worktree listing lost its retained root",
12797        )?;
12798        let removed = fixture
12799            .server
12800            .atlas_worktree_remove(Parameters(AtlasWorktreeRemoveParams {
12801                worktree: fixture.alias.to_string(),
12802            }));
12803        require(
12804            removed.contains("status: retired")
12805                && removed.contains("path_display: available")
12806                && removed.contains(&root_display)
12807                && removed.contains(MCP_WORKTREE_MISSING_RETENTION_REASON),
12808            &format!(
12809                "Git-known missing worktree retirement lost its retained display identity: {removed}"
12810            ),
12811        )?;
12812        Ok(())
12813    }
12814
12815    #[test]
12816    fn worktree_list_retains_one_invalid_registration_after_administrative_replacement()
12817    -> Result<(), Box<dyn std::error::Error>> {
12818        for replacement in ["file", "link"] {
12819            let fixture = registered_worktree_race_fixture("invalid-registration")?;
12820            let preserved = fixture.primary.join("preserved-administrative");
12821            let target = fixture.primary.join("unrelated-target");
12822            fs::create_dir(&target)?;
12823            fs::write(target.join("canary"), "unrelated")?;
12824            fs::rename(&fixture.administrative_directory, &preserved)?;
12825            if replacement == "file" {
12826                fs::write(&fixture.administrative_directory, "replacement")?;
12827            } else {
12828                #[cfg(unix)]
12829                std::os::unix::fs::symlink(&target, &fixture.administrative_directory)?;
12830                #[cfg(windows)]
12831                {
12832                    let parent = fixture
12833                        .administrative_directory
12834                        .parent()
12835                        .ok_or_else(|| io::Error::other("administrative path has no parent"))?;
12836                    let name = fixture
12837                        .administrative_directory
12838                        .file_name()
12839                        .ok_or_else(|| io::Error::other("administrative path has no name"))?;
12840                    run_fixture_command(
12841                        StdCommand::new("cmd")
12842                            .args(["/D", "/C", "mklink", "/J"])
12843                            .arg(parent.canonicalize()?.join(name))
12844                            .arg(target.canonicalize()?),
12845                    )?;
12846                }
12847            }
12848            let listed = fixture
12849                .server
12850                .atlas_worktree_list(Parameters(AtlasWorktreeListParams {
12851                    include_retired: Some(false),
12852                }));
12853            let value: serde_json::Value = toon_format::decode_default(&listed)?;
12854            let rows = value
12855                .pointer("/worktrees/worktrees")
12856                .and_then(serde_json::Value::as_array)
12857                .ok_or_else(|| io::Error::other(format!("missing worktree rows: {listed}")))?;
12858            require(
12859                rows.len() == 2
12860                    && rows
12861                        .iter()
12862                        .filter(|row| {
12863                            row.get("alias").and_then(serde_json::Value::as_str)
12864                                == Some(fixture.alias.as_str())
12865                                && row.get("git_state").and_then(serde_json::Value::as_str)
12866                                    == Some("invalid")
12867                                && row.get("registration").and_then(serde_json::Value::as_str)
12868                                    == Some("registered")
12869                                && row.get("atlas_state").and_then(serde_json::Value::as_str)
12870                                    == Some("unavailable")
12871                        })
12872                        .count()
12873                        == 1,
12874                &format!("{replacement} replacement split its registered invalid row: {listed}"),
12875            )?;
12876            let removed =
12877                fixture
12878                    .server
12879                    .atlas_worktree_remove(Parameters(AtlasWorktreeRemoveParams {
12880                        worktree: fixture.alias.to_string(),
12881                    }));
12882            let control =
12883                open_atlas_store_read_only_for_project(&fixture.control_db, &fixture.control_root)?;
12884            require(
12885                removed.contains("cannot retire worktree")
12886                    && removed.contains("Git evidence is invalid")
12887                    && control.worktree_registration(&fixture.alias)?.state
12888                        == WorktreeRegistrationState::Active,
12889                &format!(
12890                    "{replacement} invalid evidence allowed registration retirement: {removed}"
12891                ),
12892            )?;
12893            require(
12894                fixture
12895                    .server
12896                    .state_for_target(None, Some(fixture.alias.to_string()))
12897                    .is_err()
12898                    && fs::read_to_string(target.join("canary"))? == "unrelated"
12899                    && !target.join(PROJECTATLAS_DIR_NAME).exists(),
12900                "invalid administrative replacement became an actionable target",
12901            )?;
12902        }
12903        Ok(())
12904    }
12905
12906    #[cfg(windows)]
12907    #[test]
12908    fn windows_worktree_selector_survives_live_to_retained_transition()
12909    -> Result<(), Box<dyn std::error::Error>> {
12910        let common = PathBuf::from(r"C:\repo\.git");
12911        let root = PathBuf::from(r"C:\repo\linked");
12912        for (index, administrative_directory) in [
12913            PathBuf::from(r"C:\repo\.git\worktrees\linked"),
12914            PathBuf::from(r"\\?\C:\repo\.git\worktrees\CON"),
12915        ]
12916        .into_iter()
12917        .enumerate()
12918        {
12919            let entry = GitWorktreeEntry {
12920                role: GitWorktreeRole::Linked,
12921                administrative_directory: administrative_directory.clone(),
12922                state: GitWorktreeState::Active {
12923                    git_control_path: root.join(".git"),
12924                    root: root.clone(),
12925                },
12926            };
12927            let selector = ProjectAtlasMcpServer::worktree_candidate_selector(&entry);
12928            let alias = WorktreeAlias::parse(&format!("retained-{index}"))?;
12929            let registration = WorktreeRegistration {
12930                registration_id: i64::try_from(index + 1)?,
12931                alias: alias.clone(),
12932                state: WorktreeRegistrationState::Active,
12933                git_common_directory: normalize_native_path_display(&common),
12934                git_common_directory_identity: CanonicalProjectRoot::from_persisted_path(
12935                    common.clone(),
12936                )?,
12937                git_administrative_directory: normalize_native_path_display(
12938                    &administrative_directory,
12939                ),
12940                git_administrative_directory_identity: CanonicalProjectRoot::from_persisted_path(
12941                    administrative_directory,
12942                )?,
12943                git_administrative_identity: "ab".repeat(32),
12944                last_root: normalize_native_path_display(&root),
12945                last_root_identity: CanonicalProjectRoot::from_persisted_path(root.clone())?,
12946                project_instance_id: None,
12947                accepted_telemetry_revision: 0,
12948                created_at_epoch: 1,
12949                retired_at_epoch: None,
12950            };
12951            let retained = ProjectAtlasMcpServer::missing_registered_worktree_row(&registration);
12952            require(
12953                retained.selector.as_deref() == Some(selector.as_str())
12954                    && retained.alias.as_deref() == Some(alias.as_str()),
12955                "live-to-retained transition changed the selector or lost the removal alias",
12956            )?;
12957        }
12958        Ok(())
12959    }
12960
12961    #[cfg(unix)]
12962    #[test]
12963    fn non_utf8_worktree_identities_retain_native_join_and_display_state()
12964    -> Result<(), Box<dyn std::error::Error>> {
12965        let temp = tempfile::tempdir()?;
12966        let control_root = temp.path().join("control");
12967        let common_directory = control_root.join(".git");
12968        fs::create_dir_all(&common_directory)?;
12969        let database = control_root.join(".projectatlas").join("projectatlas.db");
12970        fs::create_dir_all(
12971            database
12972                .parent()
12973                .ok_or_else(|| io::Error::other("control database has no parent"))?,
12974        )?;
12975        let store = AtlasStore::open_for_project(&database, &control_root)?;
12976        let server =
12977            ProjectAtlasMcpServer::new(database, None, "non-utf8-worktrees".to_string(), false);
12978        let mut entries = Vec::new();
12979
12980        for (index, terminal_byte) in [0xff, 0xfe].into_iter().enumerate() {
12981            let name = std::ffi::OsString::from_vec(vec![b'w', b't', b'-', terminal_byte]);
12982            let administrative_directory = common_directory.join("worktrees").join(&name);
12983            let root = temp.path().join(name);
12984            fs::create_dir_all(&administrative_directory)?;
12985            fs::create_dir_all(&root)?;
12986            let entry = GitWorktreeEntry {
12987                role: GitWorktreeRole::Linked,
12988                administrative_directory: administrative_directory.clone(),
12989                state: GitWorktreeState::Active {
12990                    git_control_path: root.join(".git"),
12991                    root: root.clone(),
12992                },
12993            };
12994            let shadow = WorktreeRegistration {
12995                registration_id: i64::try_from(index + 1)?,
12996                alias: WorktreeAlias::parse(&format!("shadow-{index}"))?,
12997                state: WorktreeRegistrationState::Active,
12998                git_common_directory: normalize_native_path_display(&common_directory),
12999                git_common_directory_identity: CanonicalProjectRoot::from_path(&common_directory)?,
13000                git_administrative_directory: normalize_native_path_display(
13001                    &administrative_directory,
13002                ),
13003                git_administrative_directory_identity: CanonicalProjectRoot::from_path(
13004                    &administrative_directory,
13005                )?,
13006                git_administrative_identity: "ab".repeat(32),
13007                last_root: normalize_native_path_display(&root),
13008                last_root_identity: CanonicalProjectRoot::from_path(&root)?,
13009                project_instance_id: None,
13010                accepted_telemetry_revision: 0,
13011                created_at_epoch: 1,
13012                retired_at_epoch: None,
13013            };
13014            let row = server.worktree_list_row(&common_directory, &entry, &[shadow]);
13015            require(
13016                row.selector.is_some()
13017                    && row
13018                        .alias
13019                        .as_deref()
13020                        .is_some_and(|alias| alias == format!("shadow-{index}"))
13021                    && matches!(row.registration, McpWorktreeRegistrationState::Registered)
13022                    && matches!(row.atlas_state, McpWorktreeAtlasState::Invalid)
13023                    && row.root.is_none()
13024                    && matches!(row.path_display, McpWorktreePathDisplayState::Unavailable),
13025                "non-UTF-8 structural row lost its native selector or typed display state",
13026            )?;
13027            entries.push(entry);
13028        }
13029
13030        let first_selector = ProjectAtlasMcpServer::worktree_candidate_selector(&entries[0]);
13031        require(
13032            first_selector != ProjectAtlasMcpServer::worktree_candidate_selector(&entries[1]),
13033            "native administrative identities collapsed into one selector",
13034        )?;
13035        let repository = GitRepositoryStructure {
13036            common_directory,
13037            selection: projectatlas_fs::worktree::GitRepositorySelection::CommonManager {
13038                source_selection: projectatlas_fs::worktree::GitManagerSourceSelection::Ambiguous {
13039                    worktree_count: entries.len(),
13040                },
13041            },
13042            worktrees: entries,
13043        };
13044        let candidates = server.matching_worktree_candidates(&repository, &first_selector);
13045        require(
13046            candidates.len() == 1
13047                && ProjectAtlasMcpServer::worktree_candidate(
13048                    &repository.common_directory,
13049                    candidates[0],
13050                )
13051                .is_some_and(|candidate| {
13052                    candidate.selector == first_selector
13053                        && candidate.root.is_none()
13054                        && matches!(
13055                            candidate.path_display,
13056                            McpWorktreePathDisplayState::Unavailable
13057                        )
13058                }),
13059            "non-UTF-8 structural identity was not selectable with typed display state",
13060        )?;
13061        require(
13062            store.worktree_registrations(true)?.is_empty(),
13063            "non-UTF-8 structural identity reached the registration store",
13064        )?;
13065
13066        let non_utf8_common = temp
13067            .path()
13068            .join(std::ffi::OsString::from_vec(b"common-\xff".to_vec()));
13069        let non_utf8_administrative = non_utf8_common.join("worktrees/utf8");
13070        let non_utf8_root = temp.path().join("utf8");
13071        fs::create_dir_all(&non_utf8_administrative)?;
13072        fs::create_dir(&non_utf8_root)?;
13073        let utf8_entry = GitWorktreeEntry {
13074            role: GitWorktreeRole::Linked,
13075            administrative_directory: non_utf8_administrative,
13076            state: GitWorktreeState::Active {
13077                git_control_path: non_utf8_root.join(".git"),
13078                root: non_utf8_root,
13079            },
13080        };
13081        let row = server.worktree_list_row(&non_utf8_common, &utf8_entry, &[]);
13082        require(
13083            row.selector.is_some()
13084                && row.root.is_some()
13085                && matches!(row.path_display, McpWorktreePathDisplayState::Unavailable)
13086                && row.blocker.as_deref() == Some(MCP_ERROR_WORKTREE_PATH_NON_UTF8)
13087                && ProjectAtlasMcpServer::worktree_candidate(&non_utf8_common, &utf8_entry)
13088                    .is_some_and(|candidate| {
13089                        candidate.root.is_some()
13090                            && matches!(
13091                                candidate.path_display,
13092                                McpWorktreePathDisplayState::Unavailable
13093                            )
13094                    }),
13095            "non-UTF-8 common-directory identity lost its selector or display state",
13096        )
13097    }
13098
13099    #[cfg(unix)]
13100    fn relocate_fixture_administrative_directory(
13101        linked: &Path,
13102        current: &Path,
13103        name: std::ffi::OsString,
13104    ) -> Result<PathBuf, Box<dyn std::error::Error>> {
13105        let relocated = current
13106            .parent()
13107            .ok_or_else(|| io::Error::other("worktree administrative directory has no parent"))?
13108            .join(name);
13109        fs::rename(current, &relocated)?;
13110        let relocated = relocated.canonicalize()?;
13111        let mut pointer = b"gitdir: ".to_vec();
13112        pointer.extend_from_slice(relocated.as_os_str().as_encoded_bytes());
13113        pointer.push(b'\n');
13114        fs::write(linked.join(".git"), pointer)?;
13115        Ok(relocated)
13116    }
13117
13118    #[cfg(unix)]
13119    fn require_real_native_worktree_lifecycle(
13120        case: &str,
13121        primary_name: std::ffi::OsString,
13122        common_name: Option<std::ffi::OsString>,
13123        linked_name: std::ffi::OsString,
13124        administrative_name: Option<std::ffi::OsString>,
13125        expected_utf8: (bool, bool, bool),
13126    ) -> Result<(), Box<dyn std::error::Error>> {
13127        let temp = tempfile::tempdir()?;
13128        let primary = temp.path().join(primary_name);
13129        let linked = temp.path().join(linked_name);
13130        fs::create_dir_all(primary.join("src"))?;
13131        if let Some(common_name) = common_name {
13132            run_fixture_command(
13133                StdCommand::new("git")
13134                    .current_dir(temp.path())
13135                    .args(["init", "--quiet", "--separate-git-dir"])
13136                    .arg(temp.path().join(common_name))
13137                    .arg(&primary),
13138            )?;
13139        } else {
13140            run_fixture_command(
13141                StdCommand::new("git")
13142                    .current_dir(&primary)
13143                    .args(["init", "--quiet"]),
13144            )?;
13145        }
13146        for (key, value) in [
13147            ("user.name", "ProjectAtlas Test"),
13148            ("user.email", "projectatlas@example.invalid"),
13149            ("commit.gpgsign", "false"),
13150            ("core.autocrlf", "false"),
13151        ] {
13152            run_fixture_command(
13153                StdCommand::new("git")
13154                    .current_dir(&primary)
13155                    .args(["config", key, value]),
13156            )?;
13157        }
13158        fs::write(primary.join("src/lib.rs"), "pub fn baseline() {}\n")?;
13159        run_fixture_command(
13160            StdCommand::new("git")
13161                .current_dir(&primary)
13162                .args(["add", "."]),
13163        )?;
13164        run_fixture_command(
13165            StdCommand::new("git")
13166                .current_dir(&primary)
13167                .args(["commit", "--quiet", "-m", "fixture"]),
13168        )?;
13169        run_fixture_command(
13170            StdCommand::new("git")
13171                .current_dir(&primary)
13172                .args(["worktree", "add", "--quiet", "-b", case])
13173                .arg(&linked),
13174        )?;
13175
13176        let linked = linked.canonicalize()?;
13177        if let Some(name) = administrative_name {
13178            let RepositoryStructure::Git(initial) = discover_repository_structure(&linked)? else {
13179                return Err(io::Error::other("fixture was not discovered as Git").into());
13180            };
13181            let current = initial
13182                .worktrees
13183                .iter()
13184                .find(|entry| {
13185                    ProjectAtlasMcpServer::active_worktree_root(entry) == Some(linked.as_path())
13186                })
13187                .ok_or_else(|| io::Error::other("linked fixture was not discovered"))?
13188                .administrative_directory
13189                .clone();
13190            relocate_fixture_administrative_directory(&linked, &current, name)?;
13191        }
13192        require(
13193            run_fixture_command(
13194                StdCommand::new("git")
13195                    .current_dir(&linked)
13196                    .args(["status", "--short"]),
13197            )?
13198            .is_empty(),
13199            "Git could not use the native fixture paths",
13200        )?;
13201
13202        let primary = primary.canonicalize()?;
13203        let control_config = primary.join(PROJECTATLAS_DIR_NAME).join("config.toml");
13204        let control_db = primary
13205            .join(PROJECTATLAS_DIR_NAME)
13206            .join(PROJECTATLAS_DB_FILE_NAME);
13207        init_project_with_config(&primary, Some(&control_config))?;
13208        let mut control = AtlasStore::open_for_project(&control_db, &primary)?;
13209        let plan = ScanRuntimePlan::for_path(Some(&control_config), &primary, None)?;
13210        run_scan_pipeline(
13211            &mut control,
13212            &plan,
13213            &SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, Some(1), None),
13214        )?;
13215        drop(control);
13216        let server = ProjectAtlasMcpServer::new(
13217            control_db.clone(),
13218            Some(control_config),
13219            format!("native-{case}"),
13220            false,
13221        );
13222        let repository = server.control_git_repository()?;
13223        let entry = repository
13224            .worktrees
13225            .iter()
13226            .find(|entry| {
13227                ProjectAtlasMcpServer::active_worktree_root(entry) == Some(linked.as_path())
13228            })
13229            .ok_or_else(|| io::Error::other("native linked worktree was not discovered"))?;
13230        let common_identity = CanonicalProjectRoot::from_path(&repository.common_directory)?;
13231        let administrative_identity =
13232            CanonicalProjectRoot::from_path(&entry.administrative_directory)?;
13233        let root_identity = CanonicalProjectRoot::from_path(&linked)?;
13234        require(
13235            (
13236                linked.to_str().is_some(),
13237                repository.common_directory.to_str().is_some(),
13238                entry.administrative_directory.to_str().is_some(),
13239            ) == expected_utf8,
13240            "fixture did not isolate the expected native path component",
13241        )?;
13242        let selector = ProjectAtlasMcpServer::worktree_candidate_selector(entry);
13243        let display_available = expected_utf8 == (true, true, true);
13244        let listed = server.atlas_worktree_list(Parameters(AtlasWorktreeListParams {
13245            include_retired: Some(false),
13246        }));
13247        let listed_value: serde_json::Value = toon_format::decode_default(&listed)?;
13248        let expected_display = if display_available {
13249            "available"
13250        } else {
13251            "unavailable"
13252        };
13253        require(
13254            listed_value
13255                .pointer("/worktrees/worktrees")
13256                .and_then(serde_json::Value::as_array)
13257                .is_some_and(|rows| {
13258                    rows.iter().any(|row| {
13259                        row.get("selector").and_then(serde_json::Value::as_str)
13260                            == Some(selector.as_str())
13261                            && row.get("path_display").and_then(serde_json::Value::as_str)
13262                                == Some(expected_display)
13263                    })
13264                }),
13265            &format!("native worktree discovery was not publicly selectable: {listed}"),
13266        )?;
13267        let added = server.atlas_worktree_add(Parameters(AtlasWorktreeAddParams {
13268            worktree: selector,
13269            alias: Some(case.to_string()),
13270        }));
13271        require(
13272            added.contains("status: registered")
13273                && added.contains(&format!("alias: \"{case}\""))
13274                && added.contains(if display_available {
13275                    "path_display: available"
13276                } else {
13277                    "path_display: unavailable"
13278                }),
13279            &format!("native worktree registration failed: {added}"),
13280        )?;
13281        let alias = WorktreeAlias::parse(case)?;
13282        let control = open_atlas_store_read_only_for_project(&control_db, &primary)?;
13283        let registration = control.worktree_registration(&alias)?;
13284        require(
13285            registration.git_common_directory_identity == common_identity
13286                && registration.git_administrative_directory_identity == administrative_identity
13287                && registration.last_root_identity == root_identity,
13288            "registration did not retain the exact native identities",
13289        )?;
13290        drop(control);
13291
13292        let initialized = server.atlas_init(Parameters(AtlasInitParams {
13293            project_path: None,
13294            worktree: Some(case.to_string()),
13295            no_scan: Some(false),
13296            force_rescan: Some(false),
13297            text_index_max_bytes: None,
13298        }));
13299        require(
13300            initialized.contains("status: hydrated"),
13301            &format!("alias-routed native initialization failed: {initialized}"),
13302        )?;
13303        fs::write(linked.join("src/native.rs"), "pub fn native_refresh() {}\n")?;
13304        let watched = server.atlas_watch_once(Parameters(AtlasWatchOnceParams {
13305            project_path: None,
13306            worktree: Some(case.to_string()),
13307            path: None,
13308            nearest_project: Some(false),
13309            max_workers: Some(1),
13310            timeout_seconds: None,
13311            text_index_max_bytes: None,
13312            background: Some(false),
13313        }));
13314        require(
13315            watched.contains(MCP_PAYLOAD_WATCH),
13316            &format!("alias-routed native watcher failed: {watched}"),
13317        )?;
13318        let target_db = linked
13319            .join(PROJECTATLAS_DIR_NAME)
13320            .join(PROJECTATLAS_DB_FILE_NAME);
13321        let target = open_atlas_store_read_only_for_project(&target_db, &linked)?;
13322        require(
13323            target.load_node_by_path("src/native.rs")?.is_some(),
13324            "alias-routed watcher did not publish the changed native worktree",
13325        )?;
13326        drop(target);
13327        require(
13328            run_fixture_command(StdCommand::new("git").current_dir(&linked).args([
13329                "status",
13330                "--short",
13331                "--untracked-files=all",
13332            ]))?
13333            .contains("src/native.rs"),
13334            "Git command invocation lost the native worktree path",
13335        )?;
13336
13337        let git_pointer = fs::read(linked.join(".git"))?;
13338        fs::remove_file(linked.join(".git"))?;
13339        let missing_repository = server.control_git_repository()?;
13340        let missing_entry = missing_repository
13341            .worktrees
13342            .iter()
13343            .find(|entry| entry.administrative_directory == administrative_identity.as_path())
13344            .ok_or_else(|| io::Error::other("native missing worktree entry was not discovered"))?;
13345        let missing_row = server.worktree_list_row(
13346            &missing_repository.common_directory,
13347            missing_entry,
13348            std::slice::from_ref(&registration),
13349        );
13350        require(
13351            matches!(missing_row.git_state, McpGitWorktreeState::Missing)
13352                && missing_row.alias.as_deref() == Some(case)
13353                && missing_row.root == root_identity.display_string().ok()
13354                && matches!(
13355                    missing_row.path_display,
13356                    McpWorktreePathDisplayState::Available
13357                ) == display_available,
13358            "Git-known missing native worktree lost its retained root or display state",
13359        )?;
13360        fs::write(linked.join(".git"), git_pointer)?;
13361
13362        let preserved_administrative = temp.path().join("preserved-administrative");
13363        fs::rename(administrative_identity.as_path(), &preserved_administrative)?;
13364        std::os::unix::fs::symlink(&preserved_administrative, administrative_identity.as_path())?;
13365        let invalid_listing = server.atlas_worktree_list(Parameters(AtlasWorktreeListParams {
13366            include_retired: Some(false),
13367        }));
13368        let invalid_value: serde_json::Value = toon_format::decode_default(&invalid_listing)?;
13369        let invalid_rows = invalid_value
13370            .pointer("/worktrees/worktrees")
13371            .and_then(serde_json::Value::as_array)
13372            .ok_or_else(|| io::Error::other("native invalid worktree rows are missing"))?;
13373        require(
13374            invalid_rows.len() == 2
13375                && invalid_rows.iter().any(|row| {
13376                    row.get("alias").and_then(serde_json::Value::as_str) == Some(case)
13377                        && row.get("git_state").and_then(serde_json::Value::as_str)
13378                            == Some("invalid")
13379                        && row.get("registration").and_then(serde_json::Value::as_str)
13380                            == Some("registered")
13381                })
13382                && server
13383                    .state_for_target(None, Some(case.to_string()))
13384                    .is_err(),
13385            &format!("native invalid registration was split or admitted: {invalid_listing}"),
13386        )?;
13387        let invalid_removal = server.atlas_worktree_remove(Parameters(AtlasWorktreeRemoveParams {
13388            worktree: case.to_string(),
13389        }));
13390        require(
13391            invalid_removal.contains("cannot retire worktree")
13392                && invalid_removal.contains("Git evidence is invalid"),
13393            &format!("native invalid evidence allowed retirement: {invalid_removal}"),
13394        )?;
13395        fs::remove_file(administrative_identity.as_path())?;
13396        fs::rename(&preserved_administrative, administrative_identity.as_path())?;
13397
13398        let removed = server.atlas_worktree_remove(Parameters(AtlasWorktreeRemoveParams {
13399            worktree: case.to_string(),
13400        }));
13401        require(
13402            removed.contains("status: retired")
13403                && removed.contains(if display_available {
13404                    "path_display: available"
13405                } else {
13406                    "path_display: unavailable"
13407                }),
13408            &format!("native worktree retirement failed: {removed}"),
13409        )?;
13410        let control = open_atlas_store_read_only_for_project(&control_db, &primary)?;
13411        let retired = control
13412            .worktree_registrations(true)?
13413            .into_iter()
13414            .find(|registration| registration.alias == alias)
13415            .ok_or_else(|| io::Error::other("retired native registration is missing"))?;
13416        require(
13417            retired.state == WorktreeRegistrationState::Retired
13418                && retired.git_common_directory_identity == common_identity
13419                && retired.git_administrative_directory_identity == administrative_identity
13420                && retired.last_root_identity == root_identity,
13421            "retirement changed the persisted native identities",
13422        )?;
13423        require(
13424            server
13425                .state_for_target(None, Some(case.to_string()))
13426                .is_err(),
13427            "retired native alias remained routable",
13428        )
13429    }
13430
13431    // APFS rejects invalid UTF-8 filenames before Git or ProjectAtlas can use them.
13432    #[cfg(all(unix, not(target_os = "macos")))]
13433    #[test]
13434    fn real_non_utf8_worktree_lifecycle_routes_alias_and_preserves_native_identities()
13435    -> Result<(), Box<dyn std::error::Error>> {
13436        for (case, primary, common, linked, administrative, expected_utf8) in [
13437            (
13438                "invalid-root",
13439                std::ffi::OsString::from("root-control"),
13440                None,
13441                std::ffi::OsString::from_vec(b"root-\xff".to_vec()),
13442                Some(std::ffi::OsString::from("root-administrative")),
13443                (false, true, true),
13444            ),
13445            (
13446                "invalid-common",
13447                std::ffi::OsString::from("common-control"),
13448                Some(std::ffi::OsString::from_vec(b"common-\xfe ".to_vec())),
13449                std::ffi::OsString::from("common-linked"),
13450                None,
13451                (true, false, false),
13452            ),
13453            (
13454                "invalid-administrative",
13455                std::ffi::OsString::from("administrative-control"),
13456                None,
13457                std::ffi::OsString::from("administrative-linked"),
13458                Some(std::ffi::OsString::from_vec(
13459                    b"administrative-\xfd\t".to_vec(),
13460                )),
13461                (true, true, false),
13462            ),
13463        ] {
13464            require_real_native_worktree_lifecycle(
13465                case,
13466                primary,
13467                common,
13468                linked,
13469                administrative,
13470                expected_utf8,
13471            )?;
13472        }
13473        Ok(())
13474    }
13475
13476    #[cfg(unix)]
13477    #[test]
13478    fn real_unicode_worktree_lifecycle_routes_alias_and_preserves_native_identities()
13479    -> Result<(), Box<dyn std::error::Error>> {
13480        require_real_native_worktree_lifecycle(
13481            "valid-unicode",
13482            std::ffi::OsString::from("unicode-control-λ"),
13483            Some(std::ffi::OsString::from("unicode-common-共同 ")),
13484            std::ffi::OsString::from("unicode-linked-工作树"),
13485            Some(std::ffi::OsString::from("unicode-administrative-管理\t")),
13486            (true, true, true),
13487        )
13488    }
13489
13490    #[test]
13491    fn worktree_registration_revalidates_captured_local_atlas_identity()
13492    -> Result<(), Box<dyn std::error::Error>> {
13493        let temp = tempfile::tempdir()?;
13494        let root = temp.path().join("worktree");
13495        fs::create_dir(&root)?;
13496        let state = root.join(PROJECTATLAS_DIR_NAME);
13497        let db_path = state.join(PROJECTATLAS_DB_FILE_NAME);
13498        let config_path = state.join("config.toml");
13499        init_project_with_config(&root, Some(&config_path))?;
13500        drop(AtlasStore::open_for_project(&db_path, &root)?);
13501        let captured = ProjectAtlasMcpServer::local_worktree_atlas(&root)?
13502            .ok_or_else(|| io::Error::other("captured worktree atlas is missing"))?;
13503        ProjectAtlasMcpServer::revalidate_local_worktree_atlas_identity(
13504            &root,
13505            Some(captured.project_instance_id),
13506        )?;
13507
13508        let preserved = root.join(".projectatlas-captured-registration");
13509        fs::rename(&state, &preserved)?;
13510        fs::create_dir(&state)?;
13511        let replacement = AtlasStore::open_for_project(&db_path, &root)?;
13512        require(
13513            replacement.project_instance_id()? != Some(captured.project_instance_id),
13514            "replacement atlas reused the captured registration identity",
13515        )?;
13516        drop(replacement);
13517        let rejected = ProjectAtlasMcpServer::revalidate_local_worktree_atlas_identity(
13518            &root,
13519            Some(captured.project_instance_id),
13520        );
13521        require(
13522            rejected.as_ref().is_err_and(|error| {
13523                error
13524                    .to_string()
13525                    .contains(MCP_ERROR_WORKTREE_IDENTITY_CONFLICT)
13526            }),
13527            "registration guard accepted a replacement local atlas",
13528        )?;
13529        fs::remove_dir_all(&state)?;
13530        let missing = ProjectAtlasMcpServer::revalidate_local_worktree_atlas_identity(
13531            &root,
13532            Some(captured.project_instance_id),
13533        );
13534        require(
13535            missing.as_ref().is_err_and(|error| {
13536                error
13537                    .to_string()
13538                    .contains(MCP_ERROR_WORKTREE_IDENTITY_CONFLICT)
13539            }),
13540            "registration guard accepted a missing captured local atlas",
13541        )?;
13542
13543        let uninitialized = temp.path().join("uninitialized");
13544        fs::create_dir(&uninitialized)?;
13545        ProjectAtlasMcpServer::revalidate_local_worktree_atlas_identity(&uninitialized, None)
13546            .map_err(Into::into)
13547    }
13548
13549    #[test]
13550    fn registered_init_rejects_replaced_git_lifecycle_before_or_during_activation_and_binding()
13551    -> Result<(), Box<dyn std::error::Error>> {
13552        let fixture = registered_worktree_race_fixture("init-race")?;
13553        let (candidate, candidate_path, work_control) = prepared_hydration_candidate(&fixture)?;
13554
13555        let replacement_entry = replace_registered_worktree(&fixture, "replacement-init")?;
13556        require(
13557            replacement_entry.administrative_directory == fixture.administrative_directory
13558                && git_administrative_identity(&replacement_entry.administrative_directory)?
13559                    != fixture.administrative_identity,
13560            "replacement fixture did not reuse the administrative path with a new lifecycle",
13561        )?;
13562        fs::create_dir_all(
13563            fixture
13564                .target_db
13565                .parent()
13566                .ok_or_else(|| io::Error::other("replacement database has no parent"))?,
13567        )?;
13568        let replacement = AtlasStore::open_for_project(&fixture.target_db, &fixture.state.root)?;
13569        let replacement_project = replacement
13570            .project_instance_id()?
13571            .ok_or(DbError::ProjectInstanceIdentityMissing)?;
13572        drop(replacement);
13573
13574        let activation = fixture.server.activate_registered_worktree_hydration(
13575            &fixture.state,
13576            &fixture.selection,
13577            candidate,
13578            &work_control,
13579        );
13580        require(
13581            activation.as_ref().is_err_and(|error| {
13582                error
13583                    .to_string()
13584                    .contains("administrative lifecycle changed")
13585            }),
13586            "hydration candidate activated into a replacement Git lifecycle",
13587        )?;
13588        require(
13589            !candidate_path.exists()
13590                && open_atlas_store_read_only_for_project(&fixture.target_db, &fixture.state.root)?
13591                    .project_instance_id()?
13592                    == Some(replacement_project),
13593            "rejected hydration changed the replacement atlas or retained its candidate",
13594        )?;
13595
13596        let binding = fixture
13597            .server
13598            .bind_initialized_worktree(&fixture.selection, &fixture.state);
13599        require(
13600            binding.as_ref().is_err_and(|error| {
13601                error
13602                    .to_string()
13603                    .contains("administrative lifecycle changed")
13604            }),
13605            "final init binding accepted a replacement Git lifecycle",
13606        )?;
13607        let control =
13608            open_atlas_store_read_only_for_project(&fixture.control_db, &fixture.control_root)?;
13609        require(
13610            control
13611                .worktree_registration(&fixture.alias)?
13612                .project_instance_id
13613                .is_none(),
13614            "failed activation or binding attached the replacement atlas",
13615        )?;
13616
13617        let during = registered_worktree_race_fixture("init-publish-race")?;
13618        let (candidate, _candidate_path, work_control) = prepared_hydration_candidate(&during)?;
13619        let replacement_project = std::cell::Cell::new(None);
13620        let activation = during
13621            .server
13622            .activate_registered_worktree_hydration_with_post_publication(
13623                &during.state,
13624                &during.selection,
13625                candidate,
13626                &work_control,
13627                || {
13628                    replace_registered_worktree(&during, "replacement-during-publish")
13629                        .map_err(|error| CliError::InvalidInput(error.to_string()))?;
13630                    fs::create_dir_all(during.target_db.parent().ok_or_else(|| {
13631                        CliError::InvalidInput("replacement database has no parent".to_string())
13632                    })?)
13633                    .map_err(|source| CliError::Io {
13634                        path: during.state.root.clone(),
13635                        source,
13636                    })?;
13637                    let replacement =
13638                        AtlasStore::open_for_project(&during.target_db, &during.state.root)?;
13639                    replacement_project.set(Some(
13640                        replacement
13641                            .project_instance_id()?
13642                            .ok_or(DbError::ProjectInstanceIdentityMissing)?,
13643                    ));
13644                    Ok(())
13645                },
13646            );
13647        let activation_error = activation.as_ref().err().map(ToString::to_string);
13648        require(
13649            activation_error
13650                .as_deref()
13651                .is_some_and(|error| error.contains("administrative lifecycle changed")),
13652            &format!(
13653                "hydration bound a lifecycle replaced after candidate publication: {activation_error:?}"
13654            ),
13655        )?;
13656        let replacement =
13657            open_atlas_store_read_only_for_project(&during.target_db, &during.state.root)?;
13658        let control =
13659            open_atlas_store_read_only_for_project(&during.control_db, &during.control_root)?;
13660        require(
13661            replacement.project_instance_id()? == replacement_project.get()
13662                && control
13663                    .worktree_registration(&during.alias)?
13664                    .project_instance_id
13665                    .is_none(),
13666            "post-publication lifecycle rejection changed or bound the replacement atlas",
13667        )
13668    }
13669
13670    #[test]
13671    fn final_retirement_does_not_import_replacement_lifecycle_telemetry()
13672    -> Result<(), Box<dyn std::error::Error>> {
13673        let fixture = registered_worktree_race_fixture("retire-race")?;
13674        let replacement_entry = replace_registered_worktree(&fixture, "replacement-retire")?;
13675        fs::create_dir_all(
13676            fixture
13677                .target_db
13678                .parent()
13679                .ok_or_else(|| io::Error::other("replacement database has no parent"))?,
13680        )?;
13681        let replacement = AtlasStore::open_for_project(&fixture.target_db, &fixture.state.root)?;
13682        replacement.record_usage(&usage_from_text(
13683            "replacement",
13684            "atlas_overview",
13685            None,
13686            None,
13687            "pub fn replacement() {}",
13688            "repository overview",
13689        ))?;
13690        let replacement_project = replacement
13691            .project_instance_id()?
13692            .ok_or(DbError::ProjectInstanceIdentityMissing)?;
13693        drop(replacement);
13694        let control = open_atlas_store_for_project(&fixture.control_db, &fixture.control_root)?;
13695        let (retired, synchronized, blocker) = ProjectAtlasMcpServer::retire_registered_worktree(
13696            &control,
13697            &fixture.registration,
13698            Some(&fixture.state.root),
13699            2,
13700            None,
13701        )?;
13702        require(
13703            retired.state == WorktreeRegistrationState::Retired
13704                && retired.project_instance_id.is_none()
13705                && retired.accepted_telemetry_revision == 0
13706                && synchronized.is_none()
13707                && blocker.as_deref() == Some(MCP_ERROR_WORKTREE_LIFECYCLE_CHANGED),
13708            "replacement lifecycle telemetry was bound, imported, or hidden during retirement",
13709        )?;
13710
13711        let repository = fixture.server.control_git_repository()?;
13712        let replacement_alias = WorktreeAlias::parse("replacement-owner")?;
13713        let replacement_registration = control.register_worktree(
13714            &replacement_alias,
13715            &repository.common_directory,
13716            &replacement_entry.administrative_directory,
13717            &git_administrative_identity(&replacement_entry.administrative_directory)?,
13718            &fixture.state.root,
13719            Some(replacement_project),
13720            3,
13721        )?;
13722        require(
13723            replacement_registration.project_instance_id == Some(replacement_project),
13724            "retired origin stranded the replacement atlas identity",
13725        )?;
13726
13727        let open_race = registered_worktree_race_fixture("retire-open-race")?;
13728        let control = open_atlas_store_for_project(&open_race.control_db, &open_race.primary)?;
13729        let sentinel = b"replacement atlas must remain untouched";
13730        let (retired, synchronized, blocker) =
13731            ProjectAtlasMcpServer::retire_registered_worktree_with_pre_open(
13732                &control,
13733                &open_race.registration,
13734                Some(&open_race.state.root),
13735                2,
13736                None,
13737                || {
13738                    replace_registered_worktree(&open_race, "replacement-before-local-open")
13739                        .map_err(|error| CliError::InvalidInput(error.to_string()))?;
13740                    fs::create_dir_all(open_race.target_db.parent().ok_or_else(|| {
13741                        CliError::InvalidInput("replacement database has no parent".to_string())
13742                    })?)
13743                    .map_err(|source| CliError::Io {
13744                        path: open_race.state.root.clone(),
13745                        source,
13746                    })?;
13747                    fs::write(&open_race.target_db, sentinel).map_err(|source| CliError::Io {
13748                        path: open_race.target_db.clone(),
13749                        source,
13750                    })
13751                },
13752            )?;
13753        require(
13754            retired.state == WorktreeRegistrationState::Retired
13755                && retired.project_instance_id.is_none()
13756                && synchronized.is_none()
13757                && blocker.as_deref() == Some(MCP_ERROR_WORKTREE_LIFECYCLE_CHANGED)
13758                && fs::read(&open_race.target_db)? == sentinel,
13759            "replacement database failure blocked stale retirement or changed replacement bytes",
13760        )
13761    }
13762
13763    #[test]
13764    fn retirement_reclassifies_identity_and_snapshot_failures_after_lifecycle_replacement()
13765    -> Result<(), Box<dyn std::error::Error>> {
13766        for (suffix, failure) in [
13767            ("retire-identity-failure", "project identity read failed"),
13768            ("retire-snapshot-failure", "usage snapshot export failed"),
13769        ] {
13770            let fixture = registered_worktree_race_fixture(suffix)?;
13771            let replacement_entry = replace_registered_worktree(&fixture, suffix)?;
13772            fs::create_dir_all(
13773                fixture
13774                    .target_db
13775                    .parent()
13776                    .ok_or_else(|| io::Error::other("replacement database has no parent"))?,
13777            )?;
13778            let replacement =
13779                AtlasStore::open_for_project(&fixture.target_db, &fixture.state.root)?;
13780            replacement.record_usage(&usage_from_text(
13781                suffix,
13782                "atlas_token_report",
13783                None,
13784                None,
13785                "pub fn replacement() {}",
13786                "replacement telemetry",
13787            ))?;
13788            let replacement_project = replacement
13789                .project_instance_id()?
13790                .ok_or(DbError::ProjectInstanceIdentityMissing)?;
13791            drop(replacement);
13792
13793            let control = open_atlas_store_for_project(&fixture.control_db, &fixture.control_root)?;
13794            let classified = control.with_active_worktree_registration(
13795                fixture.registration.registration_id,
13796                &fixture.alias,
13797                |guard| {
13798                    ProjectAtlasMcpServer::classify_retirement_failure(
13799                        guard,
13800                        &fixture.state.root,
13801                        2,
13802                        CliError::InvalidInput(failure.to_string()),
13803                    )
13804                },
13805            )?;
13806            let (retired, synchronized, blocker) = classified?;
13807            let replacement =
13808                open_atlas_store_read_only_for_project(&fixture.target_db, &fixture.state.root)?;
13809            require(
13810                retired.state == WorktreeRegistrationState::Retired
13811                    && retired.project_instance_id.is_none()
13812                    && synchronized.is_none()
13813                    && blocker.as_deref() == Some(MCP_ERROR_WORKTREE_LIFECYCLE_CHANGED)
13814                    && replacement.project_instance_id()? == Some(replacement_project)
13815                    && replacement.token_overview(Some(suffix))?.calls == 1,
13816                "retirement failure classification changed or imported replacement state",
13817            )?;
13818
13819            let repository = fixture.server.control_git_repository()?;
13820            let replacement_alias = WorktreeAlias::parse(&format!("{suffix}-owner"))?;
13821            let replacement_registration = control.register_worktree(
13822                &replacement_alias,
13823                &repository.common_directory,
13824                &replacement_entry.administrative_directory,
13825                &git_administrative_identity(&replacement_entry.administrative_directory)?,
13826                &fixture.state.root,
13827                Some(replacement_project),
13828                3,
13829            )?;
13830            require(
13831                replacement_registration.project_instance_id == Some(replacement_project),
13832                "retirement failure classification stranded replacement ownership",
13833            )?;
13834        }
13835        Ok(())
13836    }
13837
13838    #[test]
13839    fn registered_reset_and_binding_linearize_without_recreating_a_reset_winner()
13840    -> Result<(), Box<dyn std::error::Error>> {
13841        let bind_wins = registered_worktree_race_fixture("reset-bind-wins")?;
13842        fs::create_dir_all(
13843            bind_wins
13844                .target_db
13845                .parent()
13846                .ok_or_else(|| io::Error::other("bind-wins database has no parent"))?,
13847        )?;
13848        let target = AtlasStore::open_for_project(&bind_wins.target_db, &bind_wins.state.root)?;
13849        let project = target
13850            .project_instance_id()?
13851            .ok_or(DbError::ProjectInstanceIdentityMissing)?;
13852        drop(target);
13853        let before = fs::read(&bind_wins.target_db)?;
13854        let control = open_atlas_store_for_project(&bind_wins.control_db, &bind_wins.primary)?;
13855        control.bind_worktree_project(
13856            bind_wins.registration.registration_id,
13857            &bind_wins.alias,
13858            &bind_wins.state.root,
13859            project,
13860        )?;
13861        let rejected = bind_wins.server.reset_registered_worktree_index(
13862            &bind_wins.state,
13863            &bind_wins.selection,
13864            false,
13865        );
13866        require(
13867            rejected.as_ref().is_err_and(|error| {
13868                error
13869                    .to_string()
13870                    .contains(MCP_ERROR_BOUND_WORKTREE_RESET_UNSUPPORTED)
13871            }) && fs::read(&bind_wins.target_db)? == before,
13872            "reset deleted a target after a concurrent binding won",
13873        )?;
13874
13875        let reset_wins = registered_worktree_race_fixture("reset-delete-wins")?;
13876        fs::create_dir_all(
13877            reset_wins
13878                .target_db
13879                .parent()
13880                .ok_or_else(|| io::Error::other("reset-wins database has no parent"))?,
13881        )?;
13882        drop(AtlasStore::open_for_project(
13883            &reset_wins.target_db,
13884            &reset_wins.state.root,
13885        )?);
13886        reset_wins.server.reset_registered_worktree_index(
13887            &reset_wins.state,
13888            &reset_wins.selection,
13889            false,
13890        )?;
13891        require(
13892            !reset_wins.target_db.exists(),
13893            "winning reset did not delete the unbound target atlas",
13894        )?;
13895        let late_bind = ProjectAtlasMcpServer::open_registered_worktree_mut_store(
13896            &reset_wins.state,
13897            &reset_wins.server.control_state,
13898            &reset_wins.selection,
13899        );
13900        let control =
13901            open_atlas_store_read_only_for_project(&reset_wins.control_db, &reset_wins.primary)?;
13902        require(
13903            late_bind.is_err()
13904                && !reset_wins.target_db.exists()
13905                && control
13906                    .worktree_registration(&reset_wins.alias)?
13907                    .project_instance_id
13908                    .is_none(),
13909            "late binding recreated or attached the atlas deleted by reset",
13910        )?;
13911
13912        let replaced = registered_worktree_race_fixture("reset-replaced-lifecycle")?;
13913        let replacement_files = [
13914            replaced.target_db.clone(),
13915            db_sidecar_path(&replaced.target_db, "wal"),
13916            db_sidecar_path(&replaced.target_db, "shm"),
13917            db_sidecar_path(&replaced.target_db, "journal"),
13918            mcp_config_path_for_db(&replaced.target_db),
13919        ];
13920        let replacement_bytes = replacement_files
13921            .iter()
13922            .enumerate()
13923            .map(|(index, _)| format!("replacement-owned-{index}").into_bytes())
13924            .collect::<Vec<_>>();
13925        let rejected = replaced
13926            .server
13927            .reset_registered_worktree_index_with_post_validation(
13928                &replaced.state,
13929                &replaced.selection,
13930                true,
13931                || {
13932                    replace_registered_worktree(&replaced, "replacement-during-reset")
13933                        .map_err(|error| CliError::InvalidInput(error.to_string()))?;
13934                    fs::create_dir_all(replaced.target_db.parent().ok_or_else(|| {
13935                        CliError::InvalidInput("replacement database has no parent".to_string())
13936                    })?)
13937                    .map_err(|source| CliError::Io {
13938                        path: replaced.state.root.clone(),
13939                        source,
13940                    })?;
13941                    for (path, bytes) in replacement_files.iter().zip(&replacement_bytes) {
13942                        fs::write(path, bytes).map_err(|source| CliError::Io {
13943                            path: path.clone(),
13944                            source,
13945                        })?;
13946                    }
13947                    Ok(())
13948                },
13949            );
13950        let control =
13951            open_atlas_store_read_only_for_project(&replaced.control_db, &replaced.primary)?;
13952        require(
13953            rejected.as_ref().is_err_and(|error| {
13954                error
13955                    .to_string()
13956                    .contains("administrative lifecycle changed")
13957            }) && replacement_files
13958                .iter()
13959                .zip(&replacement_bytes)
13960                .all(|(path, bytes)| {
13961                    fs::read(path).is_ok_and(|found| found.as_slice() == bytes.as_slice())
13962                })
13963                && control
13964                    .worktree_registration(&replaced.alias)?
13965                    .project_instance_id
13966                    .is_none(),
13967            "guarded reset deleted replacement lifecycle database, sidecars, or MCP config",
13968        )
13969    }
13970
13971    #[test]
13972    fn registered_worktree_missing_atlas_does_not_refresh_control_root()
13973    -> Result<(), Box<dyn std::error::Error>> {
13974        let fixture = registered_worktree_race_fixture("missing-atlas")?;
13975        let expected_project = ProjectInstanceId::from_bytes([0xA; 16])?;
13976        let control = AtlasStore::open_for_project(&fixture.control_db, &fixture.control_root)?;
13977        control.bind_worktree_project(
13978            fixture.registration.registration_id,
13979            &fixture.alias,
13980            &fixture.state.root,
13981            expected_project,
13982        )?;
13983        drop(control);
13984
13985        let sidecars = ["-wal", "-shm", "-journal"].map(|suffix| {
13986            let mut path = fixture.control_db.as_os_str().to_os_string();
13987            path.push(suffix);
13988            PathBuf::from(path)
13989        });
13990        let before = std::iter::once(&fixture.control_db)
13991            .chain(sidecars.iter())
13992            .map(|path| fs::read(path).ok())
13993            .collect::<Vec<_>>();
13994        let result = fixture
13995            .server
13996            .state_for_target(None, Some(fixture.alias.to_string()));
13997        require(
13998            result.is_err(),
13999            "missing bound atlas was accepted during registered resolution",
14000        )?;
14001        let after = std::iter::once(&fixture.control_db)
14002            .chain(sidecars.iter())
14003            .map(|path| fs::read(path).ok())
14004            .collect::<Vec<_>>();
14005        require(
14006            before == after,
14007            "failed atlas validation refreshed the control registration",
14008        )?;
14009        Ok(())
14010    }
14011
14012    #[test]
14013    fn unbound_registered_worktree_move_refreshes_root_before_missing_retirement()
14014    -> Result<(), Box<dyn std::error::Error>> {
14015        let fixture = registered_worktree_race_fixture("unbound-move")?;
14016        let original_root_display = normalize_native_path_display(&fixture.state.root);
14017        let moved = fixture
14018            .primary
14019            .parent()
14020            .ok_or_else(|| io::Error::other("moved worktree has no parent"))?
14021            .join("moved-unbound-worktree");
14022        run_fixture_command(
14023            StdCommand::new("git")
14024                .current_dir(&fixture.primary)
14025                .args(["worktree", "move"])
14026                .arg(&fixture.linked)
14027                .arg(&moved),
14028        )?;
14029        let moved_root = moved.canonicalize()?;
14030        let unresolved = fixture
14031            .server
14032            .state_for_target(None, Some(fixture.alias.to_string()));
14033        require(
14034            unresolved.is_err(),
14035            "unbound moved worktree without an atlas was accepted as initialized",
14036        )?;
14037
14038        let control = AtlasStore::open_for_project(&fixture.control_db, &fixture.control_root)?;
14039        let refreshed = control.worktree_registration(&fixture.alias)?;
14040        require(
14041            refreshed.project_instance_id.is_none()
14042                && refreshed.last_root == normalize_native_path_display(&moved_root),
14043            "unbound Git move did not refresh its registered root",
14044        )?;
14045        drop(control);
14046
14047        run_fixture_command(
14048            StdCommand::new("git")
14049                .current_dir(&fixture.primary)
14050                .args(["worktree", "remove", "--force"])
14051                .arg(&moved),
14052        )?;
14053        let missing = fixture
14054            .server
14055            .atlas_worktree_list(Parameters(AtlasWorktreeListParams {
14056                include_retired: Some(false),
14057            }));
14058        require(
14059            missing.contains(&normalize_native_path_display(&moved_root))
14060                && !missing.contains(&original_root_display)
14061                && missing.contains("\"unbound-move\",linked,available,missing,registered"),
14062            &format!("missing worktree reporting retained the pre-move root: {missing}"),
14063        )?;
14064        let retired = fixture
14065            .server
14066            .atlas_worktree_remove(Parameters(AtlasWorktreeRemoveParams {
14067                worktree: fixture.alias.to_string(),
14068            }));
14069        require(
14070            retired.contains("status: retired")
14071                && retired.contains(&normalize_native_path_display(&moved_root))
14072                && !retired.contains(&original_root_display),
14073            &format!("retirement reporting retained the pre-move root: {retired}"),
14074        )
14075    }
14076
14077    #[test]
14078    fn registered_worktree_move_refreshes_registry_root_after_local_rebind()
14079    -> Result<(), Box<dyn std::error::Error>> {
14080        let fixture = registered_worktree_race_fixture("moved-root")?;
14081        let original_root = fixture.state.root.clone();
14082        let original_root_display = normalize_native_path_display(&original_root);
14083        let target_config = fixture
14084            .linked
14085            .join(PROJECTATLAS_DIR_NAME)
14086            .join(PROJECTATLAS_CONFIG_FILE_NAME);
14087        init_project_with_config(&fixture.linked, Some(&target_config))?;
14088        let target_store = AtlasStore::open_for_project(&fixture.target_db, &fixture.linked)?;
14089        let target_project = target_store
14090            .project_instance_id()?
14091            .ok_or(DbError::ProjectInstanceIdentityMissing)?;
14092        drop(target_store);
14093
14094        let registered = {
14095            let control = AtlasStore::open_for_project(&fixture.control_db, &fixture.control_root)?;
14096            control.register_worktree(
14097                &fixture.alias,
14098                &fixture.control_root.join(".git"),
14099                &fixture.administrative_directory,
14100                &fixture.registration.git_administrative_identity,
14101                &original_root,
14102                Some(target_project),
14103                1,
14104            )?
14105        };
14106        require(
14107            registered.project_instance_id == Some(target_project),
14108            "moved worktree fixture remained unbound",
14109        )?;
14110        let original_registration = registered;
14111
14112        let moved = fixture
14113            .primary
14114            .parent()
14115            .ok_or_else(|| io::Error::other("moved worktree has no parent"))?
14116            .join("moved-worktree");
14117        run_fixture_command(
14118            StdCommand::new("git")
14119                .current_dir(&fixture.primary)
14120                .args(["worktree", "move"])
14121                .arg(&fixture.linked)
14122                .arg(&moved),
14123        )?;
14124        let moved_root = moved.canonicalize()?;
14125        let moved_db = moved_root
14126            .join(PROJECTATLAS_DIR_NAME)
14127            .join(PROJECTATLAS_DB_FILE_NAME);
14128        let moved_binding = AtlasStore::transition_project_root(
14129            &moved_db,
14130            &moved_root,
14131            ProjectRootTransition::Move,
14132        )?;
14133        require(
14134            moved_binding.project_instance_id == target_project,
14135            "local root rebind changed the registered project identity",
14136        )?;
14137        require(
14138            read_project_root_identity_read_only(&moved_db)?
14139                == Some(CanonicalProjectRoot::from_path(&moved_root)?),
14140            "local root rebind did not establish the moved native identity",
14141        )?;
14142
14143        let resolved = fixture
14144            .server
14145            .state_for_target(None, Some(fixture.alias.to_string()))?;
14146        require(
14147            resolved.root == moved_root,
14148            "alias resolution did not use the moved Git root",
14149        )?;
14150        let control = AtlasStore::open_for_project(&fixture.control_db, &fixture.control_root)?;
14151        let refreshed = control.worktree_registration(&fixture.alias)?;
14152        require(
14153            refreshed.last_root == normalize_native_path_display(&moved_root)
14154                && refreshed.last_root != original_root_display,
14155            "alias resolution retained the stale registry root",
14156        )?;
14157        require(
14158            refreshed.registration_id == original_registration.registration_id
14159                && refreshed.alias == original_registration.alias
14160                && refreshed.git_common_directory == original_registration.git_common_directory
14161                && refreshed.git_administrative_directory
14162                    == original_registration.git_administrative_directory
14163                && refreshed.git_administrative_identity
14164                    == original_registration.git_administrative_identity
14165                && refreshed.project_instance_id == original_registration.project_instance_id
14166                && refreshed.accepted_telemetry_revision
14167                    == original_registration.accepted_telemetry_revision,
14168            "moved root refresh changed administrative identity or telemetry state",
14169        )?;
14170        drop(control);
14171
14172        run_fixture_command(
14173            StdCommand::new("git")
14174                .current_dir(&fixture.primary)
14175                .args(["worktree", "remove", "--force"])
14176                .arg(&moved),
14177        )?;
14178        let missing = fixture
14179            .server
14180            .atlas_worktree_list(Parameters(AtlasWorktreeListParams {
14181                include_retired: Some(false),
14182            }));
14183        require(
14184            missing.contains(&normalize_native_path_display(&moved_root))
14185                && !missing.contains(&original_root_display)
14186                && missing.contains("\"moved-root\",linked,available,missing,registered"),
14187            &format!("missing worktree reporting regressed to the pre-move root: {missing}"),
14188        )?;
14189        let retired = fixture
14190            .server
14191            .atlas_worktree_remove(Parameters(AtlasWorktreeRemoveParams {
14192                worktree: fixture.alias.to_string(),
14193            }));
14194        require(
14195            retired.contains("status: retired")
14196                && retired.contains(&normalize_native_path_display(&moved_root))
14197                && !retired.contains(&original_root_display),
14198            &format!("retirement did not preserve the moved root: {retired}"),
14199        )
14200    }
14201
14202    #[test]
14203    fn worktree_tools_register_route_and_retire_without_git_or_file_lifecycle_mutation()
14204    -> Result<(), Box<dyn std::error::Error>> {
14205        let temp = tempfile::tempdir()?;
14206        let primary = temp.path().join("selected control");
14207        let worktree_a = temp.path().join("unrelated one").join("checkout");
14208        let worktree_b = temp.path().join("unrelated two").join("checkout");
14209        fs::create_dir_all(&primary)?;
14210        fs::create_dir_all(
14211            worktree_a
14212                .parent()
14213                .ok_or_else(|| io::Error::other("worktree A has no parent"))?,
14214        )?;
14215        fs::create_dir_all(
14216            worktree_b
14217                .parent()
14218                .ok_or_else(|| io::Error::other("worktree B has no parent"))?,
14219        )?;
14220        run_fixture_command(StdCommand::new("git").current_dir(&primary).arg("init"))?;
14221        for (key, value) in [
14222            ("user.name", "ProjectAtlas Test"),
14223            ("user.email", "projectatlas@example.invalid"),
14224            ("commit.gpgsign", "false"),
14225            ("core.autocrlf", "false"),
14226        ] {
14227            run_fixture_command(
14228                StdCommand::new("git")
14229                    .current_dir(&primary)
14230                    .args(["config", key, value]),
14231            )?;
14232        }
14233        fs::create_dir(primary.join("src"))?;
14234        fs::write(
14235            primary.join("src").join("lib.rs"),
14236            "mod child;\npub fn main_only() { child::helper(); }\n",
14237        )?;
14238        fs::write(primary.join("src").join("child.rs"), "pub fn helper() {}\n")?;
14239        run_fixture_command(
14240            StdCommand::new("git")
14241                .current_dir(&primary)
14242                .args(["add", "."]),
14243        )?;
14244        run_fixture_command(
14245            StdCommand::new("git")
14246                .current_dir(&primary)
14247                .args(["commit", "-m", "fixture"]),
14248        )?;
14249        run_fixture_command(
14250            StdCommand::new("git")
14251                .current_dir(&primary)
14252                .args(["worktree", "add", "-b", "issue-430-a"])
14253                .arg(&worktree_a),
14254        )?;
14255        run_fixture_command(
14256            StdCommand::new("git")
14257                .current_dir(&primary)
14258                .args(["worktree", "add", "-b", "issue-430-b"])
14259                .arg(&worktree_b),
14260        )?;
14261        fs::write(
14262            worktree_a.join("src").join("branch.rs"),
14263            "pub fn worktree_only() {}\n",
14264        )?;
14265        let git_before = run_fixture_command(StdCommand::new("git").current_dir(&primary).args([
14266            "worktree",
14267            "list",
14268            "--porcelain",
14269        ]))?;
14270
14271        let control_db = primary.join(".projectatlas").join("projectatlas.db");
14272        let control_config = primary.join(".projectatlas").join("config.toml");
14273        init_project_with_config(&primary, Some(&control_config))?;
14274        let mut control_store = AtlasStore::open_for_project(&control_db, &primary)?;
14275        let control_plan = ScanRuntimePlan::for_path(Some(&control_config), &primary, None)?;
14276        run_scan_pipeline(
14277            &mut control_store,
14278            &control_plan,
14279            &SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, None, None),
14280        )?;
14281        control_store.set_purpose(
14282            "src/lib.rs",
14283            "Own the shared library contract.",
14284            PurposeSource::Agent,
14285        )?;
14286        drop(control_store);
14287        let server = ProjectAtlasMcpServer::new(
14288            control_db.clone(),
14289            Some(control_config),
14290            "worktree-tools".to_string(),
14291            false,
14292        );
14293        let control_before_blank_selector = fs::read(&control_db)?;
14294        let blank_selector = server.atlas_reset_index(Parameters(AtlasResetIndexParams {
14295            project_path: None,
14296            worktree: Some("   ".to_string()),
14297            apply: Some(true),
14298            dry_run: Some(false),
14299            include_mcp_config: Some(true),
14300        }));
14301        require(
14302            blank_selector.contains(MCP_ERROR_WORKTREE_SELECTOR_EMPTY),
14303            "blank worktree selector fell back to the control atlas",
14304        )?;
14305        require(
14306            fs::read(&control_db)? == control_before_blank_selector,
14307            "blank worktree selector changed the control database",
14308        )?;
14309        let repository = server.control_git_repository()?;
14310        let canonical_a = worktree_a.canonicalize()?;
14311        let entry_a = repository
14312            .worktrees
14313            .iter()
14314            .find(|entry| {
14315                ProjectAtlasMcpServer::active_worktree_root(entry) == Some(canonical_a.as_path())
14316            })
14317            .ok_or_else(|| io::Error::other("worktree A was not structurally discovered"))?;
14318        let selector_a = ProjectAtlasMcpServer::worktree_candidate_selector(entry_a);
14319        let canonical_b = worktree_b.canonicalize()?;
14320        let entry_b = repository
14321            .worktrees
14322            .iter()
14323            .find(|entry| {
14324                ProjectAtlasMcpServer::active_worktree_root(entry) == Some(canonical_b.as_path())
14325            })
14326            .ok_or_else(|| io::Error::other("worktree B was not structurally discovered"))?;
14327        let selector_b = ProjectAtlasMcpServer::worktree_candidate_selector(entry_b);
14328
14329        let listed = server.atlas_worktree_list(Parameters(AtlasWorktreeListParams {
14330            include_retired: Some(false),
14331        }));
14332        require(
14333            listed.contains("control_alias: main")
14334                && listed.contains(&selector_a)
14335                && listed.contains(&normalize_native_path_display(&canonical_a)),
14336            "worktree list omitted control, stable selector, or arbitrary exact root",
14337        )?;
14338        let ambiguous = server.atlas_worktree_add(Parameters(AtlasWorktreeAddParams {
14339            worktree: "checkout".to_string(),
14340            alias: Some("issue-430".to_string()),
14341        }));
14342        require(
14343            ambiguous.contains("status: ambiguous")
14344                && ambiguous.matches(MCP_WORKTREE_SELECTOR_PREFIX).count() >= 2,
14345            "ambiguous human selector guessed or omitted bounded stable candidates",
14346        )?;
14347        let control_before_blank_alias = fs::read(&control_db)?;
14348        let blank_alias = server.atlas_worktree_add(Parameters(AtlasWorktreeAddParams {
14349            worktree: selector_a.clone(),
14350            alias: Some("   ".to_string()),
14351        }));
14352        require(
14353            blank_alias.contains("alias is empty"),
14354            "blank explicit alias fell back to the selected directory name",
14355        )?;
14356        require(
14357            fs::read(&control_db)? == control_before_blank_alias,
14358            "blank explicit alias changed the control database",
14359        )?;
14360
14361        let target_b_config = worktree_b.join(PROJECTATLAS_DIR_NAME).join("config.toml");
14362        init_project_with_config(&worktree_b, Some(&target_b_config))?;
14363        let target_b_db = worktree_b
14364            .join(PROJECTATLAS_DIR_NAME)
14365            .join(PROJECTATLAS_DB_FILE_NAME);
14366        let target_b_store = AtlasStore::open_for_project(&target_b_db, &worktree_b)?;
14367        target_b_store.record_usage(&usage_from_text(
14368            "snapshot-blocked",
14369            "atlas_overview",
14370            None,
14371            None,
14372            "pub fn main_only() {}",
14373            "repository overview",
14374        ))?;
14375        let target_b_project = target_b_store
14376            .project_instance_id()?
14377            .ok_or_else(|| io::Error::other("worktree B project identity is missing"))?;
14378        drop(target_b_store);
14379        let corrupt = rusqlite::Connection::open(&target_b_db)?;
14380        corrupt.execute_batch("PRAGMA ignore_check_constraints = ON;")?;
14381        let corrupted = corrupt.execute("UPDATE usage_global_aggregates SET calls = -1", [])?;
14382        require(
14383            corrupted > 0,
14384            "worktree B fixture did not invalidate an aggregate row",
14385        )?;
14386        drop(corrupt);
14387        let rejected_snapshot = server.atlas_worktree_add(Parameters(AtlasWorktreeAddParams {
14388            worktree: selector_b.clone(),
14389            alias: Some("snapshot-blocked".to_string()),
14390        }));
14391        require(
14392            rejected_snapshot.contains("telemetry integer overflow")
14393                && !rejected_snapshot.contains("status: registered"),
14394            &format!(
14395                "worktree registration survived a failed local usage snapshot: {rejected_snapshot}"
14396            ),
14397        )?;
14398        let control_after_rejection =
14399            open_atlas_store_read_only_for_project(&control_db, &primary)?;
14400        require(
14401            matches!(
14402                control_after_rejection
14403                    .worktree_registration(&WorktreeAlias::parse("snapshot-blocked")?),
14404                Err(DbError::WorktreeRegistrationNotFound { .. })
14405            ),
14406            "telemetry export failure committed a partial worktree registration",
14407        )?;
14408        drop(control_after_rejection);
14409        let repaired = rusqlite::Connection::open(&target_b_db)?;
14410        repaired.execute("UPDATE usage_global_aggregates SET calls = 1", [])?;
14411        drop(repaired);
14412        let registered_snapshot = server.atlas_worktree_add(Parameters(AtlasWorktreeAddParams {
14413            worktree: selector_b.clone(),
14414            alias: Some("snapshot-blocked".to_string()),
14415        }));
14416        require(
14417            registered_snapshot.contains("status: registered"),
14418            &format!(
14419                "repaired local usage snapshot did not register atomically: {registered_snapshot}"
14420            ),
14421        )?;
14422        let control_after_snapshot = open_atlas_store_read_only_for_project(&control_db, &primary)?;
14423        require(
14424            control_after_snapshot
14425                .worktree_registration(&WorktreeAlias::parse("snapshot-blocked")?)?
14426                .project_instance_id
14427                == Some(target_b_project)
14428                && control_after_snapshot
14429                    .registered_worktree_token_overview(&WorktreeAlias::parse("snapshot-blocked")?)?
14430                    .calls
14431                    == 1,
14432            "successful registration did not bind identity and import telemetry atomically",
14433        )?;
14434        drop(control_after_snapshot);
14435        let target_b_state = worktree_b.join(PROJECTATLAS_DIR_NAME);
14436        let preserved_target_b_state = worktree_b.join(".projectatlas-snapshot-blocked");
14437        fs::rename(&target_b_state, &preserved_target_b_state)?;
14438        fs::create_dir(&target_b_state)?;
14439        let replacement_b = AtlasStore::open_for_project(&target_b_db, &worktree_b)?;
14440        require(
14441            replacement_b.project_instance_id()? != Some(target_b_project),
14442            "worktree B replacement reused the registered project identity",
14443        )?;
14444        drop(replacement_b);
14445        let replacement_b_error =
14446            server.state_for_target(None, Some("snapshot-blocked".to_string()));
14447        require(
14448            replacement_b_error.as_ref().is_err_and(|error| {
14449                error
14450                    .to_string()
14451                    .contains(MCP_ERROR_WORKTREE_IDENTITY_CONFLICT)
14452            }),
14453            "snapshot-backed registration routed to a replacement atlas",
14454        )?;
14455        fs::remove_dir_all(&target_b_state)?;
14456        let refused_snapshot =
14457            server.atlas_worktree_remove(Parameters(AtlasWorktreeRemoveParams {
14458                worktree: "snapshot-blocked".to_string(),
14459            }));
14460        require(
14461            refused_snapshot.contains(MCP_ERROR_BOUND_WORKTREE_ATLAS_MISSING),
14462            &format!(
14463                "bound registration retired without its required final snapshot: {refused_snapshot}"
14464            ),
14465        )?;
14466        let control_after_refusal = open_atlas_store_read_only_for_project(&control_db, &primary)?;
14467        require(
14468            control_after_refusal
14469                .worktree_registration(&WorktreeAlias::parse("snapshot-blocked")?)?
14470                .state
14471                == WorktreeRegistrationState::Active,
14472            "missing bound atlas retirement changed the active registration",
14473        )?;
14474        drop(control_after_refusal);
14475        fs::rename(&preserved_target_b_state, &target_b_state)?;
14476        let retired_snapshot =
14477            server.atlas_worktree_remove(Parameters(AtlasWorktreeRemoveParams {
14478                worktree: "snapshot-blocked".to_string(),
14479            }));
14480        require(
14481            retired_snapshot.contains("status: retired"),
14482            &format!(
14483                "restored bound atlas did not permit final-sync retirement: {retired_snapshot}"
14484            ),
14485        )?;
14486        fs::remove_dir_all(&target_b_state)?;
14487
14488        let legacy_added = server.atlas_worktree_add(Parameters(AtlasWorktreeAddParams {
14489            worktree: selector_b.clone(),
14490            alias: Some("legacy-init".to_string()),
14491        }));
14492        require(
14493            legacy_added.contains("status: registered"),
14494            &format!("legacy-init fixture registration failed: {legacy_added}"),
14495        )?;
14496        init_project_with_config(&worktree_b, Some(&target_b_config))?;
14497        let control_before_legacy_sync =
14498            open_atlas_store_read_only_for_project(&control_db, &primary)?;
14499        require(
14500            control_before_legacy_sync
14501                .worktree_registration(&WorktreeAlias::parse("legacy-init")?)?
14502                .project_instance_id
14503                .is_none(),
14504            "independent exact-path init unexpectedly mutated the control registration",
14505        )?;
14506        drop(control_before_legacy_sync);
14507        let legacy_target = AtlasStore::open_for_project(&target_b_db, &worktree_b)?;
14508        legacy_target.record_usage(&usage_from_text(
14509            "legacy-init",
14510            "atlas_overview",
14511            None,
14512            None,
14513            "pub fn independent() {}",
14514            "repository overview",
14515        ))?;
14516        let legacy_project = legacy_target
14517            .project_instance_id()?
14518            .ok_or_else(|| io::Error::other("legacy-init target identity is missing"))?;
14519        drop(legacy_target);
14520        let legacy_state = server.state_for_target(None, Some("legacy-init".to_string()))?;
14521        let legacy_selection = legacy_state
14522            .worktree
14523            .as_ref()
14524            .ok_or_else(|| io::Error::other("legacy-init selection is missing"))?;
14525        let corrupt = rusqlite::Connection::open(&target_b_db)?;
14526        corrupt.execute_batch("PRAGMA ignore_check_constraints = ON;")?;
14527        corrupt.execute("UPDATE usage_global_aggregates SET calls = -1", [])?;
14528        drop(corrupt);
14529        let rejected_bind = ProjectAtlasMcpServer::open_registered_worktree_mut_store(
14530            &legacy_state,
14531            &server.control_state,
14532            legacy_selection,
14533        );
14534        let control_after_rejected_bind =
14535            open_atlas_store_read_only_for_project(&control_db, &primary)?;
14536        require(
14537            rejected_bind
14538                .as_ref()
14539                .is_err_and(|error| error.to_string().contains("telemetry integer overflow"))
14540                && control_after_rejected_bind
14541                    .worktree_registration(&WorktreeAlias::parse("legacy-init")?)?
14542                    .project_instance_id
14543                    .is_none()
14544                && control_after_rejected_bind
14545                    .registered_worktree_token_overview(&WorktreeAlias::parse("legacy-init")?)?
14546                    .calls
14547                    == 0,
14548            "failed deferred snapshot synchronization committed its project binding or aggregate",
14549        )?;
14550        drop(control_after_rejected_bind);
14551        let repaired = rusqlite::Connection::open(&target_b_db)?;
14552        repaired.execute("UPDATE usage_global_aggregates SET calls = 1", [])?;
14553        drop(repaired);
14554        drop(ProjectAtlasMcpServer::open_registered_worktree_mut_store(
14555            &legacy_state,
14556            &server.control_state,
14557            legacy_selection,
14558        )?);
14559        let control_after_legacy_init =
14560            open_atlas_store_read_only_for_project(&control_db, &primary)?;
14561        require(
14562            control_after_legacy_init
14563                .worktree_registration(&WorktreeAlias::parse("legacy-init")?)?
14564                .project_instance_id
14565                == Some(legacy_project),
14566            "first alias mutation did not bind the independently initialized alias",
14567        )?;
14568        require(
14569            control_after_legacy_init
14570                .registered_worktree_token_overview(&WorktreeAlias::parse("legacy-init")?)?
14571                .calls
14572                == 1,
14573            "first alias mutation omitted independently initialized worktree usage",
14574        )?;
14575        drop(control_after_legacy_init);
14576        let preserved_legacy_state = worktree_b.join(".projectatlas-legacy-init");
14577        fs::rename(&target_b_state, &preserved_legacy_state)?;
14578        fs::create_dir(&target_b_state)?;
14579        let legacy_replacement = AtlasStore::open_for_project(&target_b_db, &worktree_b)?;
14580        require(
14581            legacy_replacement.project_instance_id()? != Some(legacy_project),
14582            "legacy-init replacement reused the registered project identity",
14583        )?;
14584        drop(legacy_replacement);
14585        let legacy_replacement_error =
14586            server.state_for_target(None, Some("legacy-init".to_string()));
14587        require(
14588            legacy_replacement_error.as_ref().is_err_and(|error| {
14589                error
14590                    .to_string()
14591                    .contains(MCP_ERROR_WORKTREE_IDENTITY_CONFLICT)
14592            }),
14593            "legacy exact-path init left its alias unbound to a replacement atlas",
14594        )?;
14595        fs::remove_dir_all(&target_b_state)?;
14596        let refused_legacy = server.atlas_worktree_remove(Parameters(AtlasWorktreeRemoveParams {
14597            worktree: "legacy-init".to_string(),
14598        }));
14599        require(
14600            refused_legacy.contains(MCP_ERROR_BOUND_WORKTREE_ATLAS_MISSING),
14601            &format!("legacy-init registration retired without its atlas: {refused_legacy}"),
14602        )?;
14603        fs::rename(&preserved_legacy_state, &target_b_state)?;
14604        let retired_legacy = server.atlas_worktree_remove(Parameters(AtlasWorktreeRemoveParams {
14605            worktree: "legacy-init".to_string(),
14606        }));
14607        require(
14608            retired_legacy.contains("status: retired"),
14609            &format!("restored legacy-init registration could not be retired: {retired_legacy}"),
14610        )?;
14611        fs::remove_dir_all(&target_b_state)?;
14612
14613        init_project_with_config(&worktree_b, Some(&target_b_config))?;
14614        let migratable_store = AtlasStore::open_for_project(&target_b_db, &worktree_b)?;
14615        let migratable_project = migratable_store
14616            .project_instance_id()?
14617            .ok_or_else(|| io::Error::other("migratable target identity is missing"))?;
14618        drop(migratable_store);
14619        let incomplete_current = rusqlite::Connection::open(&target_b_db)?;
14620        incomplete_current.execute(
14621            "UPDATE metadata SET value = ?1 WHERE key = 'project_root'",
14622            [worktree_b.join(".").to_string_lossy().into_owned()],
14623        )?;
14624        // Exercise the cross-platform typed predecessor migration path. The
14625        // schema-19 table-absent route is supported only where its legacy
14626        // display is lossless (Windows); Unix must reject that non-injective
14627        // predecessor before any write. Schema 20 retains the typed singleton
14628        // and is the common predecessor for this alias-routed lifecycle test.
14629        incomplete_current.execute_batch(
14630            "DROP INDEX IF EXISTS idx_worktree_registrations_active_native_administrative_directory;
14631             DROP INDEX IF EXISTS idx_worktree_registrations_active_native_root;
14632             ALTER TABLE worktree_registrations DROP COLUMN git_common_directory_identity;
14633             ALTER TABLE worktree_registrations DROP COLUMN git_administrative_directory_identity;
14634             ALTER TABLE worktree_registrations DROP COLUMN last_root_identity;
14635             DROP TABLE graph_identity_rejections;
14636             UPDATE metadata SET value = '20' WHERE key = 'schema_version';",
14637        )?;
14638        drop(incomplete_current);
14639        let migratable_added = server.atlas_worktree_add(Parameters(AtlasWorktreeAddParams {
14640            worktree: selector_b,
14641            alias: Some("migratable-atlas".to_string()),
14642        }));
14643        require(
14644            migratable_added.contains("status: registered")
14645                && migratable_added
14646                    .contains("registration committed without local telemetry import"),
14647            &format!(
14648                "current repair atlas did not retain its explicit unbound registration: {migratable_added}"
14649            ),
14650        )?;
14651        let control_before_migration =
14652            open_atlas_store_read_only_for_project(&control_db, &primary)?;
14653        require(
14654            control_before_migration
14655                .worktree_registration(&WorktreeAlias::parse("migratable-atlas")?)?
14656                .project_instance_id
14657                .is_none(),
14658            "current repair fixture unexpectedly bound before scan",
14659        )?;
14660        drop(control_before_migration);
14661        let migrated = server.atlas_scan(Parameters(AtlasScanParams {
14662            project_path: None,
14663            worktree: Some("migratable-atlas".to_string()),
14664            path: None,
14665            nearest_project: Some(false),
14666            max_bytes: None,
14667            max_workers: Some(1),
14668            timeout_seconds: None,
14669            text_index_max_bytes: None,
14670            background: Some(false),
14671        }));
14672        require(
14673            migrated.contains("scan:"),
14674            &format!("alias-routed current repair did not complete its scan: {migrated}"),
14675        )?;
14676        let migrated_target = open_atlas_store_read_only_for_project(&target_b_db, &worktree_b)?;
14677        require(
14678            migrated_target.project_instance_id()? == Some(migratable_project),
14679            "current repair changed the worktree project identity",
14680        )?;
14681        drop(migrated_target);
14682        let control_after_migration =
14683            open_atlas_store_read_only_for_project(&control_db, &primary)?;
14684        require(
14685            control_after_migration
14686                .worktree_registration(&WorktreeAlias::parse("migratable-atlas")?)?
14687                .project_instance_id
14688                == Some(migratable_project),
14689            "alias-routed current repair left its registration unbound",
14690        )?;
14691        drop(control_after_migration);
14692        let retired_migratable =
14693            server.atlas_worktree_remove(Parameters(AtlasWorktreeRemoveParams {
14694                worktree: "migratable-atlas".to_string(),
14695            }));
14696        require(
14697            retired_migratable.contains("status: retired"),
14698            &format!("current-repair registration could not be retired: {retired_migratable}"),
14699        )?;
14700
14701        let added = server.atlas_worktree_add(Parameters(AtlasWorktreeAddParams {
14702            worktree: selector_a,
14703            alias: Some("issue-430".to_string()),
14704        }));
14705        require(
14706            added.contains("status: registered")
14707                && added.contains("alias: \"issue-430\"")
14708                && added.contains("git_unchanged: true")
14709                && added.contains("files_unchanged: true"),
14710            &format!("stable selector did not create one lifecycle-neutral registration: {added}"),
14711        )?;
14712        require(
14713            !worktree_a.join(".projectatlas").exists(),
14714            "registration created a target atlas before explicit init",
14715        )?;
14716
14717        let missing = server.atlas_overview_response(
14718            AtlasProjectParams {
14719                project_path: None,
14720                worktree: Some("issue-430".to_string()),
14721            },
14722            None,
14723        );
14724        require(
14725            missing.contains("init_required")
14726                && missing.contains("tool: atlas_init")
14727                && missing.contains("worktree: \"issue-430\"")
14728                && !missing.contains("project_path:"),
14729            "missing alias target did not preserve its short selector in typed init guidance",
14730        )?;
14731        let conflict = server.atlas_overview_response(
14732            AtlasProjectParams {
14733                project_path: Some(primary.to_string_lossy().to_string()),
14734                worktree: Some("issue-430".to_string()),
14735            },
14736            None,
14737        );
14738        require(
14739            conflict.contains(MCP_WORKTREE_PROJECT_PATH_CONFLICT),
14740            "worktree/project_path conflict was not rejected by the shared resolver",
14741        )?;
14742
14743        let initialized = server.atlas_init(Parameters(AtlasInitParams {
14744            project_path: None,
14745            worktree: Some("issue-430".to_string()),
14746            no_scan: Some(false),
14747            force_rescan: Some(false),
14748            text_index_max_bytes: None,
14749        }));
14750        require(
14751            initialized.contains("status: hydrated")
14752                && initialized.contains("source_project_instance_id:")
14753                && initialized.contains("target_project_instance_id:")
14754                && initialized.contains("reconciled_generation:")
14755                && initialized.contains("parsed: 1"),
14756            &format!("registered alias init did not expose one completed hydration: {initialized}"),
14757        )?;
14758        let target_db = worktree_a.join(".projectatlas").join("projectatlas.db");
14759        let target_store = open_atlas_store_read_only_for_project(&target_db, &worktree_a)?;
14760        require(
14761            target_store.load_node_by_path("src/branch.rs")?.is_some(),
14762            "hydration reconciliation omitted a target-only dirty source file",
14763        )?;
14764        require(
14765            target_store
14766                .load_node_by_path("src/lib.rs")?
14767                .is_some_and(|node| {
14768                    node.purpose.purpose.as_deref() == Some("Own the shared library contract.")
14769                        && node.purpose.source == PurposeSource::Agent
14770                }),
14771            "hydration did not preserve an applicable approved main purpose",
14772        )?;
14773        let target_project = target_store
14774            .project_instance_id()?
14775            .ok_or_else(|| io::Error::other("hydrated target identity is missing"))?;
14776        drop(target_store);
14777        let captured_alias_state = server.state_for_target(None, Some("issue-430".to_string()))?;
14778        let captured_federated_aliases = ["main".to_string(), "issue-430".to_string()];
14779        let (captured_federated_roots, captured_federated_selections) =
14780            server.federated_worktree_roots(&captured_federated_aliases)?;
14781        let captured_main_state = server.state_for_target(None, Some("main".to_string()))?;
14782        let control_after_init = open_atlas_store_read_only_for_project(&control_db, &primary)?;
14783        let control_project = control_after_init
14784            .project_instance_id()?
14785            .ok_or_else(|| io::Error::other("control project identity is missing"))?;
14786        require(
14787            captured_main_state
14788                .worktree
14789                .as_ref()
14790                .and_then(|selection| selection.project_instance_id)
14791                == Some(control_project),
14792            "main alias did not capture the current control atlas identity",
14793        )?;
14794        require(
14795            [&captured_main_state, &captured_alias_state]
14796                .iter()
14797                .all(|state| {
14798                    state
14799                        .worktree
14800                        .as_ref()
14801                        .and_then(|selection| selection.control_project_instance_id)
14802                        == Some(control_project)
14803                }),
14804            "alias selection did not capture its control atlas identity",
14805        )?;
14806        require(
14807            control_after_init
14808                .load_node_by_path("src/branch.rs")?
14809                .is_none(),
14810            "target-only source state bled into the control graph",
14811        )?;
14812        require(
14813            control_after_init
14814                .worktree_registration(&WorktreeAlias::parse("issue-430")?)?
14815                .project_instance_id
14816                == Some(target_project),
14817            "successful alias init did not bind the exact target atlas identity",
14818        )?;
14819        drop(control_after_init);
14820        let target_before_reset = fs::read(&target_db)?;
14821        let bound_reset = server.atlas_reset_index(Parameters(AtlasResetIndexParams {
14822            project_path: None,
14823            worktree: Some("issue-430".to_string()),
14824            apply: Some(true),
14825            dry_run: Some(false),
14826            include_mcp_config: Some(true),
14827        }));
14828        require(
14829            bound_reset.contains(MCP_ERROR_BOUND_WORKTREE_RESET_UNSUPPORTED),
14830            "applied reset did not reject a bound worktree alias",
14831        )?;
14832        require(
14833            fs::read(&target_db)? == target_before_reset,
14834            "rejected bound worktree reset changed its database",
14835        )?;
14836        let control_state = primary.join(PROJECTATLAS_DIR_NAME);
14837        let preserved_control_state = primary.join(".projectatlas-captured-main");
14838        fs::rename(&control_state, &preserved_control_state)?;
14839        fs::create_dir(&control_state)?;
14840        init_project_with_config(&primary, captured_main_state.config_path.as_deref())?;
14841        let mut replacement_control = AtlasStore::open_for_project(&control_db, &primary)?;
14842        let replacement_control_plan =
14843            ScanRuntimePlan::for_path(captured_main_state.config_path.as_deref(), &primary, None)?;
14844        run_scan_pipeline(
14845            &mut replacement_control,
14846            &replacement_control_plan,
14847            &SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, None, None),
14848        )?;
14849        require(
14850            replacement_control.project_instance_id()? != Some(control_project),
14851            "replacement control atlas reused the captured main identity",
14852        )?;
14853        let captured_registration_id = captured_alias_state
14854            .worktree
14855            .as_ref()
14856            .and_then(|selection| selection.registration_id)
14857            .ok_or_else(|| io::Error::other("captured alias registration identity is missing"))?;
14858        let common = primary.join(".git");
14859        let mut replacement_alias = None;
14860        for index in 1..=captured_registration_id {
14861            let alias = WorktreeAlias::parse(&format!("replacement-{index}"))?;
14862            let registration = replacement_control.register_worktree(
14863                &alias,
14864                &common,
14865                &common
14866                    .join("worktrees")
14867                    .join(format!("replacement-{index}")),
14868                &format!("{index:064x}"),
14869                &temp.path().join(format!("replacement-worktree-{index}")),
14870                Some(ProjectInstanceId::from_bytes([u8::try_from(index)?; 16])?),
14871                u64::try_from(index)?,
14872            )?;
14873            require(
14874                registration.registration_id == index,
14875                "replacement control did not reuse the expected registration identity",
14876            )?;
14877            if index == captured_registration_id {
14878                replacement_alias = Some(alias);
14879            }
14880        }
14881        let replacement_alias = replacement_alias
14882            .ok_or_else(|| io::Error::other("replacement alias was not created"))?;
14883        let replacement_calls_before = replacement_control
14884            .registered_worktree_token_overview(&replacement_alias)?
14885            .calls;
14886        require(
14887            ProjectAtlasMcpServer::require_captured_control_identity(
14888                captured_alias_state.worktree.as_ref(),
14889                &replacement_control,
14890            )
14891            .as_ref()
14892            .is_err_and(|error| {
14893                error
14894                    .to_string()
14895                    .contains(MCP_ERROR_WORKTREE_CONTROL_IDENTITY_CONFLICT)
14896            }),
14897            "captured alias accepted a replacement control atlas",
14898        )?;
14899        drop(replacement_control);
14900        let accepted = server.with_fresh_string_and_usage_for_request(
14901            &captured_alias_state,
14902            None,
14903            |_store, _stamp| {
14904                Ok((
14905                    "accepted result".to_string(),
14906                    Some(McpUsageIntent::estimate(
14907                        MCP_EVENT_ATLAS_OVERVIEW,
14908                        None,
14909                        None,
14910                        1,
14911                    )),
14912                ))
14913            },
14914        )?;
14915        require(
14916            accepted == "accepted result",
14917            "accepted target read was lost",
14918        )?;
14919        let replacement_control = open_atlas_store_read_only_for_project(&control_db, &primary)?;
14920        require(
14921            replacement_control
14922                .registered_worktree_token_overview(&replacement_alias)?
14923                .calls
14924                == replacement_calls_before,
14925            "deferred telemetry was attributed through a replacement control catalog",
14926        )?;
14927        drop(replacement_control);
14928        require(
14929            matches!(
14930                synchronize_registered_worktree_usage(
14931                    &captured_main_state.db_path,
14932                    &captured_main_state.root,
14933                    Some(control_project),
14934                ),
14935                Err(CliError::InvalidInput(message))
14936                    if message.contains("control atlas identity changed")
14937            ),
14938            "main token synchronization did not revalidate its captured project identity",
14939        )?;
14940        let captured_main_direct_read =
14941            ProjectAtlasMcpServer::open_read_store(&captured_main_state);
14942        require(
14943            captured_main_direct_read.as_ref().is_err_and(|error| {
14944                error
14945                    .to_string()
14946                    .contains(MCP_ERROR_WORKTREE_IDENTITY_CONFLICT)
14947            }),
14948            "main direct token read did not revalidate its captured project identity",
14949        )?;
14950        let captured_main_read =
14951            server.with_fresh_store(&captured_main_state, |_store, _stamp| Ok(()));
14952        require(
14953            captured_main_read.as_ref().is_err_and(|error| {
14954                error
14955                    .to_string()
14956                    .contains(MCP_ERROR_WORKTREE_IDENTITY_CONFLICT)
14957            }),
14958            "main read snapshot did not revalidate its captured project identity",
14959        )?;
14960        let captured_main_write = ProjectAtlasMcpServer::open_existing_mut_store(
14961            &captured_main_state,
14962            &server.control_state,
14963        );
14964        require(
14965            captured_main_write.as_ref().is_err_and(|error| {
14966                error
14967                    .to_string()
14968                    .contains(MCP_ERROR_WORKTREE_IDENTITY_CONFLICT)
14969            }),
14970            "main mutation did not revalidate its captured project identity",
14971        )?;
14972        let captured_federated_labels = captured_federated_selections
14973            .iter()
14974            .map(|selection| selection.alias.clone())
14975            .collect::<Vec<_>>();
14976        let federated_control =
14977            index_work_control(&SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, None, None));
14978        let captured_federated_stores = open_federated_atlas_stores_for_project(
14979            &captured_main_state.db_path,
14980            &captured_main_state.root,
14981            captured_main_state.config_path.as_deref(),
14982            &captured_federated_roots,
14983            Some(&captured_federated_labels),
14984            &federated_control,
14985        )?;
14986        let captured_main_federation = ProjectAtlasMcpServer::require_federated_worktree_identities(
14987            captured_federated_stores,
14988            &captured_federated_selections,
14989        );
14990        require(
14991            captured_main_federation.as_ref().is_err_and(|error| {
14992                error
14993                    .to_string()
14994                    .contains(MCP_ERROR_WORKTREE_IDENTITY_CONFLICT)
14995                    && error.to_string().contains(MCP_MAIN_WORKTREE_ALIAS)
14996            }),
14997            "federation did not revalidate the captured main identity",
14998        )?;
14999        fs::remove_dir_all(&control_state)?;
15000        fs::rename(&preserved_control_state, &control_state)?;
15001        let target_state = worktree_a.join(PROJECTATLAS_DIR_NAME);
15002        let preserved_target_state = worktree_a.join(".projectatlas-registered");
15003        fs::rename(&target_state, &preserved_target_state)?;
15004        fs::create_dir(&target_state)?;
15005        init_project_with_config(&worktree_a, captured_alias_state.config_path.as_deref())?;
15006        let mut replacement_store = AtlasStore::open_for_project(&target_db, &worktree_a)?;
15007        let replacement_plan = ScanRuntimePlan::for_path(
15008            captured_alias_state.config_path.as_deref(),
15009            &worktree_a,
15010            None,
15011        )?;
15012        run_scan_pipeline(
15013            &mut replacement_store,
15014            &replacement_plan,
15015            &SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, None, None),
15016        )?;
15017        let replacement_project = replacement_store
15018            .project_instance_id()?
15019            .ok_or_else(|| io::Error::other("replacement target identity is missing"))?;
15020        require(
15021            replacement_project != target_project,
15022            "replacement atlas reused the registered project identity",
15023        )?;
15024        drop(replacement_store);
15025        let captured_read = server.with_fresh_store(&captured_alias_state, |_store, _stamp| Ok(()));
15026        require(
15027            captured_read.as_ref().is_err_and(|error| {
15028                error
15029                    .to_string()
15030                    .contains(MCP_ERROR_WORKTREE_IDENTITY_CONFLICT)
15031            }),
15032            "alias read snapshot did not revalidate its captured project identity",
15033        )?;
15034        let captured_federated_labels = captured_federated_selections
15035            .iter()
15036            .map(|selection| selection.alias.clone())
15037            .collect::<Vec<_>>();
15038        let federated_control =
15039            index_work_control(&SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, None, None));
15040        let captured_federated_stores = open_federated_atlas_stores_for_project(
15041            &captured_main_state.db_path,
15042            &captured_main_state.root,
15043            captured_main_state.config_path.as_deref(),
15044            &captured_federated_roots,
15045            Some(&captured_federated_labels),
15046            &federated_control,
15047        )?;
15048        let captured_federation = ProjectAtlasMcpServer::require_federated_worktree_identities(
15049            captured_federated_stores,
15050            &captured_federated_selections,
15051        );
15052        require(
15053            captured_federation.as_ref().is_err_and(|error| {
15054                error
15055                    .to_string()
15056                    .contains(MCP_ERROR_WORKTREE_IDENTITY_CONFLICT)
15057                    && error.to_string().contains("issue-430")
15058            }),
15059            "federated snapshots did not retain their captured alias identities",
15060        )?;
15061        let replacement_error = server.state_for_target(None, Some("issue-430".to_string()));
15062        require(
15063            replacement_error.as_ref().is_err_and(|error| {
15064                error
15065                    .to_string()
15066                    .contains(MCP_ERROR_WORKTREE_IDENTITY_CONFLICT)
15067            }),
15068            "alias routing accepted a replacement atlas at the registered root",
15069        )?;
15070        fs::remove_dir_all(&target_state)?;
15071        fs::rename(&preserved_target_state, &target_state)?;
15072        let selected = server.state_for_target(None, Some("issue-430".to_string()))?;
15073        require(
15074            selected.root == canonical_a
15075                && selected
15076                    .worktree
15077                    .as_ref()
15078                    .is_some_and(|selection| selection.alias == "issue-430"),
15079            "short alias did not capture the exact target root and identity",
15080        )?;
15081        let selected_diagnostics = ProjectAtlasMcpServer::render_project_state(&selected)?;
15082        require(
15083            selected_diagnostics.contains("worktree: \"issue-430\"")
15084                && selected_diagnostics.contains("registration_id:"),
15085            &format!(
15086                "selected-project diagnostics omitted the captured alias or registration identity: {selected_diagnostics}"
15087            ),
15088        )?;
15089        let settings = server.atlas_settings(Parameters(AtlasProjectParams {
15090            project_path: None,
15091            worktree: Some("issue-430".to_string()),
15092        }));
15093        require(
15094            settings.contains("worktree: \"issue-430\"") && settings.contains("registration_id:"),
15095            &format!(
15096                "alias-routed settings omitted the captured alias or registration identity: {settings}"
15097            ),
15098        )?;
15099        let root_report = server.atlas_root(Parameters(AtlasRootParams {
15100            project_path: None,
15101            worktree: Some("issue-430".to_string()),
15102            verify: Some(false),
15103            control_root: None,
15104        }));
15105        require(
15106            root_report.contains("worktree: \"issue-430\"")
15107                && root_report.contains("registration_id:"),
15108            &format!(
15109                "alias-routed root diagnostics omitted the captured alias or registration identity: {root_report}"
15110            ),
15111        )?;
15112        let routed_overview = server.atlas_overview_response(
15113            AtlasProjectParams {
15114                project_path: None,
15115                worktree: Some("issue-430".to_string()),
15116            },
15117            None,
15118        );
15119        require(
15120            routed_overview.contains("overview:") && routed_overview.contains("files:"),
15121            &format!("alias-routed overview failed: {routed_overview}"),
15122        )?;
15123        let alias = WorktreeAlias::parse("issue-430")?;
15124        let control_after_routed = open_atlas_store_read_only_for_project(&control_db, &primary)?;
15125        require(
15126            control_after_routed
15127                .registered_worktree_token_overview(&alias)?
15128                .calls
15129                == 1
15130                && control_after_routed.repository_token_overview()?.calls == 3,
15131            "alias-routed MCP usage or retained independently initialized usage was miscounted",
15132        )?;
15133        drop(control_after_routed);
15134        let local_event = usage_from_text(
15135            "worktree-local",
15136            "atlas_summary",
15137            Some("src/lib.rs".to_string()),
15138            None,
15139            "pub fn main_only() { child::helper(); }",
15140            "Own the shared library contract.",
15141        );
15142        let local_target = AtlasStore::open_for_project(&target_db, &worktree_a)?;
15143        local_target.record_usage(&local_event)?;
15144        require(
15145            local_target.token_overview(None)?.calls == 1,
15146            "independent worktree usage was not retained in its exact local atlas",
15147        )?;
15148        drop(local_target);
15149        let repository_tokens = server.atlas_token_report(Parameters(AtlasTokenParams {
15150            project_path: None,
15151            worktree: Some("main".to_string()),
15152            session: None,
15153            include_chart: Some(false),
15154            trend_window: None,
15155            benchmark_results: None,
15156            theme: None,
15157        }));
15158        require(
15159            repository_tokens.contains("worktree: main") && repository_tokens.contains("calls: 4"),
15160            &format!(
15161                "control token report did not combine routed and synchronized worktree usage: {repository_tokens}"
15162            ),
15163        )?;
15164        let worktree_tokens = server.atlas_token_report(Parameters(AtlasTokenParams {
15165            project_path: None,
15166            worktree: Some("issue-430".to_string()),
15167            session: None,
15168            include_chart: Some(false),
15169            trend_window: None,
15170            benchmark_results: None,
15171            theme: None,
15172        }));
15173        require(
15174            worktree_tokens.contains("worktree: \"issue-430\"")
15175                && worktree_tokens.contains("calls: 1"),
15176            &format!(
15177                "exact worktree token report included routed or sibling usage: {worktree_tokens}"
15178            ),
15179        )?;
15180        let federated = server.atlas_symbol_relations_response(
15181            &AtlasSymbolRelationsParams {
15182                file: Some("src/lib.rs".to_string()),
15183                view: Some(MCP_SYMBOL_RELATION_VIEW_DETAILED.to_string()),
15184                compact: Some(true),
15185                worktrees: Some(vec!["main".to_string(), "issue-430".to_string()]),
15186                limit: Some(1),
15187                ..AtlasSymbolRelationsParams::default()
15188            },
15189            None,
15190        );
15191        require(
15192            federated.contains("primary_worktree: main")
15193                && federated.contains("participants[2]{order,worktree")
15194                && federated.contains("0,main,")
15195                && federated.contains("1,\"issue-430\","),
15196            &format!(
15197                "alias federation did not label the primary and every participant: {federated}"
15198            ),
15199        )?;
15200        let federation_conflict = server.atlas_symbol_relations_response(
15201            &AtlasSymbolRelationsParams {
15202                file: Some("src/lib.rs".to_string()),
15203                view: Some(MCP_SYMBOL_RELATION_VIEW_DETAILED.to_string()),
15204                roots: Some(vec![
15205                    normalize_native_path_display(&primary),
15206                    normalize_native_path_display(&worktree_a),
15207                ]),
15208                worktrees: Some(vec!["main".to_string(), "issue-430".to_string()]),
15209                ..AtlasSymbolRelationsParams::default()
15210            },
15211            None,
15212        );
15213        require(
15214            federation_conflict.contains(MCP_ERROR_FEDERATED_SELECTOR_CONFLICT),
15215            "alias federation did not reject legacy roots before opening participants",
15216        )?;
15217        let target_database_before_blocker = fs::read(&target_db)?;
15218        fs::write(
15219            worktree_a.join("src").join("branch.rs"),
15220            "pub fn worktree_only_changed() {}\n",
15221        )?;
15222        let blocked_federation = server.atlas_symbol_relations_response(
15223            &AtlasSymbolRelationsParams {
15224                file: Some("src/lib.rs".to_string()),
15225                view: Some(MCP_SYMBOL_RELATION_VIEW_DETAILED.to_string()),
15226                worktrees: Some(vec!["main".to_string(), "issue-430".to_string()]),
15227                ..AtlasSymbolRelationsParams::default()
15228            },
15229            None,
15230        );
15231        require(
15232            blocked_federation.contains("refresh_required")
15233                && blocked_federation.contains("worktree: \"issue-430\""),
15234            &format!(
15235                "stale federated participant blocker omitted its exact worktree alias: {blocked_federation}"
15236            ),
15237        )?;
15238        require(
15239            fs::read(&target_db)? == target_database_before_blocker,
15240            "read-only alias federation repaired or changed a sibling database",
15241        )?;
15242        let main = server.state_for_target(None, Some("main".to_string()))?;
15243        require(
15244            main.root == primary.canonicalize()? && selected.root != main.root,
15245            "interleaved main and worktree selections bled into one target",
15246        )?;
15247        let source_before = fs::read(worktree_a.join("src").join("lib.rs"))?;
15248        let removed = server.atlas_worktree_remove(Parameters(AtlasWorktreeRemoveParams {
15249            worktree: "issue-430".to_string(),
15250        }));
15251        require(
15252            removed.contains("status: retired")
15253                && removed.contains("git_unchanged: true")
15254                && removed.contains("files_unchanged: true"),
15255            "unregister did not retire the alias with lifecycle-neutral status",
15256        )?;
15257        require(
15258            target_db.is_file()
15259                && worktree_a.join(".git").is_file()
15260                && fs::read(worktree_a.join("src").join("lib.rs"))? == source_before,
15261            "unregister deleted or modified target-owned Git, source, or atlas state",
15262        )?;
15263        let git_after = run_fixture_command(StdCommand::new("git").current_dir(&primary).args([
15264            "worktree",
15265            "list",
15266            "--porcelain",
15267        ]))?;
15268        require(
15269            git_after == git_before,
15270            "ProjectAtlas worktree operations changed Git lifecycle state",
15271        )?;
15272        let control = open_atlas_store_read_only_for_project(&control_db, &primary)?;
15273        let registrations = control.worktree_registrations(true)?;
15274        require(
15275            registrations.iter().any(|registration| {
15276                registration.alias.as_str() == "issue-430"
15277                    && registration.state == WorktreeRegistrationState::Retired
15278            }),
15279            "retired registration and retained telemetry identity were not durable",
15280        )?;
15281        require(
15282            server
15283                .state_for_target(None, Some("issue-430".to_string()))
15284                .is_err(),
15285            "retired alias remained selectable for source operations",
15286        )
15287    }
15288
15289    #[test]
15290    fn uninitialized_alias_rejects_a_reused_git_administrative_path()
15291    -> Result<(), Box<dyn std::error::Error>> {
15292        let temp = tempfile::tempdir()?;
15293        let primary = temp.path().join("control");
15294        let original = temp.path().join("first location").join("checkout");
15295        let replacement = temp.path().join("second location").join("checkout");
15296        fs::create_dir_all(&primary)?;
15297        fs::create_dir_all(
15298            original
15299                .parent()
15300                .ok_or_else(|| io::Error::other("original worktree has no parent"))?,
15301        )?;
15302        fs::create_dir_all(
15303            replacement
15304                .parent()
15305                .ok_or_else(|| io::Error::other("replacement worktree has no parent"))?,
15306        )?;
15307        run_fixture_command(StdCommand::new("git").current_dir(&primary).arg("init"))?;
15308        for (key, value) in [
15309            ("user.name", "ProjectAtlas Test"),
15310            ("user.email", "projectatlas@example.invalid"),
15311            ("commit.gpgsign", "false"),
15312        ] {
15313            run_fixture_command(
15314                StdCommand::new("git")
15315                    .current_dir(&primary)
15316                    .args(["config", key, value]),
15317            )?;
15318        }
15319        fs::write(primary.join("lib.rs"), "pub fn control() {}\n")?;
15320        run_fixture_command(
15321            StdCommand::new("git")
15322                .current_dir(&primary)
15323                .args(["add", "."]),
15324        )?;
15325        run_fixture_command(
15326            StdCommand::new("git")
15327                .current_dir(&primary)
15328                .args(["commit", "-m", "fixture"]),
15329        )?;
15330        run_fixture_command(
15331            StdCommand::new("git")
15332                .current_dir(&primary)
15333                .args(["worktree", "add", "-b", "original"])
15334                .arg(&original),
15335        )?;
15336
15337        let control_db = primary.join(".projectatlas").join("projectatlas.db");
15338        let control_config = primary.join(".projectatlas").join("config.toml");
15339        init_project_with_config(&primary, Some(&control_config))?;
15340        drop(AtlasStore::open_for_project(&control_db, &primary)?);
15341        let server = ProjectAtlasMcpServer::new(
15342            control_db,
15343            Some(control_config),
15344            "worktree-lifecycle".to_string(),
15345            false,
15346        );
15347        let repository = server.control_git_repository()?;
15348        let original_root = original.canonicalize()?;
15349        let original_entry = repository
15350            .worktrees
15351            .iter()
15352            .find(|entry| {
15353                ProjectAtlasMcpServer::active_worktree_root(entry) == Some(original_root.as_path())
15354            })
15355            .ok_or_else(|| io::Error::other("original worktree was not discovered"))?;
15356        let administrative_directory = original_entry.administrative_directory.clone();
15357        let administrative_identity = git_administrative_identity(&administrative_directory)?;
15358        let added = server.atlas_worktree_add(Parameters(AtlasWorktreeAddParams {
15359            worktree: ProjectAtlasMcpServer::worktree_candidate_selector(original_entry),
15360            alias: Some("replacement-check".to_string()),
15361        }));
15362        require(
15363            added.contains("status: registered"),
15364            &format!("original uninitialized worktree was not registered: {added}"),
15365        )?;
15366
15367        run_fixture_command(
15368            StdCommand::new("git")
15369                .current_dir(&primary)
15370                .args(["worktree", "remove", "--force"])
15371                .arg(&original),
15372        )?;
15373        let missing_registration =
15374            server.atlas_worktree_list(Parameters(AtlasWorktreeListParams {
15375                include_retired: Some(false),
15376            }));
15377        require(
15378            missing_registration
15379                .contains("\"replacement-check\",linked,available,missing,registered")
15380                && missing_registration.contains(&normalize_native_path_display(&original))
15381                && missing_registration.contains(",unavailable,unavailable,0,")
15382                && missing_registration.contains(MCP_WORKTREE_MISSING_RETENTION_REASON),
15383            &format!(
15384                "active alias disappeared after external Git worktree removal: {missing_registration}"
15385            ),
15386        )?;
15387        run_fixture_command(
15388            StdCommand::new("git")
15389                .current_dir(&primary)
15390                .args(["worktree", "add", "-b", "replacement"])
15391                .arg(&replacement),
15392        )?;
15393        let replacement_repository = server.control_git_repository()?;
15394        let replacement_root = replacement.canonicalize()?;
15395        let replacement_entry = replacement_repository
15396            .worktrees
15397            .iter()
15398            .find(|entry| {
15399                ProjectAtlasMcpServer::active_worktree_root(entry)
15400                    == Some(replacement_root.as_path())
15401            })
15402            .ok_or_else(|| io::Error::other("replacement worktree was not discovered"))?;
15403        require(
15404            replacement_entry.administrative_directory == administrative_directory,
15405            "Git fixture did not reuse the administrative path",
15406        )?;
15407        require(
15408            git_administrative_identity(&replacement_entry.administrative_directory)?
15409                != administrative_identity,
15410            "replacement Git worktree reused the prior lifecycle identity",
15411        )?;
15412        let add_revalidation = server.revalidate_worktree_candidate(
15413            &repository,
15414            original_entry,
15415            &administrative_identity,
15416        );
15417        require(
15418            add_revalidation.as_ref().is_err_and(|error| {
15419                error
15420                    .to_string()
15421                    .contains(MCP_ERROR_WORKTREE_LIFECYCLE_CHANGED)
15422            }),
15423            "add revalidation combined the old root with a replacement lifecycle",
15424        )?;
15425
15426        let resolution_error = server
15427            .state_for_target(None, Some("replacement-check".to_string()))
15428            .err()
15429            .ok_or_else(|| io::Error::other("reused administrative path was accepted"))?;
15430        require(
15431            resolution_error
15432                .to_string()
15433                .contains(MCP_ERROR_WORKTREE_LIFECYCLE_CHANGED),
15434            "reused administrative path did not return the lifecycle blocker",
15435        )?;
15436        require(
15437            !replacement.join(".projectatlas").exists(),
15438            "failed lifecycle validation initialized the replacement worktree",
15439        )?;
15440        let before_remove =
15441            run_fixture_command(StdCommand::new("git").current_dir(&primary).args([
15442                "worktree",
15443                "list",
15444                "--porcelain",
15445            ]))?;
15446        let removed = server.atlas_worktree_remove(Parameters(AtlasWorktreeRemoveParams {
15447            worktree: "replacement-check".to_string(),
15448        }));
15449        require(
15450            removed.contains("status: retired")
15451                && removed.contains(MCP_ERROR_WORKTREE_LIFECYCLE_CHANGED),
15452            &format!("stale registration could not be retired safely: {removed}"),
15453        )?;
15454        let after_remove =
15455            run_fixture_command(StdCommand::new("git").current_dir(&primary).args([
15456                "worktree",
15457                "list",
15458                "--porcelain",
15459            ]))?;
15460        require(
15461            after_remove == before_remove,
15462            "retiring a stale registration changed Git lifecycle state",
15463        )
15464    }
15465
15466    #[test]
15467    fn reused_git_administrative_path_cannot_synchronize_replacement_telemetry()
15468    -> Result<(), Box<dyn std::error::Error>> {
15469        let temp = tempfile::tempdir()?;
15470        let primary = temp.path().join("control");
15471        let worktree = temp.path().join("checkout");
15472        fs::create_dir_all(&primary)?;
15473        run_fixture_command(StdCommand::new("git").current_dir(&primary).arg("init"))?;
15474        for (key, value) in [
15475            ("user.name", "ProjectAtlas Test"),
15476            ("user.email", "projectatlas@example.invalid"),
15477            ("commit.gpgsign", "false"),
15478        ] {
15479            run_fixture_command(
15480                StdCommand::new("git")
15481                    .current_dir(&primary)
15482                    .args(["config", key, value]),
15483            )?;
15484        }
15485        fs::write(primary.join("lib.rs"), "pub fn control() {}\n")?;
15486        run_fixture_command(
15487            StdCommand::new("git")
15488                .current_dir(&primary)
15489                .args(["add", "."]),
15490        )?;
15491        run_fixture_command(
15492            StdCommand::new("git")
15493                .current_dir(&primary)
15494                .args(["commit", "-m", "fixture"]),
15495        )?;
15496        run_fixture_command(
15497            StdCommand::new("git")
15498                .current_dir(&primary)
15499                .args(["worktree", "add", "-b", "original-telemetry"])
15500                .arg(&worktree),
15501        )?;
15502
15503        let control_db = primary.join(".projectatlas").join("projectatlas.db");
15504        let target_db = worktree.join(".projectatlas").join("projectatlas.db");
15505        fs::create_dir_all(
15506            control_db
15507                .parent()
15508                .ok_or_else(|| io::Error::other("control database has no parent"))?,
15509        )?;
15510        fs::create_dir_all(
15511            target_db
15512                .parent()
15513                .ok_or_else(|| io::Error::other("target database has no parent"))?,
15514        )?;
15515        let event = usage_from_text(
15516            "worktree-lifecycle",
15517            "atlas_overview",
15518            None,
15519            None,
15520            "pub fn source() {}",
15521            "repository overview",
15522        );
15523        let target = AtlasStore::open_for_project(&target_db, &worktree)?;
15524        target.record_usage(&event)?;
15525        let target_project = target
15526            .project_instance_id()?
15527            .ok_or_else(|| io::Error::other("target project identity is missing"))?;
15528        drop(target);
15529
15530        let RepositoryStructure::Git(repository) = discover_repository_structure(&primary)? else {
15531            return Err(io::Error::other("Git repository was not discovered").into());
15532        };
15533        let canonical_worktree = worktree.canonicalize()?;
15534        let entry = repository
15535            .worktrees
15536            .iter()
15537            .find(|entry| {
15538                ProjectAtlasMcpServer::active_worktree_root(entry)
15539                    == Some(canonical_worktree.as_path())
15540            })
15541            .ok_or_else(|| io::Error::other("worktree was not discovered"))?;
15542        let administrative_directory = entry.administrative_directory.clone();
15543        let administrative_identity = git_administrative_identity(&administrative_directory)?;
15544        let alias = WorktreeAlias::parse("lifecycle-telemetry")?;
15545        let control = AtlasStore::open_for_project(&control_db, &primary)?;
15546        control.register_worktree(
15547            &alias,
15548            &repository.common_directory,
15549            &administrative_directory,
15550            &administrative_identity,
15551            &canonical_worktree,
15552            Some(target_project),
15553            1,
15554        )?;
15555        drop(control);
15556        synchronize_registered_worktree_usage(&control_db, &primary, None)?;
15557        let control = AtlasStore::open_for_project(&control_db, &primary)?;
15558        require(
15559            control.registered_worktree_token_overview(&alias)?.calls == 1,
15560            "initial worktree telemetry was not synchronized",
15561        )?;
15562        drop(control);
15563
15564        let saved_db = temp.path().join("saved-projectatlas.db");
15565        fs::copy(&target_db, &saved_db)?;
15566        run_fixture_command(
15567            StdCommand::new("git")
15568                .current_dir(&primary)
15569                .args(["worktree", "remove", "--force"])
15570                .arg(&worktree),
15571        )?;
15572        require(
15573            matches!(
15574                synchronize_registered_worktree_usage(&control_db, &primary, None),
15575                Err(CliError::InvalidInput(message))
15576                    if message.contains("aggregate token totals cannot be synchronized")
15577            ),
15578            "externally removed bound worktree reported stale aggregate success",
15579        )?;
15580        run_fixture_command(
15581            StdCommand::new("git")
15582                .current_dir(&primary)
15583                .args(["worktree", "add", "-b", "replacement-telemetry"])
15584                .arg(&worktree),
15585        )?;
15586        fs::create_dir_all(
15587            target_db
15588                .parent()
15589                .ok_or_else(|| io::Error::other("target database has no parent"))?,
15590        )?;
15591        fs::copy(&saved_db, &target_db)?;
15592        let replacement = AtlasStore::open_for_project(&target_db, &worktree)?;
15593        replacement.record_usage(&event)?;
15594        drop(replacement);
15595
15596        let RepositoryStructure::Git(replacement_repository) =
15597            discover_repository_structure(&primary)?
15598        else {
15599            return Err(io::Error::other("replacement Git repository was not discovered").into());
15600        };
15601        let replacement_entry = replacement_repository
15602            .worktrees
15603            .iter()
15604            .find(|entry| {
15605                ProjectAtlasMcpServer::active_worktree_root(entry)
15606                    == Some(canonical_worktree.as_path())
15607            })
15608            .ok_or_else(|| io::Error::other("replacement worktree was not discovered"))?;
15609        require(
15610            replacement_entry.administrative_directory == administrative_directory,
15611            "Git fixture did not reuse the administrative path",
15612        )?;
15613        require(
15614            git_administrative_identity(&replacement_entry.administrative_directory)?
15615                != administrative_identity,
15616            "replacement Git worktree reused the prior lifecycle identity",
15617        )?;
15618
15619        require(
15620            matches!(
15621                synchronize_registered_worktree_usage(&control_db, &primary, None),
15622                Err(CliError::InvalidInput(message))
15623                    if message.contains("aggregate token totals cannot be synchronized")
15624            ),
15625            "replacement lifecycle reported stale aggregate success",
15626        )?;
15627        let control = open_atlas_store_read_only_for_project(&control_db, &primary)?;
15628        require(
15629            control.registered_worktree_token_overview(&alias)?.calls == 1,
15630            "replacement lifecycle telemetry was imported into the retired origin",
15631        )
15632    }
15633
15634    #[test]
15635    fn aggregate_synchronization_propagates_local_atlas_and_identity_failures()
15636    -> Result<(), Box<dyn std::error::Error>> {
15637        let temp = tempfile::tempdir()?;
15638        let primary = temp.path().join("control");
15639        let worktree = temp.path().join("external").join("worktree");
15640        fs::create_dir_all(&primary)?;
15641        fs::create_dir_all(
15642            worktree
15643                .parent()
15644                .ok_or_else(|| io::Error::other("worktree has no parent"))?,
15645        )?;
15646        run_fixture_command(StdCommand::new("git").current_dir(&primary).arg("init"))?;
15647        for (key, value) in [
15648            ("user.name", "ProjectAtlas Test"),
15649            ("user.email", "projectatlas@example.invalid"),
15650            ("commit.gpgsign", "false"),
15651        ] {
15652            run_fixture_command(
15653                StdCommand::new("git")
15654                    .current_dir(&primary)
15655                    .args(["config", key, value]),
15656            )?;
15657        }
15658        fs::write(primary.join("README.md"), "# fixture\n")?;
15659        run_fixture_command(
15660            StdCommand::new("git")
15661                .current_dir(&primary)
15662                .args(["add", "."]),
15663        )?;
15664        run_fixture_command(
15665            StdCommand::new("git")
15666                .current_dir(&primary)
15667                .args(["commit", "-m", "fixture"]),
15668        )?;
15669        run_fixture_command(
15670            StdCommand::new("git")
15671                .current_dir(&primary)
15672                .args(["worktree", "add", "-b", "sync-failure"])
15673                .arg(&worktree),
15674        )?;
15675
15676        let control_db = primary.join(".projectatlas").join("projectatlas.db");
15677        let target_db = worktree.join(".projectatlas").join("projectatlas.db");
15678        fs::create_dir_all(
15679            control_db
15680                .parent()
15681                .ok_or_else(|| io::Error::other("control database has no parent"))?,
15682        )?;
15683        fs::create_dir_all(
15684            target_db
15685                .parent()
15686                .ok_or_else(|| io::Error::other("target database has no parent"))?,
15687        )?;
15688        let target = AtlasStore::open_for_project(&target_db, &worktree)?;
15689        target.record_usage(&usage_from_text(
15690            "worktree-sync",
15691            "atlas_overview",
15692            None,
15693            None,
15694            "pub fn source() {}",
15695            "repository overview",
15696        ))?;
15697        let target_project = target
15698            .project_instance_id()?
15699            .ok_or_else(|| io::Error::other("target project identity is missing"))?;
15700        drop(target);
15701
15702        let RepositoryStructure::Git(repository) = discover_repository_structure(&primary)? else {
15703            return Err(io::Error::other("Git repository was not discovered").into());
15704        };
15705        let canonical_worktree = worktree.canonicalize()?;
15706        let entry = repository
15707            .worktrees
15708            .iter()
15709            .find(|entry| {
15710                ProjectAtlasMcpServer::active_worktree_root(entry)
15711                    == Some(canonical_worktree.as_path())
15712            })
15713            .ok_or_else(|| io::Error::other("worktree was not discovered"))?;
15714        let alias = WorktreeAlias::parse("sync-failure")?;
15715        let foreign_project = if target_project == ProjectInstanceId::from_bytes([0x7f; 16])? {
15716            ProjectInstanceId::from_bytes([0x7e; 16])?
15717        } else {
15718            ProjectInstanceId::from_bytes([0x7f; 16])?
15719        };
15720        let control = AtlasStore::open_for_project(&control_db, &primary)?;
15721        control.register_worktree(
15722            &alias,
15723            &repository.common_directory,
15724            &entry.administrative_directory,
15725            &git_administrative_identity(&entry.administrative_directory)?,
15726            &canonical_worktree,
15727            Some(foreign_project),
15728            1,
15729        )?;
15730        drop(control);
15731
15732        require(
15733            matches!(
15734                synchronize_registered_worktree_usage(&control_db, &primary, None),
15735                Err(CliError::Db(
15736                    DbError::WorktreeTelemetryProjectMismatch { .. }
15737                ))
15738            ),
15739            "aggregate synchronization hid the project identity failure behind stale success",
15740        )?;
15741
15742        let git_directory = primary.join(".git");
15743        let unavailable_git_directory = temp.path().join("unavailable-control-git");
15744        fs::rename(&git_directory, &unavailable_git_directory)?;
15745        require(
15746            matches!(
15747                synchronize_registered_worktree_usage(&control_db, &primary, None),
15748                Err(CliError::InvalidInput(message))
15749                    if message.contains("requires Git control evidence")
15750            ),
15751            "aggregate synchronization treated missing control Git evidence as success",
15752        )?;
15753        fs::rename(&unavailable_git_directory, &git_directory)?;
15754
15755        let control_head = git_directory.join("HEAD");
15756        let valid_control_head = temp.path().join("valid-control-head");
15757        fs::rename(&control_head, &valid_control_head)?;
15758        fs::create_dir(&control_head)?;
15759        require(
15760            matches!(
15761                synchronize_registered_worktree_usage(&control_db, &primary, None),
15762                Err(CliError::InvalidInput(message))
15763                    if message.contains("invalid Git evidence")
15764            ),
15765            "aggregate synchronization treated invalid control Git evidence as success",
15766        )?;
15767        fs::remove_dir(&control_head)?;
15768        fs::rename(&valid_control_head, &control_head)?;
15769
15770        let corrupt = rusqlite::Connection::open(&target_db)?;
15771        corrupt.pragma_update(None, "ignore_check_constraints", true)?;
15772        corrupt.execute("UPDATE usage_aggregate_revisions SET revision = -1", [])?;
15773        drop(corrupt);
15774        require(
15775            matches!(
15776                synchronize_registered_worktree_usage(&control_db, &primary, None),
15777                Err(CliError::Db(DbError::TelemetryIntegerOverflow {
15778                    field: "usage_aggregate_revisions.revision"
15779                }))
15780            ),
15781            "aggregate synchronization hid the local snapshot export failure",
15782        )?;
15783
15784        fs::remove_file(&target_db)?;
15785        require(
15786            matches!(
15787                synchronize_registered_worktree_usage(&control_db, &primary, None),
15788                Err(CliError::InvalidInput(message))
15789                    if message.contains("aggregate token totals cannot be synchronized")
15790            ),
15791            "aggregate synchronization reported stale success for a bound missing atlas",
15792        )?;
15793        fs::create_dir(&target_db)?;
15794        require(
15795            synchronize_registered_worktree_usage(&control_db, &primary, None).is_err(),
15796            "aggregate synchronization hid the existing local atlas open failure",
15797        )
15798    }
15799
15800    #[test]
15801    fn registered_worktree_init_falls_back_from_incomplete_control_and_preserves_existing_atlas()
15802    -> Result<(), Box<dyn std::error::Error>> {
15803        let temp = tempfile::tempdir()?;
15804        let primary = temp.path().join("control");
15805        let linked = temp.path().join("external").join("linked");
15806        fs::create_dir_all(&primary)?;
15807        fs::create_dir_all(
15808            linked
15809                .parent()
15810                .ok_or_else(|| io::Error::other("linked worktree has no parent"))?,
15811        )?;
15812        run_fixture_command(StdCommand::new("git").current_dir(&primary).arg("init"))?;
15813        for (key, value) in [
15814            ("user.name", "ProjectAtlas Test"),
15815            ("user.email", "projectatlas@example.invalid"),
15816            ("commit.gpgsign", "false"),
15817        ] {
15818            run_fixture_command(
15819                StdCommand::new("git")
15820                    .current_dir(&primary)
15821                    .args(["config", key, value]),
15822            )?;
15823        }
15824        fs::write(primary.join("lib.rs"), "pub fn control() {}\n")?;
15825        run_fixture_command(
15826            StdCommand::new("git")
15827                .current_dir(&primary)
15828                .args(["add", "."]),
15829        )?;
15830        run_fixture_command(
15831            StdCommand::new("git")
15832                .current_dir(&primary)
15833                .args(["commit", "-m", "fixture"]),
15834        )?;
15835        run_fixture_command(
15836            StdCommand::new("git")
15837                .current_dir(&primary)
15838                .args(["worktree", "add", "-b", "fallback"])
15839                .arg(&linked),
15840        )?;
15841        let git_before = run_fixture_command(StdCommand::new("git").current_dir(&primary).args([
15842            "worktree",
15843            "list",
15844            "--porcelain",
15845        ]))?;
15846
15847        let control_db = primary.join(".projectatlas").join("projectatlas.db");
15848        fs::create_dir_all(
15849            control_db
15850                .parent()
15851                .ok_or_else(|| io::Error::other("control DB has no parent"))?,
15852        )?;
15853        drop(AtlasStore::open_for_project(&control_db, &primary)?);
15854        let server = ProjectAtlasMcpServer::new(
15855            control_db.clone(),
15856            None,
15857            "worktree-fallback".to_string(),
15858            false,
15859        );
15860        let repository = server.control_git_repository()?;
15861        let canonical_linked = linked.canonicalize()?;
15862        let entry = repository
15863            .worktrees
15864            .iter()
15865            .find(|entry| {
15866                ProjectAtlasMcpServer::active_worktree_root(entry)
15867                    == Some(canonical_linked.as_path())
15868            })
15869            .ok_or_else(|| io::Error::other("linked worktree was not discovered"))?;
15870        let added = server.atlas_worktree_add(Parameters(AtlasWorktreeAddParams {
15871            worktree: ProjectAtlasMcpServer::worktree_candidate_selector(entry),
15872            alias: Some("fallback".to_string()),
15873        }));
15874        require(
15875            added.contains("status: registered"),
15876            "fallback fixture registration failed",
15877        )?;
15878
15879        let initialized = server.atlas_init(Parameters(AtlasInitParams {
15880            project_path: None,
15881            worktree: Some("fallback".to_string()),
15882            no_scan: Some(false),
15883            force_rescan: Some(false),
15884            text_index_max_bytes: None,
15885        }));
15886        require(
15887            initialized.contains("status: fallback")
15888                && initialized.contains("fallback_reason:")
15889                && initialized.contains("repository graph is unavailable"),
15890            &format!(
15891                "incomplete control atlas did not produce visible ordinary fallback: {initialized}"
15892            ),
15893        )?;
15894        let target_db = linked.join(".projectatlas").join("projectatlas.db");
15895        let target = open_atlas_store_read_only_for_project(&target_db, &linked)?;
15896        let identity = target
15897            .project_instance_id()?
15898            .ok_or_else(|| io::Error::other("fallback target identity is missing"))?;
15899        require(
15900            target.index_publication()?.is_some_and(|publication| {
15901                publication.state == projectatlas_db::IndexPublicationState::Complete
15902            }),
15903            "ordinary fallback did not publish a complete target index",
15904        )?;
15905        drop(target);
15906
15907        let repeated = server.atlas_init(Parameters(AtlasInitParams {
15908            project_path: None,
15909            worktree: Some("fallback".to_string()),
15910            no_scan: Some(true),
15911            force_rescan: Some(false),
15912            text_index_max_bytes: None,
15913        }));
15914        require(
15915            repeated.contains("status: existing"),
15916            &format!("repeat init did not preserve the valid target atlas: {repeated}"),
15917        )?;
15918        let preserved = open_atlas_store_read_only_for_project(&target_db, &linked)?;
15919        require(
15920            preserved.project_instance_id()? == Some(identity),
15921            "repeat init replaced the valid target atlas identity",
15922        )?;
15923        let control = open_atlas_store_read_only_for_project(&control_db, &primary)?;
15924        require(
15925            control
15926                .worktree_registration(&WorktreeAlias::parse("fallback")?)?
15927                .project_instance_id
15928                == Some(identity),
15929            "fallback init did not bind the exact target identity",
15930        )?;
15931        let git_after = run_fixture_command(StdCommand::new("git").current_dir(&primary).args([
15932            "worktree",
15933            "list",
15934            "--porcelain",
15935        ]))?;
15936        require(
15937            git_after == git_before,
15938            "fallback or repeat init changed Git lifecycle state",
15939        )?;
15940
15941        let config_path = init_config_path(&linked, None);
15942        let config_before = fs::read(&config_path)?;
15943        drop(preserved);
15944        drop(control);
15945        fs::remove_file(&target_db)?;
15946        let refused = server.atlas_init(Parameters(AtlasInitParams {
15947            project_path: None,
15948            worktree: Some("fallback".to_string()),
15949            no_scan: Some(false),
15950            force_rescan: Some(false),
15951            text_index_max_bytes: None,
15952        }));
15953        require(
15954            refused.contains(MCP_ERROR_BOUND_WORKTREE_ATLAS_MISSING),
15955            &format!("bound missing atlas did not fail before initialization: {refused}"),
15956        )?;
15957        require(
15958            !target_db.exists() && fs::read(&config_path)? == config_before,
15959            "bound missing atlas refusal changed target ProjectAtlas state",
15960        )
15961    }
15962
15963    #[test]
15964    fn bare_startup_and_root_set_preserve_worktree_required_without_state()
15965    -> Result<(), Box<dyn std::error::Error>> {
15966        let temp = tempfile::tempdir()?;
15967        let bare = temp.path().join("repository.git");
15968        let output = StdCommand::new("git")
15969            .args(["init", "--bare"])
15970            .arg(&bare)
15971            .output()?;
15972        require(output.status.success(), "git init --bare failed")?;
15973        let db_path = bare.join(".projectatlas").join("projectatlas.db");
15974        let server =
15975            ProjectAtlasMcpServer::new(db_path, None, "mcp-bare-root-test".to_string(), false);
15976
15977        let Err(error) = server.active_project_state() else {
15978            return Err(io::Error::other("bare MCP startup state was exposed as active").into());
15979        };
15980        if !matches!(error, CliError::WorktreeRequired(_)) {
15981            return Err(io::Error::other(format!(
15982                "bare MCP startup did not preserve typed worktree_required state: {error:?}"
15983            ))
15984            .into());
15985        }
15986
15987        let response = server.atlas_root_set(Parameters(AtlasRootSetParams {
15988            root: bare.to_string_lossy().to_string(),
15989            transition: None,
15990            nearest_project: None,
15991        }));
15992        require(
15993            response.contains("worktree_required"),
15994            "atlas_root_set did not reject a bare Git control root",
15995        )?;
15996        require(
15997            !bare.join(".projectatlas").exists(),
15998            "bare MCP startup or root-set refusal created project state",
15999        )
16000    }
16001
16002    #[test]
16003    fn selected_project_config_cannot_redirect_root() -> Result<(), Box<dyn std::error::Error>> {
16004        let temp = tempfile::tempdir()?;
16005        let repo_a = temp.path().join("repo-a");
16006        let repo_b = temp.path().join("repo-b");
16007        fs::create_dir(&repo_a)?;
16008        fs::create_dir(&repo_b)?;
16009        fs::create_dir(repo_b.join(".projectatlas"))?;
16010        let escaped_repo_a = repo_a.to_string_lossy().replace('\\', "/");
16011        fs::write(
16012            repo_b.join(".projectatlas").join("config.toml"),
16013            format!("[project]\nroot = \"{escaped_repo_a}\"\n"),
16014        )?;
16015
16016        let Err(error) = ProjectAtlasMcpServer::project_state_from_root(&repo_b) else {
16017            return Err(io::Error::other("stale selected-project config was accepted").into());
16018        };
16019        require(
16020            error.to_string().contains("outside selected project root"),
16021            "stale selected-project config error was not root-scoped",
16022        )?;
16023
16024        Ok(())
16025    }
16026
16027    #[test]
16028    fn startup_config_mismatch_cannot_bind_one_root_to_another_db()
16029    -> Result<(), Box<dyn std::error::Error>> {
16030        let temp = tempfile::tempdir()?;
16031        let repo_a = temp.path().join("repo-a");
16032        let repo_b = temp.path().join("repo-b");
16033        fs::create_dir(&repo_a)?;
16034        fs::create_dir(&repo_b)?;
16035        fs::create_dir(repo_a.join(".projectatlas"))?;
16036        let escaped_repo_a = repo_a.to_string_lossy().replace('\\', "/");
16037        let config_a = repo_a.join(".projectatlas").join("config.toml");
16038        fs::write(
16039            &config_a,
16040            format!("[project]\nroot = \"{escaped_repo_a}\"\n"),
16041        )?;
16042
16043        let db_b = repo_b.join(".projectatlas").join("projectatlas.db");
16044        let server =
16045            ProjectAtlasMcpServer::new(db_b.clone(), Some(config_a), "mcp-test".to_string(), false);
16046        let state = server.active_project_state()?;
16047
16048        require(
16049            state.root == canonical_project_root(&repo_b)?,
16050            "startup state did not fall back to the DB project root",
16051        )?;
16052        require(
16053            state.db_path == db_b,
16054            "startup state changed the selected DB path",
16055        )?;
16056        require(
16057            state.config_path.is_none(),
16058            "startup state retained a config from another project root",
16059        )?;
16060
16061        Ok(())
16062    }
16063
16064    #[test]
16065    fn read_only_store_does_not_create_missing_index() -> Result<(), Box<dyn std::error::Error>> {
16066        let temp = tempfile::tempdir()?;
16067        let repo = temp.path().join("repo-a");
16068        fs::create_dir(&repo)?;
16069        let state = ProjectAtlasMcpServer::project_state_from_root(&repo)?;
16070
16071        let Err(error) = ProjectAtlasMcpServer::open_read_store(&state) else {
16072            return Err(io::Error::other("missing index opened unexpectedly").into());
16073        };
16074        require(
16075            matches!(error, CliError::InitRequired(_)),
16076            "missing index did not return typed init_required state",
16077        )?;
16078        let payload = ProjectAtlasMcpServer::encode_error_payload(&error);
16079        require(
16080            payload.contains("kind: init_required")
16081                && payload.contains("init_required:")
16082                && payload.contains("tool: atlas_init")
16083                && payload.contains(&normalize_native_path_display(&repo)),
16084            "missing index payload did not contain the exact atlas_init recovery call",
16085        )?;
16086        require(
16087            !repo.join(".projectatlas").exists(),
16088            "read-only store created .projectatlas",
16089        )?;
16090
16091        Ok(())
16092    }
16093
16094    #[test]
16095    fn atlas_init_explicit_project_path_bootstraps_without_switching_active_project()
16096    -> Result<(), Box<dyn std::error::Error>> {
16097        let temp = tempfile::tempdir()?;
16098        let repo_a = temp.path().join("repo-a");
16099        let repo_b = temp.path().join("repo-b");
16100        fs::create_dir(&repo_a)?;
16101        fs::create_dir(&repo_b)?;
16102        let db_a = repo_a.join(".projectatlas").join("projectatlas.db");
16103        let server = ProjectAtlasMcpServer::new(db_a, None, "mcp-test".to_string(), false);
16104        let active_before = server.active_project_state()?;
16105
16106        let text = server.atlas_init(Parameters(AtlasInitParams {
16107            project_path: Some(repo_b.to_string_lossy().to_string()),
16108            worktree: None,
16109            no_scan: Some(true),
16110            force_rescan: Some(false),
16111            text_index_max_bytes: None,
16112        }));
16113
16114        let expected_b = normalize_native_path_display(canonical_project_root(&repo_b)?);
16115        require(
16116            text.contains("init:"),
16117            "atlas_init did not return named init payload",
16118        )?;
16119        require(
16120            text.contains(&expected_b),
16121            "atlas_init did not report the explicit project path",
16122        )?;
16123        require(
16124            text.contains("status: skipped"),
16125            "atlas_init --no-scan did not report skipped scan",
16126        )?;
16127        require(
16128            text.contains("purpose_handoff:")
16129                && text.contains("execution_owner: agent_host")
16130                && text.contains(&format!(
16131                    "recommended_subagent_reasoning: {PURPOSE_CURATOR_RECOMMENDED_REASONING}"
16132                ))
16133                && text.contains("main_agent_fallback: true")
16134                && text.contains("server_started_curator: false")
16135                && text.contains("silent_on_success: true")
16136                && text.contains("curation_scope: low"),
16137            "atlas_init did not expose the host-owned low-scope curator handoff",
16138        )?;
16139        require(
16140            repo_b
16141                .join(".projectatlas")
16142                .join("projectatlas.db")
16143                .is_file(),
16144            "atlas_init did not create the explicit project's DB",
16145        )?;
16146        require(
16147            repo_b
16148                .join(".projectatlas")
16149                .join("projectatlas.mcp.json")
16150                .is_file()
16151                && repo_b
16152                    .join(".projectatlas")
16153                    .join("projectatlas.claude.mcp.json")
16154                    .is_file()
16155                && repo_b
16156                    .join(".projectatlas")
16157                    .join("projectatlas.opencode.json")
16158                    .is_file(),
16159            "atlas_init did not generate host MCP configs",
16160        )?;
16161
16162        let active_after = server.active_project_state()?;
16163        require(
16164            active_after.root == active_before.root,
16165            "atlas_init with explicit project_path changed the active default root",
16166        )?;
16167        require(
16168            !repo_a.join(".projectatlas").exists(),
16169            "explicit atlas_init mutated the active project",
16170        )?;
16171
16172        Ok(())
16173    }
16174
16175    #[test]
16176    fn session_brief_missing_index_stays_read_only() -> Result<(), Box<dyn std::error::Error>> {
16177        let temp = tempfile::tempdir()?;
16178        let repo = temp.path().join("repo-a");
16179        fs::create_dir(&repo)?;
16180        let db_path = repo.join(".projectatlas").join("projectatlas.db");
16181        let server = ProjectAtlasMcpServer::new(db_path, None, "mcp-test".to_string(), false);
16182
16183        let brief = server.build_session_brief(
16184            AtlasSessionBriefParams {
16185                project_path: None,
16186                worktree: None,
16187                query: Some("startup".to_string()),
16188                purpose_task: None,
16189                compact: None,
16190                folder_limit: None,
16191                file_limit: None,
16192                blocker_limit: None,
16193                purpose_limit: None,
16194            },
16195            None,
16196        )?;
16197
16198        require(
16199            brief.project.index_status == McpIndexStatus::Missing,
16200            "missing index was not represented as typed state",
16201        )?;
16202        require(
16203            brief.overview.is_none(),
16204            "missing-index brief unexpectedly included overview",
16205        )?;
16206        require(
16207            brief.recommendations.iter().any(|recommendation| {
16208                matches!(recommendation.kind, McpBriefRecommendationKind::Init)
16209                    && recommendation.target == MCP_TOOL_ATLAS_INIT
16210                    && recommendation.arguments.get(MCP_BRIEF_ARG_PROJECT_PATH)
16211                        == Some(&serde_json::json!(normalize_native_path_display(&repo)))
16212            }),
16213            "missing-index brief did not recommend atlas_init for the exact selected root",
16214        )?;
16215        require(
16216            !repo.join(".projectatlas").exists(),
16217            "session brief created .projectatlas for a missing index",
16218        )?;
16219
16220        Ok(())
16221    }
16222
16223    #[test]
16224    fn session_brief_recommendations_preserve_per_call_project_path()
16225    -> Result<(), Box<dyn std::error::Error>> {
16226        let project_path = "F:/example/repo-b".to_string();
16227        let recommendations = ProjectAtlasMcpServer::indexed_project_recommendations(
16228            "startup",
16229            Some(NavigationNextCall {
16230                capability: NavigationNextCapability::Summary,
16231                path: "src/lib.rs".to_string(),
16232            }),
16233            1,
16234            7,
16235            Some(project_path.clone()),
16236            None,
16237        );
16238
16239        require(
16240            recommendations.iter().all(|recommendation| {
16241                recommendation.arguments.get(MCP_BRIEF_ARG_PROJECT_PATH)
16242                    == Some(&serde_json::Value::String(project_path.clone()))
16243            }),
16244            "indexed brief recommendations did not preserve project_path",
16245        )?;
16246        require(
16247            recommendations.iter().any(|recommendation| {
16248                matches!(recommendation.kind, McpBriefRecommendationKind::Summary)
16249                    && recommendation.target == MCP_TOOL_ATLAS_FILE_SUMMARY
16250                    && recommendation.arguments.get(MCP_BRIEF_ARG_FILE)
16251                        == Some(&serde_json::Value::String("src/lib.rs".to_string()))
16252            }),
16253            "summary recommendation did not preserve the ranked file selector",
16254        )?;
16255        require(
16256            recommendations.iter().all(|recommendation| {
16257                recommendation.target != MCP_TOOL_ATLAS_FOLDERS
16258                    && recommendation.target != MCP_TOOL_ATLAS_FILES
16259            }),
16260            "indexed brief recommended rerunning folder or file ranking",
16261        )?;
16262        require(
16263            recommendations.iter().any(|recommendation| {
16264                matches!(recommendation.kind, McpBriefRecommendationKind::Health)
16265                    && recommendation.arguments.get(MCP_BRIEF_ARG_LIMIT)
16266                        == Some(&serde_json::json!(7))
16267            }),
16268            "health recommendation did not preserve limit",
16269        )?;
16270
16271        let relation_recommendations = ProjectAtlasMcpServer::indexed_project_recommendations(
16272            "startup",
16273            Some(NavigationNextCall {
16274                capability: NavigationNextCapability::Relations,
16275                path: "src/graph.rs".to_string(),
16276            }),
16277            0,
16278            7,
16279            Some(project_path),
16280            None,
16281        );
16282        require(
16283            relation_recommendations.iter().any(|recommendation| {
16284                matches!(recommendation.kind, McpBriefRecommendationKind::Relations)
16285                    && recommendation.target == MCP_TOOL_ATLAS_SYMBOL_RELATIONS
16286                    && recommendation.arguments.get(MCP_BRIEF_ARG_FILE)
16287                        == Some(&serde_json::Value::String("src/graph.rs".to_string()))
16288                    && recommendation.arguments.get(MCP_BRIEF_ARG_VIEW)
16289                        == Some(&serde_json::Value::String("detailed".to_string()))
16290            }),
16291            "relation recommendation did not preserve the ranked file and detailed view",
16292        )?;
16293
16294        let worktree_recommendations = ProjectAtlasMcpServer::indexed_project_recommendations(
16295            "startup",
16296            None,
16297            1,
16298            7,
16299            None,
16300            Some("issue-430".to_string()),
16301        );
16302        require(
16303            worktree_recommendations.iter().all(|recommendation| {
16304                recommendation.arguments.get(MCP_BRIEF_ARG_WORKTREE)
16305                    == Some(&serde_json::Value::String("issue-430".to_string()))
16306                    && recommendation
16307                        .arguments
16308                        .get(MCP_BRIEF_ARG_PROJECT_PATH)
16309                        .is_none()
16310            }),
16311            "indexed brief recommendations did not preserve the mutually exclusive worktree alias",
16312        )?;
16313
16314        Ok(())
16315    }
16316
16317    #[test]
16318    fn session_brief_file_candidates_ignore_indexed_text_fallback()
16319    -> Result<(), Box<dyn std::error::Error>> {
16320        let temp = tempfile::tempdir()?;
16321        let repo = temp.path().join("repo-a");
16322        fs::create_dir_all(repo.join("src"))?;
16323        fs::write(
16324            repo.join("src").join("owner.rs"),
16325            "const ROUTE: &str = \"hiddenNeedle\";\n",
16326        )?;
16327        let db_path = repo.join(".projectatlas").join("projectatlas.db");
16328        let plan = ScanRuntimePlan::for_path(None, &repo, None)?;
16329        let mut store = open_atlas_store_for_project(&db_path, &plan.root)?;
16330        let symbol_options = SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, Some(1), Some(30));
16331        run_scan_pipeline(&mut store, &plan, &symbol_options)?;
16332        drop(store);
16333
16334        let server = ProjectAtlasMcpServer::new(db_path, None, "mcp-test".to_string(), false);
16335        let brief = server.build_session_brief(
16336            AtlasSessionBriefParams {
16337                project_path: None,
16338                worktree: None,
16339                query: Some("hiddenNeedle".to_string()),
16340                purpose_task: None,
16341                compact: None,
16342                folder_limit: Some(5),
16343                file_limit: Some(5),
16344                blocker_limit: Some(5),
16345                purpose_limit: Some(5),
16346            },
16347            None,
16348        )?;
16349
16350        require(
16351            brief.files.is_empty(),
16352            "session brief returned a content-only indexed-text hit",
16353        )?;
16354        require(
16355            brief.recommendations.iter().any(|recommendation| {
16356                matches!(recommendation.kind, McpBriefRecommendationKind::Search)
16357                    && recommendation.target == MCP_TOOL_ATLAS_SEARCH
16358                    && recommendation.arguments.get(MCP_BRIEF_ARG_PATTERN)
16359                        == Some(&serde_json::Value::String("hiddenNeedle".to_string()))
16360            }),
16361            "session brief did not route a content-only query directly to indexed search",
16362        )?;
16363
16364        let navigable = server.build_session_brief(
16365            AtlasSessionBriefParams {
16366                project_path: None,
16367                worktree: None,
16368                query: Some("owner".to_string()),
16369                purpose_task: None,
16370                compact: None,
16371                folder_limit: Some(5),
16372                file_limit: Some(5),
16373                blocker_limit: Some(5),
16374                purpose_limit: Some(5),
16375            },
16376            None,
16377        )?;
16378        let candidate = navigable
16379            .files
16380            .first()
16381            .ok_or_else(|| std::io::Error::other("navigable brief file is missing"))?;
16382        require(
16383            candidate.reason_codes.contains(&RankedReasonCode::Path)
16384                && candidate.next_call.capability == NavigationNextCapability::Summary
16385                && !candidate.purpose_agent_reviewed,
16386            "session brief dropped ranked navigation evidence",
16387        )?;
16388        require(
16389            navigable.recommendations.iter().any(|recommendation| {
16390                matches!(recommendation.kind, McpBriefRecommendationKind::Summary)
16391                    && recommendation.target == MCP_TOOL_ATLAS_FILE_SUMMARY
16392                    && recommendation.arguments.get(MCP_BRIEF_ARG_FILE)
16393                        == Some(&serde_json::Value::String(candidate.path.clone()))
16394            }) && navigable.recommendations.iter().all(|recommendation| {
16395                recommendation.target != MCP_TOOL_ATLAS_FOLDERS
16396                    && recommendation.target != MCP_TOOL_ATLAS_FILES
16397            }),
16398            "session brief recommendation did not follow its returned ranked file directly",
16399        )?;
16400
16401        Ok(())
16402    }
16403
16404    #[test]
16405    fn mcp_navigation_and_session_brief_propagate_typed_graph_evidence()
16406    -> Result<(), Box<dyn std::error::Error>> {
16407        let temp = tempfile::tempdir()?;
16408        let repo = temp.path().join("repo-a");
16409        fs::create_dir_all(repo.join("src"))?;
16410        fs::create_dir_all(repo.join("tests"))?;
16411        fs::write(
16412            repo.join("Cargo.toml"),
16413            "[package]\nname = \"adapter-navigation\"\nversion = \"0.1.0\"\n",
16414        )?;
16415        for path in [
16416            "src/navigation_owner.rs",
16417            "src/navigation_local.rs",
16418            "src/navigation_unresolved.rs",
16419            "tests/navigation_owner.rs",
16420        ] {
16421            fs::write(repo.join(path), "pub fn navigation_fixture() {}\n")?;
16422        }
16423        let db_path = repo.join(".projectatlas").join("projectatlas.db");
16424        let plan = ScanRuntimePlan::for_path(None, &repo, None)?;
16425        let mut store = open_atlas_store_for_project(&db_path, &plan.root)?;
16426        run_scan_pipeline(
16427            &mut store,
16428            &plan,
16429            &SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, Some(1), Some(30)),
16430        )?;
16431        publish_mcp_navigation_graph(&mut store)?;
16432        drop(store);
16433
16434        let server =
16435            ProjectAtlasMcpServer::new(db_path, None, "mcp-navigation-test".to_string(), false);
16436        let folders_text = server.atlas_folders_response(
16437            AtlasQueryParams {
16438                project_path: None,
16439                worktree: None,
16440                query: Some("navigation".to_string()),
16441                limit: Some(10),
16442            },
16443            None,
16444        );
16445        let files_text = server.atlas_files_response(
16446            AtlasFilesParams {
16447                project_path: None,
16448                worktree: None,
16449                query: Some("navigation".to_string()),
16450                folder: None,
16451                nearest_project: Some(false),
16452                file_pattern: None,
16453                include_content: Some(false),
16454                content_selection: None,
16455                limit: Some(10),
16456            },
16457            None,
16458        );
16459        for (surface, text) in [
16460            ("atlas_folders", &folders_text),
16461            ("atlas_files", &files_text),
16462        ] {
16463            require(
16464                text.contains("connection_counts")
16465                    && text.contains("connections")
16466                    && text.contains("direction:")
16467                    && text.contains("target:")
16468                    && text.contains("connections_truncated: true"),
16469                &format!("{surface} dropped nonempty typed graph evidence: {text}"),
16470            )?;
16471        }
16472
16473        let brief = server.build_session_brief(
16474            AtlasSessionBriefParams {
16475                project_path: None,
16476                worktree: None,
16477                query: Some("navigation".to_string()),
16478                purpose_task: None,
16479                compact: None,
16480                folder_limit: Some(10),
16481                file_limit: Some(10),
16482                blocker_limit: Some(10),
16483                purpose_limit: Some(10),
16484            },
16485            None,
16486        )?;
16487        let folder = brief
16488            .folders
16489            .iter()
16490            .find(|candidate| candidate.path == "src")
16491            .ok_or_else(|| io::Error::other("graph-enriched MCP folder is missing"))?;
16492        require(
16493            folder.connection_counts.len() == 7
16494                && folder.connections.len() == 3
16495                && folder.connections_truncated,
16496            "MCP folder lost count, sample, or global truncation evidence",
16497        )?;
16498        let owner = brief
16499            .files
16500            .iter()
16501            .find(|candidate| candidate.path == "src/navigation_owner.rs")
16502            .ok_or_else(|| io::Error::other("graph-enriched MCP owner file is missing"))?;
16503        require(
16504            owner.connection_counts.len() == 7
16505                && owner.connections.len() == 3
16506                && owner.connections_truncated
16507                && owner.next_call.capability
16508                    == projectatlas_core::NavigationNextCapability::Relations,
16509            "MCP file or session brief lost graph truncation or relations navigation",
16510        )?;
16511        let compact_relations = server.atlas_symbol_relations_response(
16512            &AtlasSymbolRelationsParams {
16513                project_path: None,
16514                file: Some("src/navigation_owner.rs".to_string()),
16515                nearest_project: Some(false),
16516                view: Some("detailed".to_string()),
16517                compact: Some(true),
16518                direction: Some("outbound".to_string()),
16519                include_occurrences: Some(true),
16520                limit: Some(10),
16521                output_bytes: Some(64 * 1_024),
16522                ..AtlasSymbolRelationsParams::default()
16523            },
16524            None,
16525        );
16526        require(
16527            compact_relations.contains("status: resolved")
16528                && compact_relations.contains("status: ambiguous")
16529                && compact_relations.contains("status: external")
16530                && compact_relations.contains("status: unresolved")
16531                && compact_relations.contains("reference: \"navigation-ambiguous\"")
16532                && compact_relations.contains("candidates: 2")
16533                && compact_relations.contains("next_call:"),
16534            &format!(
16535                "compact detailed relations dropped a resolution state or reusable next call: {compact_relations}"
16536            ),
16537        )?;
16538
16539        let compact = server.build_compact_session_brief(
16540            AtlasSessionBriefParams {
16541                project_path: None,
16542                worktree: None,
16543                query: Some("navigation_owner".to_string()),
16544                purpose_task: None,
16545                compact: Some(true),
16546                folder_limit: None,
16547                file_limit: None,
16548                blocker_limit: None,
16549                purpose_limit: None,
16550            },
16551            None,
16552        )?;
16553        let compact_owner = compact
16554            .files
16555            .iter()
16556            .find(|candidate| candidate.path == "src/navigation_owner.rs")
16557            .ok_or_else(|| io::Error::other("compact graph owner file is missing"))?;
16558        require(
16559            compact_owner.connections.len() == 1
16560                && compact_owner.connections.iter().all(|connection| {
16561                    connection.kind != RankedConnectionKind::Import
16562                        && !matches!(
16563                            &connection.target,
16564                            RankedConnectionTarget::Unresolved { .. }
16565                        )
16566                })
16567                && compact_owner.next_call.capability == NavigationNextCapability::Summary
16568                && compact_owner.purpose_agent_reviewed,
16569            "compact session brief did not retain one crisp edge and summary-first routing",
16570        )?;
16571        let expanded_text =
16572            ProjectAtlasMcpServer::encode_named_payload(MCP_PAYLOAD_SESSION_BRIEF, &brief)?;
16573        require(
16574            expanded_text.contains("missing_purposes:")
16575                && expanded_text.contains("stale_purposes:")
16576                && expanded_text.contains("approved_purposes:")
16577                && expanded_text.contains("suggested_purposes:"),
16578            "compatibility session brief lost purpose lifecycle counts",
16579        )?;
16580        let compact_text =
16581            ProjectAtlasMcpServer::encode_named_payload(MCP_PAYLOAD_SESSION_BRIEF, &compact)?;
16582        require(
16583            compact.purpose_handoff.as_ref().is_some_and(|handoff| {
16584                handoff.recommended_subagent_reasoning == PURPOSE_CURATOR_RECOMMENDED_REASONING
16585                    && handoff.instructions.len() == 1
16586                    && handoff.instructions.first()
16587                        == brief
16588                            .purpose_handoff
16589                            .as_ref()
16590                            .and_then(|expanded| expanded.instructions.first())
16591            }) && compact_text.contains(&format!(
16592                "recommended_subagent_reasoning: {PURPOSE_CURATOR_RECOMMENDED_REASONING}"
16593            )) && compact_text
16594                .contains("lowest reliable reasoning and cost tier the host supports"),
16595            &format!(
16596                "compact actionable handoff lost its reliable-tier instruction: {compact_text}"
16597            ),
16598        )?;
16599        require(
16600            compact
16601                .blockers
16602                .as_ref()
16603                .is_some_and(|blockers| blockers.total > 0)
16604                && !compact_text.contains("\n    db:")
16605                && !compact_text.contains("\n    config:")
16606                && !compact_text.contains("\n  policy:")
16607                && !compact_text.contains("\n    items:")
16608                && !compact_text.contains("reason_codes")
16609                && !compact_text.contains("connection_counts")
16610                && !compact_text.contains("agent_harness_expected")
16611                && !compact_text.contains("server_started_curator")
16612                && !compact_text.contains("missing_purposes")
16613                && compact_text.len() <= 4_096,
16614            &format!("compact session brief retained default-only chatter: {compact_text}"),
16615        )?;
16616
16617        let families = brief
16618            .files
16619            .iter()
16620            .flat_map(|candidate| candidate.connection_counts.iter().map(|count| count.kind))
16621            .collect::<BTreeSet<_>>();
16622        require(
16623            families
16624                == BTreeSet::from([
16625                    RankedConnectionKind::Package,
16626                    RankedConnectionKind::Import,
16627                    RankedConnectionKind::Call,
16628                    RankedConnectionKind::Reference,
16629                    RankedConnectionKind::Test,
16630                    RankedConnectionKind::Route,
16631                    RankedConnectionKind::Config,
16632                ]),
16633            &format!("MCP graph families were not propagated: {families:?}"),
16634        )?;
16635        let connections = brief
16636            .files
16637            .iter()
16638            .flat_map(|candidate| candidate.connections.iter())
16639            .collect::<Vec<_>>();
16640        require(
16641            connections
16642                .iter()
16643                .any(|connection| connection.direction == RankedConnectionDirection::Outbound)
16644                && connections
16645                    .iter()
16646                    .any(|connection| connection.direction == RankedConnectionDirection::Inbound),
16647            "MCP graph samples did not preserve both directions",
16648        )?;
16649        for (name, present) in [
16650            (
16651                "local",
16652                connections.iter().any(|connection| {
16653                    matches!(connection.target, RankedConnectionTarget::Local { .. })
16654                }),
16655            ),
16656            (
16657                "package",
16658                connections.iter().any(|connection| {
16659                    matches!(connection.target, RankedConnectionTarget::Package { .. })
16660                }),
16661            ),
16662            (
16663                "external",
16664                connections.iter().any(|connection| {
16665                    matches!(connection.target, RankedConnectionTarget::External { .. })
16666                }),
16667            ),
16668            (
16669                "unresolved",
16670                connections.iter().any(|connection| {
16671                    matches!(connection.target, RankedConnectionTarget::Unresolved { .. })
16672                }),
16673            ),
16674        ] {
16675            require(
16676                present,
16677                &format!("MCP graph samples omitted {name} targets"),
16678            )?;
16679        }
16680        Ok(())
16681    }
16682
16683    fn publish_mcp_navigation_graph(
16684        store: &mut AtlasStore,
16685    ) -> Result<(), Box<dyn std::error::Error>> {
16686        let project = store
16687            .project_instance_id()?
16688            .ok_or_else(|| io::Error::other("MCP navigation project identity is missing"))?;
16689        let current_publication = store
16690            .index_publication()?
16691            .ok_or_else(|| io::Error::other("MCP navigation publication is missing"))?;
16692        let fingerprint = current_publication
16693            .contract_fingerprint
16694            .clone()
16695            .ok_or_else(|| io::Error::other("MCP navigation fingerprint is missing"))?;
16696        let generation = current_publication
16697            .generation
16698            .checked_next()
16699            .ok_or_else(|| io::Error::other("MCP navigation generation overflow"))?;
16700        let file_entity = |path: &str| {
16701            GraphEntity::new(
16702                project,
16703                EntitySelector::File {
16704                    path: RepositoryFilePath::new(Path::new(path))?,
16705                },
16706                generation,
16707            )
16708        };
16709        let owner = file_entity("src/navigation_owner.rs")?;
16710        let local = file_entity("src/navigation_local.rs")?;
16711        let unresolved = file_entity("src/navigation_unresolved.rs")?;
16712        let test = file_entity("tests/navigation_owner.rs")?;
16713        let package = GraphEntity::new(
16714            project,
16715            EntitySelector::Package {
16716                package: PackageSelector {
16717                    manager: GraphIdentityText::new("cargo")?,
16718                    name: GraphIdentityText::new("adapter-navigation")?,
16719                    manifest: RepositoryFilePath::new(Path::new("Cargo.toml"))?,
16720                },
16721            },
16722            generation,
16723        )?;
16724        let external = GraphEntity::new(
16725            project,
16726            EntitySelector::External {
16727                external: ExternalSelector {
16728                    system: GraphIdentityText::new("crates.io")?,
16729                    identity: GraphIdentityText::new("serde@1")?,
16730                },
16731            },
16732            generation,
16733        )?;
16734        let resolved = |source: &GraphEntity, kind, target: &GraphEntity| {
16735            Ok::<_, Box<dyn std::error::Error>>(LogicalRelation::new(
16736                source,
16737                kind,
16738                RelationResolution::resolved(target)?,
16739                ConfidenceClass::Exact,
16740                Completeness::Complete,
16741                generation,
16742            )?)
16743        };
16744        let unresolved_relation = |source: &GraphEntity, kind, reference: &str| {
16745            Ok::<_, Box<dyn std::error::Error>>(LogicalRelation::new(
16746                source,
16747                kind,
16748                RelationResolution::Unresolved {
16749                    reference: GraphIdentityText::new(reference)?,
16750                },
16751                ConfidenceClass::High,
16752                Completeness::Partial,
16753                generation,
16754            )?)
16755        };
16756        let relations = vec![
16757            resolved(
16758                &package,
16759                GraphRelationKind::Legacy(RelationKind::DependsOn),
16760                &owner,
16761            )?,
16762            LogicalRelation::new(
16763                &owner,
16764                GraphRelationKind::Legacy(RelationKind::Imports),
16765                RelationResolution::external(&external)?,
16766                ConfidenceClass::Exact,
16767                Completeness::Complete,
16768                generation,
16769            )?,
16770            resolved(
16771                &owner,
16772                GraphRelationKind::Legacy(RelationKind::Calls),
16773                &local,
16774            )?,
16775            unresolved_relation(
16776                &unresolved,
16777                GraphRelationKind::Extended(ExtendedRelationKind::References),
16778                "navigation-reference",
16779            )?,
16780            resolved(
16781                &test,
16782                GraphRelationKind::Extended(ExtendedRelationKind::Tests),
16783                &owner,
16784            )?,
16785            resolved(
16786                &owner,
16787                GraphRelationKind::Extended(ExtendedRelationKind::RoutesTo),
16788                &local,
16789            )?,
16790            LogicalRelation::new(
16791                &owner,
16792                GraphRelationKind::Extended(ExtendedRelationKind::References),
16793                RelationResolution::Ambiguous {
16794                    reference: GraphIdentityText::new("navigation-ambiguous")?,
16795                    candidates: std::num::NonZeroU32::new(2)
16796                        .ok_or_else(|| io::Error::other("ambiguous fixture count is zero"))?,
16797                },
16798                ConfidenceClass::High,
16799                Completeness::Partial,
16800                generation,
16801            )?,
16802            unresolved_relation(
16803                &owner,
16804                GraphRelationKind::Extended(ExtendedRelationKind::Configures),
16805                "NAVIGATION_MODE",
16806            )?,
16807        ];
16808        let nodes = store
16809            .load_nodes()?
16810            .into_iter()
16811            .map(|node| node.node)
16812            .collect::<Vec<_>>();
16813        {
16814            let mut publication = store.begin_index_publication(&fingerprint)?;
16815            publication.begin_scan_replacement()?;
16816            publication.upsert_scan_node_batch(&nodes)?;
16817            publication.finish_scan_replacement()?;
16818            publication.replace_repository_graph(
16819                project,
16820                &[owner, local, unresolved, test, package, external],
16821                &relations,
16822                &[],
16823                &[],
16824            )?;
16825            publication.complete()?;
16826        }
16827        store.set_purpose("src", "Navigation graph folder", PurposeSource::Agent)?;
16828        store.set_purpose(
16829            "src/navigation_owner.rs",
16830            "Navigation graph owner",
16831            PurposeSource::Agent,
16832        )?;
16833        store.set_purpose(
16834            "src/navigation_unresolved.rs",
16835            "Navigation unresolved graph owner",
16836            PurposeSource::Agent,
16837        )?;
16838        Ok(())
16839    }
16840
16841    #[test]
16842    fn session_brief_exposes_host_owned_purpose_curator_handoff()
16843    -> Result<(), Box<dyn std::error::Error>> {
16844        let temp = tempfile::tempdir()?;
16845        let repo = temp.path().join("repo-a");
16846        fs::create_dir_all(repo.join("src"))?;
16847        fs::write(repo.join("src").join("main.rs"), "fn main() {}\n")?;
16848        let db_path = repo.join(".projectatlas").join("projectatlas.db");
16849        let plan = ScanRuntimePlan::for_path(None, &repo, None)?;
16850        let mut store = open_atlas_store_for_project(&db_path, &plan.root)?;
16851        let symbol_options = SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, Some(1), Some(30));
16852        run_scan_pipeline(&mut store, &plan, &symbol_options)?;
16853        drop(store);
16854
16855        let server = ProjectAtlasMcpServer::new(db_path, None, "mcp-test".to_string(), false);
16856        let brief = server.build_session_brief(
16857            AtlasSessionBriefParams {
16858                project_path: None,
16859                worktree: None,
16860                query: Some("startup".to_string()),
16861                purpose_task: Some("startup-task".to_string()),
16862                compact: None,
16863                folder_limit: Some(5),
16864                file_limit: Some(5),
16865                blocker_limit: Some(5),
16866                purpose_limit: Some(1),
16867            },
16868            None,
16869        )?;
16870        let handoff = brief
16871            .purpose_handoff
16872            .as_ref()
16873            .ok_or_else(|| std::io::Error::other("actionable purpose handoff missing"))?;
16874        require(
16875            handoff.execution_owner == "agent_host",
16876            "session handoff was not host-owned",
16877        )?;
16878        require(
16879            handoff.recommended_subagent_reasoning == PURPOSE_CURATOR_RECOMMENDED_REASONING,
16880            "session handoff did not request the lowest reliable host-supported reasoning",
16881        )?;
16882        require(
16883            handoff.main_agent_fallback && !handoff.server_started_curator,
16884            "session handoff misrepresented curator execution ownership",
16885        )?;
16886        require(
16887            handoff.silent_on_success,
16888            "session handoff was not quiet on successful maintenance",
16889        )?;
16890        require(
16891            handoff.queue.task == "startup-task"
16892                && handoff.queue.curation_scope == "low"
16893                && handoff.queue.actionable
16894                && handoff.queue.returned == 1
16895                && handoff.queue.limit == 1
16896                && handoff.queue.truncated,
16897            "compatibility session handoff lost its bounded purpose queue metadata",
16898        )?;
16899        require(
16900            handoff.queue.items.iter().all(|item| {
16901                item.work_key.len() == 64
16902                    && item.state_token.len() == 64
16903                    && !item.purpose_agent_reviewed
16904            }),
16905            "session handoff item tokens or lifecycle state were incomplete",
16906        )?;
16907        let compact = server.build_compact_session_brief(
16908            AtlasSessionBriefParams {
16909                project_path: None,
16910                worktree: None,
16911                query: Some("startup".to_string()),
16912                purpose_task: Some("startup-task".to_string()),
16913                compact: Some(true),
16914                folder_limit: Some(5),
16915                file_limit: Some(5),
16916                blocker_limit: Some(5),
16917                purpose_limit: Some(1),
16918            },
16919            None,
16920        )?;
16921        let compact_handoff = compact
16922            .purpose_handoff
16923            .as_ref()
16924            .ok_or_else(|| std::io::Error::other("compact purpose handoff missing"))?;
16925        require(
16926            matches!(
16927                compact_handoff.next_call.kind,
16928                McpBriefRecommendationKind::PurposeQueue
16929            ) && compact_handoff.next_call.target == MCP_TOOL_ATLAS_PURPOSE_QUEUE
16930                && compact_handoff.next_call.arguments.get(MCP_BRIEF_ARG_TASK)
16931                    == Some(&serde_json::json!("startup-task"))
16932                && compact_handoff.next_call.arguments.get(MCP_BRIEF_ARG_LIMIT)
16933                    == Some(&serde_json::json!(1)),
16934            "compact handoff did not preserve the exact bounded purpose-queue call",
16935        )?;
16936        require(
16937            brief.limits.purpose_limit == 1 && brief.limits.purposes_truncated,
16938            "session brief purpose limits were not reported",
16939        )?;
16940        Ok(())
16941    }
16942
16943    #[test]
16944    fn settings_capabilities_report_nearest_policy() -> Result<(), Box<dyn std::error::Error>> {
16945        let temp = tempfile::tempdir()?;
16946        let repo = temp.path().join("repo-a");
16947        fs::create_dir(&repo)?;
16948        let db_path = repo.join(".projectatlas").join("projectatlas.db");
16949        let disabled =
16950            ProjectAtlasMcpServer::new(db_path.clone(), None, "mcp-test".to_string(), false);
16951        let enabled = ProjectAtlasMcpServer::new(db_path, None, "mcp-test".to_string(), true);
16952        let disabled_state = disabled.active_project_state()?;
16953        let enabled_state = enabled.active_project_state()?;
16954
16955        let disabled_text = disabled.render_settings_with_capabilities(&disabled_state)?;
16956        require(
16957            disabled_text.contains("mcp_session:"),
16958            "settings did not include mcp_session capabilities",
16959        )?;
16960        require(
16961            disabled_text.contains("path_scope: selected_project"),
16962            "disabled nearest-project policy was not typed",
16963        )?;
16964        require(
16965            disabled_text.contains("language_registry:")
16966                && disabled_text.contains("accepted_set_digest:")
16967                && disabled_text.contains("semantic_provider_digest:")
16968                && disabled_text.contains("semantic_relation_contract_digest:")
16969                && disabled_text.contains("relation_family_inventory:")
16970                && disabled_text.contains("optional_disabled_families:")
16971                && disabled_text.contains("benchmarked:")
16972                && !disabled_text.contains("accepted_minimum:")
16973                && !disabled_text.contains("provenance_source:")
16974                && disabled_text.contains("optional_catalog:")
16975                && disabled_text.contains("database:")
16976                && disabled_text.contains("compile_options:")
16977                && disabled_text.contains("search:")
16978                && disabled_text.contains("default_mode: lexical")
16979                && disabled_text.contains("optional_parser_pack:"),
16980            "settings did not project compact shared language registry truth",
16981        )?;
16982        require(
16983            disabled_text.len() <= MCP_SETTINGS_RESPONSE_MAX_BYTES,
16984            "settings exceeded its agent-facing output bound",
16985        )?;
16986        let enabled_text = enabled.render_settings_with_capabilities(&enabled_state)?;
16987        require(
16988            enabled_text.contains("path_scope: nearest_indexed_project"),
16989            "enabled nearest-project policy was not typed",
16990        )?;
16991        require(
16992            !enabled_text.contains("GITHUB_TOKEN") && !enabled_text.contains("GH_TOKEN"),
16993            "settings capabilities leaked token environment names",
16994        )?;
16995
16996        Ok(())
16997    }
16998
16999    #[cfg(windows)]
17000    #[test]
17001    fn mcp_settings_reports_native_root_equivalence() -> Result<(), Box<dyn std::error::Error>> {
17002        let temp = tempfile::tempdir()?;
17003        let base = temp
17004            .path()
17005            .to_str()
17006            .ok_or("temporary directory was not UTF-8")?;
17007        let long_component = "a".repeat(220);
17008        let verbatim_root = PathBuf::from(format!(r"\\?\{base}\{long_component}"));
17009        fs::create_dir(&verbatim_root)?;
17010        let verbatim_database = verbatim_root.join(".projectatlas/projectatlas.db");
17011        fs::create_dir_all(
17012            verbatim_database
17013                .parent()
17014                .ok_or("verbatim MCP database has no parent")?,
17015        )?;
17016        drop(AtlasStore::open_for_project(
17017            &verbatim_database,
17018            &verbatim_root,
17019        )?);
17020        let verbatim_config = temp.path().join("mcp-verbatim-config.toml");
17021        let verbatim_text = verbatim_root
17022            .to_str()
17023            .ok_or("verbatim MCP root was not UTF-8")?;
17024        fs::write(
17025            &verbatim_config,
17026            format!(
17027                "[project]\nroot = {}\n",
17028                serde_json::to_string(verbatim_text)?
17029            ),
17030        )?;
17031        let verbatim_expected =
17032            CanonicalProjectRoot::from_path(&verbatim_root)?.display_string()?;
17033        let verbatim_server = ProjectAtlasMcpServer::new(
17034            verbatim_database.clone(),
17035            None,
17036            "mcp-verbatim-settings-test".to_string(),
17037            false,
17038        );
17039        let verbatim_state = McpProjectState {
17040            root: verbatim_root,
17041            db_path: verbatim_database,
17042            config_path: Some(verbatim_config),
17043            worktree: None,
17044        };
17045        let verbatim: serde_json::Value = toon_format::decode_default(
17046            &verbatim_server.render_settings_with_capabilities(&verbatim_state)?,
17047        )?;
17048        require(
17049            verbatim.pointer("/settings/repo_root") == Some(&serde_json::json!(verbatim_expected)),
17050            "MCP settings lost the verbatim native root projection",
17051        )?;
17052
17053        let original = temp.path().join("McpCaseRoot");
17054        let staging = temp.path().join("McpCaseRootStaging");
17055        let renamed = temp.path().join("mcpcaseroot");
17056        fs::create_dir(&original)?;
17057        let database = original.join(".projectatlas/projectatlas.db");
17058        fs::create_dir_all(
17059            database
17060                .parent()
17061                .ok_or_else(|| io::Error::other("MCP case-only database has no parent"))?,
17062        )?;
17063        drop(AtlasStore::open_for_project(&database, &original)?);
17064        fs::rename(&original, &staging)?;
17065        fs::rename(&staging, &renamed)?;
17066        let renamed_database = renamed.join(".projectatlas/projectatlas.db");
17067        let positive_config = temp.path().join("mcp-case-only-config.toml");
17068        fs::write(
17069            &positive_config,
17070            format!(
17071                "[project]\nroot = {}\n",
17072                serde_json::to_string(&renamed.to_string_lossy())?
17073            ),
17074        )?;
17075        let positive_server = ProjectAtlasMcpServer::new(
17076            renamed_database.clone(),
17077            None,
17078            "mcp-settings-test".to_string(),
17079            false,
17080        );
17081        let positive_state = McpProjectState {
17082            root: renamed,
17083            db_path: renamed_database,
17084            config_path: Some(positive_config),
17085            worktree: None,
17086        };
17087        let positive: serde_json::Value = toon_format::decode_default(
17088            &positive_server.render_settings_with_capabilities(&positive_state)?,
17089        )?;
17090        require(
17091            positive.pointer("/settings/root_verified") == Some(&serde_json::json!(true)),
17092            "MCP settings rejected case-only root rename",
17093        )?;
17094
17095        let case_sensitive_parent = temp.path().join("mcp-case-sensitive-parent");
17096        fs::create_dir(&case_sensitive_parent)?;
17097        let enabled = StdCommand::new("fsutil")
17098            .args(["file", "SetCaseSensitiveInfo"])
17099            .arg(&case_sensitive_parent)
17100            .arg("enable")
17101            .status()
17102            .is_ok_and(|status| status.success());
17103        if !enabled {
17104            return Ok(());
17105        }
17106        let stored_root = case_sensitive_parent.join("Repo");
17107        let selected_root = case_sensitive_parent.join("repo");
17108        fs::create_dir(&stored_root)?;
17109        fs::create_dir(&selected_root)?;
17110        let database = stored_root.join(".projectatlas/projectatlas.db");
17111        fs::create_dir_all(
17112            database
17113                .parent()
17114                .ok_or_else(|| io::Error::other("MCP case-sensitive database has no parent"))?,
17115        )?;
17116        drop(AtlasStore::open_for_project(&database, &stored_root)?);
17117        let negative_config = temp.path().join("mcp-case-sensitive-config.toml");
17118        fs::write(
17119            &negative_config,
17120            format!(
17121                "[project]\nroot = {}\n",
17122                serde_json::to_string(&selected_root.to_string_lossy())?
17123            ),
17124        )?;
17125        let negative_server = ProjectAtlasMcpServer::new(
17126            database.clone(),
17127            None,
17128            "mcp-settings-test".to_string(),
17129            false,
17130        );
17131        let negative_state = McpProjectState {
17132            root: selected_root,
17133            db_path: database,
17134            config_path: Some(negative_config),
17135            worktree: None,
17136        };
17137        let negative: serde_json::Value = toon_format::decode_default(
17138            &negative_server.render_settings_with_capabilities(&negative_state)?,
17139        )?;
17140        require(
17141            negative.pointer("/settings/root_verified") == Some(&serde_json::json!(false)),
17142            "MCP settings accepted a case-sensitive sibling",
17143        )?;
17144        Ok(())
17145    }
17146
17147    #[test]
17148    fn task_progress_status_and_cancel_are_typed() -> Result<(), Box<dyn std::error::Error>> {
17149        let temp = tempfile::tempdir()?;
17150        let repo = temp.path().join("repo-a");
17151        fs::create_dir(&repo)?;
17152        let db_path = repo.join(".projectatlas").join("projectatlas.db");
17153        let server = ProjectAtlasMcpServer::new(db_path, None, "mcp-test".to_string(), false);
17154
17155        let status = server.task_status(MCP_TASK_CONTRACT_ID.to_string())?;
17156        require(
17157            status.lookup == McpTaskLookupStatus::Found,
17158            "contract task missing",
17159        )?;
17160        require(
17161            status
17162                .task
17163                .as_ref()
17164                .is_some_and(|task| task.state == McpTaskState::Complete),
17165            "contract task was not complete",
17166        )?;
17167        require(
17168            status.states.contains(&McpTaskState::Pending)
17169                && status.states.contains(&McpTaskState::Canceled),
17170            "status response did not expose task state contract",
17171        )?;
17172        let cancel = server.task_cancel(MCP_TASK_CONTRACT_ID.to_string())?;
17173        require(
17174            cancel.result == McpTaskCancelResult::AlreadyFinished,
17175            "completed contract task did not return already_finished",
17176        )?;
17177        let missing = server.task_status("missing-task".to_string())?;
17178        require(
17179            missing.lookup == McpTaskLookupStatus::NotFound,
17180            "unknown task did not return not_found",
17181        )?;
17182
17183        Ok(())
17184    }
17185
17186    #[test]
17187    fn background_task_envelope_and_cancellation_reach_owned_control()
17188    -> Result<(), Box<dyn std::error::Error>> {
17189        let temp = tempfile::tempdir()?;
17190        let repo = temp.path().join("repo-a");
17191        fs::create_dir(&repo)?;
17192        let db_path = repo.join(".projectatlas").join("projectatlas.db");
17193        let mut server = ProjectAtlasMcpServer::new(db_path, None, "mcp-test".to_string(), false);
17194        let host_envelope = server.background_resources;
17195        let host_workers = thread::available_parallelism().map_or(1, usize::from);
17196        let representative_envelope = McpBackgroundResourceEnvelope::from_available_workers(8);
17197        require(
17198            host_envelope.task_limit <= MCP_BACKGROUND_TASK_SAFE_CEILING
17199                && host_envelope.workers_per_task > 0
17200                && host_envelope.total_worker_limit
17201                    <= host_workers.clamp(1, INDEX_WORKER_SAFE_CEILING)
17202                && host_envelope.workers_per_task * host_envelope.task_limit
17203                    <= host_envelope.total_worker_limit,
17204            "background resource envelope exceeded its host or process worker budget",
17205        )?;
17206        require(
17207            representative_envelope
17208                == (McpBackgroundResourceEnvelope {
17209                    task_limit: 4,
17210                    workers_per_task: 2,
17211                    total_worker_limit: 8,
17212                })
17213                && McpBackgroundResourceEnvelope::from_available_workers(0).total_worker_limit == 1
17214                && McpBackgroundResourceEnvelope::from_available_workers(usize::MAX)
17215                    .total_worker_limit
17216                    == INDEX_WORKER_SAFE_CEILING,
17217            "background resource envelope did not partition representative host capacities",
17218        )?;
17219
17220        server.background_resources = McpBackgroundResourceEnvelope::from_available_workers(4);
17221        let envelope = server.background_resources;
17222        let started = Arc::new(std::sync::Barrier::new(envelope.task_limit + 1));
17223        let databases_ready = Arc::new(std::sync::Barrier::new(envelope.task_limit + 1));
17224        let release = Arc::new(std::sync::Barrier::new(envelope.task_limit + 1));
17225        let observed_option_workers = Arc::new(AtomicU64::new(0));
17226        let observed_control_workers = Arc::new(AtomicU64::new(0));
17227        let mut concurrent_tasks = Vec::new();
17228        for task_index in 0..envelope.task_limit {
17229            let isolated_root = temp.path().join(format!("repo-{task_index}"));
17230            let isolated_db = isolated_root.join(".projectatlas").join("projectatlas.db");
17231            fs::create_dir_all(isolated_root.join(".projectatlas"))?;
17232            let worker_started = Arc::clone(&started);
17233            let worker_databases_ready = Arc::clone(&databases_ready);
17234            let worker_release = Arc::clone(&release);
17235            let worker_option_workers = Arc::clone(&observed_option_workers);
17236            let worker_control_workers = Arc::clone(&observed_control_workers);
17237            concurrent_tasks.push(server.start_index_task(
17238                McpTaskOperation::Scan,
17239                SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, None, None),
17240                MCP_TOOL_ATLAS_OVERVIEW,
17241                move |control, options| {
17242                    worker_option_workers
17243                        .fetch_add(options.reported_workers() as u64, Ordering::Relaxed);
17244                    worker_control_workers.fetch_add(
17245                        control.worker_ceiling().unwrap_or_default() as u64,
17246                        Ordering::Relaxed,
17247                    );
17248                    worker_started.wait();
17249                    let store = open_atlas_store_for_project(&isolated_db, &isolated_root);
17250                    worker_databases_ready.wait();
17251                    worker_release.wait();
17252                    let _store = store?;
17253                    Ok(())
17254                },
17255            )?);
17256        }
17257        started.wait();
17258        let admitted_workers = envelope.workers_per_task * envelope.task_limit;
17259        require(
17260            observed_option_workers.load(Ordering::Relaxed) == admitted_workers as u64
17261                && observed_control_workers.load(Ordering::Relaxed) == admitted_workers as u64
17262                && admitted_workers <= envelope.total_worker_limit,
17263            "concurrent background tasks did not share the aggregate worker envelope",
17264        )?;
17265        let overflow = server.start_index_task(
17266            McpTaskOperation::Scan,
17267            SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, None, None),
17268            MCP_TOOL_ATLAS_OVERVIEW,
17269            |_control, _options| Ok(()),
17270        );
17271        require(
17272            matches!(overflow, Err(CliError::Mcp(message)) if message.starts_with(MCP_INDEX_TASK_LIMIT_PREFIX)),
17273            "background task admission exceeded the server task limit",
17274        )?;
17275        require(
17276            server.task_status(MCP_TASK_CONTRACT_ID.to_string())?.lookup
17277                == McpTaskLookupStatus::Found,
17278            "task status became unresponsive while concurrent work was active",
17279        )?;
17280        databases_ready.wait();
17281        release.wait();
17282        for task in concurrent_tasks {
17283            require(
17284                wait_for_background_task(&server, &task.task_id)?.state == McpTaskState::Complete,
17285                "concurrent background task did not finish successfully",
17286            )?;
17287        }
17288
17289        let cancel_started = Arc::new(std::sync::Barrier::new(2));
17290        let worker_cancel_started = Arc::clone(&cancel_started);
17291        let canceled_task = server.start_index_task(
17292            McpTaskOperation::Scan,
17293            SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, None, None),
17294            MCP_TOOL_ATLAS_OVERVIEW,
17295            move |control, options| {
17296                if options.reported_workers() != control.worker_ceiling().unwrap_or_default() {
17297                    return Err(CliError::Mcp(
17298                        "background parser and operation worker ceilings diverged".to_string(),
17299                    ));
17300                }
17301                worker_cancel_started.wait();
17302                loop {
17303                    control
17304                        .check(projectatlas_core::IndexWorkStage::RepositoryTraversal)
17305                        .map_err(|failure| {
17306                            CliError::Fs(projectatlas_fs::FsError::IndexWork(failure))
17307                        })?;
17308                    thread::yield_now();
17309                }
17310            },
17311        )?;
17312        cancel_started.wait();
17313
17314        let running = server.task_status(canceled_task.task_id.clone())?;
17315        require(
17316            running
17317                .task
17318                .as_ref()
17319                .is_some_and(|record| record.state == McpTaskState::Running),
17320            "background task did not become running",
17321        )?;
17322        let cancel = server.task_cancel(canceled_task.task_id.clone())?;
17323        require(
17324            cancel.result == McpTaskCancelResult::CancellationRequested,
17325            "task cancellation did not reach the active work control",
17326        )?;
17327
17328        require(
17329            wait_for_background_task(&server, &canceled_task.task_id)?.state
17330                == McpTaskState::Canceled,
17331            "background task did not finish canceled after consuming the signal",
17332        )?;
17333        require(
17334            run_successful_background_task(&server)?.state == McpTaskState::Complete,
17335            "successful task was not admitted after cancellation",
17336        )?;
17337
17338        let failed_task = server.start_index_task(
17339            McpTaskOperation::Scan,
17340            SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, None, None),
17341            MCP_TOOL_ATLAS_OVERVIEW,
17342            |_control, _options| Err(CliError::Mcp("expected task failure".to_string())),
17343        )?;
17344        require(
17345            wait_for_background_task(&server, &failed_task.task_id)?.state == McpTaskState::Failed,
17346            "background task error did not become a terminal failure",
17347        )?;
17348        require(
17349            run_successful_background_task(&server)?.state == McpTaskState::Complete,
17350            "successful task was not admitted after failure",
17351        )?;
17352
17353        let panicked_task = server.start_index_task(
17354            McpTaskOperation::Scan,
17355            SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, None, None),
17356            MCP_TOOL_ATLAS_OVERVIEW,
17357            |_control, _options| -> Result<(), CliError> {
17358                std::panic::resume_unwind(Box::new("expected background task panic"));
17359            },
17360        )?;
17361        let panicked = wait_for_background_task(&server, &panicked_task.task_id)?;
17362        require(
17363            panicked.state == McpTaskState::Failed
17364                && panicked.error.as_deref() == Some(MCP_INDEX_WORKER_PANIC_ERROR),
17365            "background task panic did not remain bounded and terminal",
17366        )?;
17367        require(
17368            run_successful_background_task(&server)?.state == McpTaskState::Complete,
17369            "terminal task lifecycle did not release background admission capacity",
17370        )?;
17371
17372        Ok(())
17373    }
17374
17375    #[test]
17376    fn background_scan_defers_config_validation_and_rejects_root_redirection()
17377    -> Result<(), Box<dyn std::error::Error>> {
17378        let temp = tempfile::tempdir()?;
17379        let repo = temp.path().join("repo-a");
17380        let redirected = temp.path().join("repo-b");
17381        let atlas_dir = repo.join(".projectatlas");
17382        fs::create_dir_all(&atlas_dir)?;
17383        fs::create_dir_all(&redirected)?;
17384        let config_path = atlas_dir.join("config.toml");
17385        fs::write(&config_path, "[project]\nroot = \"../../repo-b\"\n")?;
17386        let db_path = atlas_dir.join("projectatlas.db");
17387        let server =
17388            ProjectAtlasMcpServer::new(db_path, Some(config_path), "mcp-test".to_string(), false);
17389
17390        let response = server.atlas_scan(Parameters(AtlasScanParams {
17391            project_path: Some(repo.to_string_lossy().into_owned()),
17392            worktree: None,
17393            path: None,
17394            nearest_project: Some(false),
17395            max_bytes: None,
17396            max_workers: Some(1),
17397            timeout_seconds: None,
17398            text_index_max_bytes: None,
17399            background: Some(true),
17400        }));
17401        require(
17402            response.contains(MCP_PAYLOAD_TASK_START),
17403            "explicit background project validated redirecting config before task admission",
17404        )?;
17405        let admitted_task_id = server
17406            .task_registry
17407            .read()
17408            .map_err(|_poisoned| io::Error::other("task registry lock poisoned"))?
17409            .latest_task_id(&McpTaskOperation::Scan)
17410            .ok_or_else(|| io::Error::other("admitted background scan task missing"))?;
17411        let mut admitted_terminal = None;
17412        for _attempt in 0..1_000 {
17413            let status = server.task_status(admitted_task_id.clone())?;
17414            if status
17415                .task
17416                .as_ref()
17417                .is_some_and(McpTaskRecord::is_terminal_state)
17418            {
17419                admitted_terminal = status.task;
17420                break;
17421            }
17422            thread::sleep(Duration::from_millis(1));
17423        }
17424        require(
17425            admitted_terminal
17426                .as_ref()
17427                .is_some_and(|record| record.state == McpTaskState::Failed),
17428            "redirecting background config did not fail inside the admitted task",
17429        )?;
17430        require(
17431            admitted_terminal
17432                .as_ref()
17433                .and_then(|record| record.error.as_deref())
17434                .is_some_and(|error| error.contains("outside selected project root")),
17435            "controlled plan loading did not preserve root-redirection refusal",
17436        )?;
17437
17438        Ok(())
17439    }
17440
17441    #[test]
17442    fn watch_once_requires_initialized_state_without_creating_a_database()
17443    -> Result<(), Box<dyn std::error::Error>> {
17444        let temp = tempfile::tempdir()?;
17445        let root = temp.path();
17446        fs::write(root.join("lib.rs"), "pub fn source() {}\n")?;
17447        let database = root.join(".projectatlas/projectatlas.db");
17448        let server = ProjectAtlasMcpServer::new(
17449            database.clone(),
17450            None,
17451            "missing-watch-test".to_string(),
17452            false,
17453        );
17454        let response = server.atlas_watch_once(Parameters(AtlasWatchOnceParams {
17455            project_path: Some(root.to_string_lossy().into_owned()),
17456            worktree: None,
17457            path: None,
17458            nearest_project: Some(false),
17459            max_workers: Some(1),
17460            timeout_seconds: None,
17461            text_index_max_bytes: None,
17462            background: Some(false),
17463        }));
17464        require(
17465            response.contains("init_required"),
17466            "missing index did not return typed initialization guidance",
17467        )?;
17468        require(
17469            !database.exists(),
17470            "watch implicitly created the missing database",
17471        )?;
17472        fs::create_dir(root.join(".projectatlas"))?;
17473        fs::write(&database, [])?;
17474        let response = server.atlas_watch_once(Parameters(AtlasWatchOnceParams {
17475            project_path: Some(root.to_string_lossy().into_owned()),
17476            worktree: None,
17477            path: None,
17478            nearest_project: Some(false),
17479            max_workers: Some(1),
17480            timeout_seconds: None,
17481            text_index_max_bytes: None,
17482            background: Some(false),
17483        }));
17484        require(
17485            response.contains("init_required"),
17486            "empty database did not return typed initialization guidance",
17487        )?;
17488        require(
17489            fs::read(&database)?.is_empty()
17490                && fs::read_dir(root.join(".projectatlas"))?.count() == 1,
17491            "watch initialized or journaled an empty database",
17492        )?;
17493        Ok(())
17494    }
17495
17496    #[test]
17497    fn watch_adapters_reconcile_noop_and_report_real_changed_work()
17498    -> Result<(), Box<dyn std::error::Error>> {
17499        let temp = tempfile::tempdir()?;
17500        let root = temp.path();
17501        fs::write(root.join("lib.rs"), "pub fn initial() {}\n")?;
17502        let config = root.join(".projectatlas/config.toml");
17503        init_project_with_config(root, Some(&config))?;
17504        let database = root.join(".projectatlas/projectatlas.db");
17505        let plan = ScanRuntimePlan::for_path(None, root, None)?;
17506        let options = SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, Some(1), None);
17507        let mut writer = open_atlas_store_for_project(&database, root)?;
17508        run_scan_pipeline(&mut writer, &plan, &options)?;
17509        let initial = writer.index_publication()?;
17510        let server = ProjectAtlasMcpServer::new(
17511            database.clone(),
17512            None,
17513            "watch-epoch-test".to_string(),
17514            false,
17515        );
17516        let watch = |background| {
17517            server.atlas_watch_once(Parameters(AtlasWatchOnceParams {
17518                project_path: Some(root.to_string_lossy().into_owned()),
17519                worktree: None,
17520                path: None,
17521                nearest_project: Some(false),
17522                max_workers: Some(1),
17523                timeout_seconds: None,
17524                text_index_max_bytes: None,
17525                background: Some(background),
17526            }))
17527        };
17528        for _ in 0..2 {
17529            let result: serde_json::Value = toon_format::decode_default(&watch(false))?;
17530            require(
17531                result
17532                    .pointer("/watch/last_symbols/parsed")
17533                    .and_then(serde_json::Value::as_u64)
17534                    == Some(0),
17535                "unchanged watch reported parser work",
17536            )?;
17537            let epoch = server
17538                .source_observations
17539                .with_verified_current_read(
17540                    &database,
17541                    root,
17542                    Some(&config),
17543                    &index_work_control(&options),
17544                    |_store, _stamp| Ok(()),
17545                )?
17546                .require_current()?;
17547            require(
17548                epoch.work.exact_verifications == 1
17549                    && epoch.work.filesystem_entries > 0
17550                    && epoch.work.filesystem_bytes > 0
17551                    && epoch.work.decoded_nodes > 0,
17552                "watch adapter trusted an unfenced callback queue",
17553            )?;
17554        }
17555        require(
17556            writer.index_publication()? == initial,
17557            "unchanged watch published a generation",
17558        )?;
17559        require(
17560            watch(true).contains(MCP_PAYLOAD_TASK_START),
17561            "background no-op was not admitted",
17562        )?;
17563        let terminal = wait_for_background_operation(&server, &McpTaskOperation::WatchOnce)?;
17564        require(
17565            terminal.state == McpTaskState::Complete,
17566            "background no-op failed",
17567        )?;
17568        require(
17569            writer.index_publication()? == initial,
17570            "background no-op published",
17571        )?;
17572        fs::write(root.join("lib.rs"), "pub fn changed() {}\n")?;
17573        // The existing server must detect this immediate write even before callback delivery.
17574        let changed: serde_json::Value = toon_format::decode_default(&server.atlas_watch_once(
17575            Parameters(AtlasWatchOnceParams {
17576                project_path: Some(root.to_string_lossy().into_owned()),
17577                worktree: None,
17578                path: None,
17579                nearest_project: Some(false),
17580                max_workers: Some(1),
17581                timeout_seconds: None,
17582                text_index_max_bytes: None,
17583                background: Some(false),
17584            }),
17585        ))?;
17586        require(
17587            changed
17588                .pointer("/watch/last_symbols/parsed")
17589                .and_then(serde_json::Value::as_u64)
17590                == Some(1),
17591            "source repair was hidden behind a no-op report",
17592        )?;
17593        require(
17594            writer
17595                .load_file_text("lib.rs")?
17596                .is_some_and(|text| text.content == "pub fn changed() {}\n"),
17597            "watch did not publish changed text",
17598        )?;
17599        let before = initial
17600            .ok_or_else(|| io::Error::other("initial generation missing"))?
17601            .generation;
17602        require(
17603            writer
17604                .index_publication()?
17605                .is_some_and(|publication| Some(publication.generation) == before.checked_next()),
17606            "changed watch did not publish exactly one generation",
17607        )?;
17608        let identity = writer.project_instance_id()?;
17609        drop(writer);
17610        let database = root.join(".projectatlas/projectatlas.db");
17611        let predecessor = rusqlite::Connection::open(&database)?;
17612        drop_native_worktree_identity_schema(&predecessor)?;
17613        predecessor.execute_batch(
17614            "DROP TABLE graph_identity_rejections;
17615             UPDATE metadata SET value = '20' WHERE key = 'schema_version';",
17616        )?;
17617        drop(predecessor);
17618        let cold = ProjectAtlasMcpServer::new(
17619            database.clone(),
17620            None,
17621            "migration-watch-test".to_string(),
17622            false,
17623        );
17624        let migrated: serde_json::Value = toon_format::decode_default(&cold.atlas_watch_once(
17625            Parameters(AtlasWatchOnceParams {
17626                project_path: Some(root.to_string_lossy().into_owned()),
17627                worktree: None,
17628                path: None,
17629                nearest_project: Some(false),
17630                max_workers: Some(1),
17631                timeout_seconds: None,
17632                text_index_max_bytes: None,
17633                background: Some(false),
17634            }),
17635        ))?;
17636        require(
17637            migrated.pointer("/watch/last_symbols").is_some(),
17638            &format!("no-op admission prevented supported migration: {migrated}"),
17639        )?;
17640        require(
17641            open_existing_atlas_store_for_project(&database, root)?.project_instance_id()?
17642                == identity,
17643            "supported migration changed project instance identity",
17644        )?;
17645        Ok(())
17646    }
17647
17648    #[test]
17649    fn index_adapters_publish_scan_symbol_and_watch_effects()
17650    -> Result<(), Box<dyn std::error::Error>> {
17651        let temp = tempfile::tempdir()?;
17652        let repo = temp.path().join("repo");
17653        let source_dir = repo.join("src");
17654        fs::create_dir_all(&source_dir)?;
17655        let source_path = source_dir.join("lib.rs");
17656        fs::write(
17657            &source_path,
17658            "pub fn first() { second(); }\nfn second() {}\n",
17659        )?;
17660        let config_path = repo.join(".projectatlas").join("config.toml");
17661        init_project_with_config(&repo, Some(&config_path))?;
17662        let db_path = repo.join(".projectatlas").join("projectatlas.db");
17663        let server =
17664            ProjectAtlasMcpServer::new(db_path.clone(), None, "mcp-test".to_string(), false);
17665        let project_path = repo.to_string_lossy().into_owned();
17666
17667        for operation in [
17668            McpTaskOperation::Scan,
17669            McpTaskOperation::SymbolsBuild,
17670            McpTaskOperation::WatchOnce,
17671        ] {
17672            match operation {
17673                McpTaskOperation::SymbolsBuild => {
17674                    let store = open_atlas_store_for_project(&db_path, &repo)?;
17675                    store.clear_symbol_graph_for_path("src/lib.rs")?;
17676                    require(
17677                        store.symbol_count_for_path("src/lib.rs")? == 0,
17678                        "symbol fixture was not cleared before background rebuild",
17679                    )?;
17680                }
17681                McpTaskOperation::WatchOnce => {
17682                    fs::write(
17683                        &source_path,
17684                        "pub fn first() { second(); third(); }\nfn second() {}\nfn third() {}\n",
17685                    )?;
17686                }
17687                McpTaskOperation::Scan | McpTaskOperation::Contract | McpTaskOperation::Search => {}
17688            }
17689
17690            let response = match operation {
17691                McpTaskOperation::Scan | McpTaskOperation::SymbolsBuild => {
17692                    let params = AtlasScanParams {
17693                        project_path: Some(project_path.clone()),
17694                        worktree: None,
17695                        path: None,
17696                        nearest_project: Some(false),
17697                        max_bytes: None,
17698                        max_workers: Some(1),
17699                        timeout_seconds: None,
17700                        text_index_max_bytes: None,
17701                        background: Some(true),
17702                    };
17703                    if operation == McpTaskOperation::Scan {
17704                        server.atlas_scan(Parameters(params))
17705                    } else {
17706                        server.atlas_symbols_build(Parameters(params))
17707                    }
17708                }
17709                McpTaskOperation::WatchOnce => {
17710                    server.atlas_watch_once(Parameters(AtlasWatchOnceParams {
17711                        project_path: Some(project_path.clone()),
17712                        worktree: None,
17713                        path: None,
17714                        nearest_project: Some(false),
17715                        max_workers: Some(1),
17716                        timeout_seconds: None,
17717                        text_index_max_bytes: None,
17718                        background: Some(true),
17719                    }))
17720                }
17721                McpTaskOperation::Contract | McpTaskOperation::Search => unreachable!(),
17722            };
17723            require(
17724                response.contains(MCP_PAYLOAD_TASK_START),
17725                "production background adapter did not admit its task",
17726            )?;
17727            let terminal = wait_for_background_operation(&server, &operation)?;
17728            require(
17729                terminal.state == McpTaskState::Complete,
17730                terminal
17731                    .error
17732                    .as_deref()
17733                    .unwrap_or("background task failed"),
17734            )?;
17735
17736            let expected_symbol = match operation {
17737                McpTaskOperation::Scan | McpTaskOperation::SymbolsBuild => "second",
17738                McpTaskOperation::WatchOnce => "third",
17739                McpTaskOperation::Contract | McpTaskOperation::Search => unreachable!(),
17740            };
17741            let store = open_atlas_store_for_project(&db_path, &repo)?;
17742            store.set_purpose(
17743                "src/lib.rs",
17744                "Own café λ relation navigation",
17745                PurposeSource::Agent,
17746            )?;
17747            drop(store);
17748            require_agent_index_reads(&server, &project_path, expected_symbol)?;
17749        }
17750
17751        let store = open_atlas_store_for_project(&db_path, &repo)?;
17752        store.clear_symbol_graph_for_path("src/lib.rs")?;
17753        drop(store);
17754        let synchronous_symbols = server.atlas_symbols_build(Parameters(AtlasScanParams {
17755            project_path: Some(project_path.clone()),
17756            worktree: None,
17757            path: None,
17758            nearest_project: Some(false),
17759            max_bytes: None,
17760            max_workers: Some(1),
17761            timeout_seconds: None,
17762            text_index_max_bytes: None,
17763            background: Some(false),
17764        }));
17765        require(
17766            synchronous_symbols.contains(MCP_PAYLOAD_SYMBOLS_BUILD),
17767            "synchronous symbol adapter did not return its completed report",
17768        )?;
17769        require_agent_index_reads(&server, &project_path, "second")?;
17770
17771        fs::write(
17772            &source_path,
17773            "pub fn first() { second(); fourth(); }\nfn second() {}\nfn fourth() {}\n",
17774        )?;
17775        let synchronous_watch = server.atlas_watch_once(Parameters(AtlasWatchOnceParams {
17776            project_path: Some(project_path.clone()),
17777            worktree: None,
17778            path: None,
17779            nearest_project: Some(false),
17780            max_workers: Some(1),
17781            timeout_seconds: None,
17782            text_index_max_bytes: None,
17783            background: Some(false),
17784        }));
17785        require(
17786            synchronous_watch.contains(MCP_PAYLOAD_WATCH),
17787            "synchronous watch adapter did not return its completed report",
17788        )?;
17789        require_agent_index_reads(&server, &project_path, "fourth")?;
17790        Ok(())
17791    }
17792
17793    #[test]
17794    fn long_lived_mcp_query_families_reuse_one_verified_epoch()
17795    -> Result<(), Box<dyn std::error::Error>> {
17796        const LARGE_UNRELATED_SOURCE_FILES: usize = 256;
17797        const MAX_MEASURED_QUERY_OUTPUT_BYTES: usize = 64 * 1_024;
17798        const MAX_MEASURED_QUERY_ELAPSED: Duration = Duration::from_secs(30);
17799
17800        let measure = |unrelated_source_files: usize| {
17801            let temp = tempfile::tempdir()?;
17802            let repo = temp.path().join("repo");
17803            let source_dir = repo.join("src");
17804            fs::create_dir_all(&source_dir)?;
17805            fs::write(
17806                source_dir.join("lib.rs"),
17807                "pub fn first() { second(); }\nfn second() {}\n",
17808            )?;
17809            for index in 0..unrelated_source_files {
17810                fs::write(
17811                    source_dir.join(format!("unrelated_{index:03}.rs")),
17812                    format!("pub fn unrelated_{index:03}() {{}}\n"),
17813                )?;
17814            }
17815            let config_path = repo.join(".projectatlas").join("config.toml");
17816            init_project_with_config(&repo, Some(&config_path))?;
17817            let db_path = repo.join(".projectatlas").join("projectatlas.db");
17818            let mut writer = open_atlas_store_for_project(&db_path, &repo)?;
17819            let plan = ScanRuntimePlan::for_path(None, &repo, None)?;
17820            run_scan_pipeline(
17821                &mut writer,
17822                &plan,
17823                &SymbolBuildOptions::new(MAX_SYMBOL_FILE_BYTES, Some(1), None),
17824            )?;
17825            drop(writer);
17826
17827            let server = ProjectAtlasMcpServer::new(
17828                db_path.clone(),
17829                None,
17830                "verified-epoch-test".to_string(),
17831                false,
17832            );
17833            let state = McpProjectState {
17834                root: repo,
17835                db_path,
17836                config_path: None,
17837                worktree: None,
17838            };
17839            let first = server.with_fresh_store(&state, |store, _stamp| Ok(store.overview()?))?;
17840            require(
17841                first.work.exact_verifications >= 1,
17842                "first long-lived MCP read did not establish exact source truth",
17843            )?;
17844            require(
17845                first.work.filesystem_entries > u64::try_from(unrelated_source_files)?,
17846                "scale fixture did not exercise its complete repository source set",
17847            )?;
17848            let expected_stamp = first.stamp;
17849
17850            let folder = server.with_fresh_store(&state, |store, _stamp| {
17851                Ok(render_ranked_nodes(
17852                    NODE_LABEL_FOLDERS,
17853                    &ranked_folder_nodes_with_reasons(store, "", 4)?,
17854                ))
17855            })?;
17856            let folder_output_bytes = folder.value.len();
17857            let folder = folder.with_output_bytes(folder_output_bytes);
17858            let files = server.with_fresh_store(&state, |store, _stamp| {
17859                Ok(render_ranked_nodes(
17860                    NODE_LABEL_FILES,
17861                    &ranked_file_nodes_with_reasons(store, "", Some("src"), None, 4, false)?,
17862                ))
17863            })?;
17864            let files_output_bytes = files.value.len();
17865            let files = files.with_output_bytes(files_output_bytes);
17866            let summary = server.with_fresh_store(&state, |store, _stamp| {
17867                let content = read_indexed_file_content(store, "src/lib.rs")?;
17868                let report = build_file_summary_from_source(
17869                    store,
17870                    Path::new("src/lib.rs"),
17871                    DEFAULT_FILE_SUMMARY_LIMIT,
17872                    &content,
17873                )?;
17874                Ok(render_file_summary(&report))
17875            })?;
17876            let summary_output_bytes = summary.value.len();
17877            let summary = summary.with_output_bytes(summary_output_bytes);
17878            let relations = server.with_fresh_store(&state, |store, _stamp| {
17879                Ok(render_symbol_relations(&store.load_symbol_relations(
17880                    Some("src/lib.rs"),
17881                    None,
17882                    8,
17883                )?))
17884            })?;
17885            let relation_output_bytes = relations.value.len();
17886            let relations = relations.with_output_bytes(relation_output_bytes);
17887
17888            let mut measurements = Vec::new();
17889            for (family, outcome) in [
17890                ("folder", folder),
17891                ("file", files),
17892                ("summary", summary),
17893                ("relation", relations),
17894            ] {
17895                require(
17896                    outcome.stamp == expected_stamp,
17897                    &format!("{family} call did not remain bound to the verified epoch"),
17898                )?;
17899                require(
17900                    outcome.work.exact_verifications == 0
17901                        && outcome.work.filesystem_entries == 0
17902                        && outcome.work.filesystem_bytes == 0
17903                        && outcome.work.decoded_nodes == 0,
17904                    &format!("{family} call repeated repository-sized freshness work"),
17905                )?;
17906                require(
17907                    outcome.work.sqlite_read_statements == 1,
17908                    &format!("{family} freshness check used unexpected SQLite work"),
17909                )?;
17910                require(
17911                    outcome.work.output_bytes == u64::try_from(outcome.value.len())?,
17912                    &format!("{family} call did not record its accepted rendered bytes"),
17913                )?;
17914                require(
17915                    outcome.value.len() <= MAX_MEASURED_QUERY_OUTPUT_BYTES,
17916                    &format!("{family} call exceeded the focused output bound"),
17917                )?;
17918                require(
17919                    !outcome.work.elapsed.is_zero()
17920                        && outcome.work.elapsed <= MAX_MEASURED_QUERY_ELAPSED,
17921                    &format!("{family} call did not retain a bounded elapsed measurement"),
17922                )?;
17923                measurements.push((family, outcome.work));
17924            }
17925            Ok::<_, Box<dyn std::error::Error>>(measurements)
17926        };
17927
17928        let small = measure(0)?;
17929        let large = measure(LARGE_UNRELATED_SOURCE_FILES)?;
17930        for ((small_family, small_work), (large_family, large_work)) in small.into_iter().zip(large)
17931        {
17932            require(
17933                small_family == large_family,
17934                "small and large query measurements used different families",
17935            )?;
17936            require(
17937                small_work.exact_verifications == large_work.exact_verifications
17938                    && small_work.filesystem_entries == large_work.filesystem_entries
17939                    && small_work.filesystem_bytes == large_work.filesystem_bytes
17940                    && small_work.sqlite_read_statements == large_work.sqlite_read_statements
17941                    && small_work.decoded_nodes == large_work.decoded_nodes,
17942                &format!("{small_family} warm freshness work changed with repository scale"),
17943            )?;
17944        }
17945        Ok(())
17946    }
17947
17948    #[test]
17949    fn selected_root_absolute_path_keys_stay_inside_selected_project()
17950    -> Result<(), Box<dyn std::error::Error>> {
17951        let temp = tempfile::tempdir()?;
17952        let repo = temp.path().join("repo");
17953        let outside = temp.path().join("outside");
17954        fs::create_dir_all(repo.join("src"))?;
17955        fs::create_dir_all(outside.join("src"))?;
17956        let inside_file = repo.join("src").join("lib.rs");
17957        let outside_file = outside.join("src").join("lib.rs");
17958        fs::write(&inside_file, "pub fn inside() {}\n")?;
17959        fs::write(&outside_file, "pub fn outside() {}\n")?;
17960        let state = McpProjectState {
17961            root: canonical_project_root(&repo)?,
17962            db_path: repo.join(".projectatlas").join("projectatlas.db"),
17963            config_path: None,
17964            worktree: None,
17965        };
17966
17967        let inside_key =
17968            ProjectAtlasMcpServer::absolute_path_key_in_selected_project(&state, &inside_file)?
17969                .ok_or_else(|| io::Error::other("inside selected root did not produce key"))?;
17970        require(
17971            inside_key == "src/lib.rs",
17972            "inside selected root produced wrong repo key",
17973        )?;
17974        require(
17975            ProjectAtlasMcpServer::absolute_path_key_in_selected_project(&state, &outside_file)?
17976                .is_none(),
17977            "outside selected root produced a repo key",
17978        )?;
17979
17980        Ok(())
17981    }
17982
17983    #[test]
17984    fn indexed_root_candidate_requires_matching_project_root()
17985    -> Result<(), Box<dyn std::error::Error>> {
17986        let temp = tempfile::tempdir()?;
17987        let repo = temp.path().join("repo");
17988        let other = temp.path().join("other");
17989        fs::create_dir_all(repo.join(".projectatlas"))?;
17990        fs::create_dir_all(&other)?;
17991
17992        require(
17993            ProjectAtlasMcpServer::indexed_root_from_candidate(&repo).is_none(),
17994            "candidate without DB was treated as indexed",
17995        )?;
17996
17997        let db_path = repo.join(".projectatlas").join("projectatlas.db");
17998        {
17999            let _store = open_atlas_store_for_project(&db_path, &other)?;
18000        }
18001        require(
18002            ProjectAtlasMcpServer::indexed_root_from_candidate(&repo).is_none(),
18003            "candidate with mismatched DB root was treated as indexed",
18004        )?;
18005
18006        reset_index_files(&db_path, true, false, false, None)?;
18007        {
18008            let _store = open_atlas_store_for_project(&db_path, &repo)?;
18009        }
18010        let indexed = ProjectAtlasMcpServer::indexed_root_from_candidate(&repo)
18011            .ok_or_else(|| io::Error::other("matching DB root was not accepted"))?;
18012        require(
18013            indexed.root == canonical_project_root(&repo)?,
18014            "indexed root did not preserve canonical candidate root",
18015        )?;
18016        let expected_db_path =
18017            ProjectAtlasMcpServer::projectatlas_db_path(&canonical_project_root(&repo)?);
18018        require(
18019            indexed.db_path == expected_db_path,
18020            "indexed root changed DB path",
18021        )?;
18022
18023        Ok(())
18024    }
18025
18026    #[cfg(windows)]
18027    #[test]
18028    fn indexed_root_predecessor_candidate_supports_nearest_routing()
18029    -> Result<(), Box<dyn std::error::Error>> {
18030        let temp = tempfile::tempdir()?;
18031        let root = temp.path().join("predecessor");
18032        let source = root.join("src").join("lib.rs");
18033        let database = root.join(".projectatlas").join("projectatlas.db");
18034        fs::create_dir_all(
18035            source
18036                .parent()
18037                .ok_or_else(|| io::Error::other("predecessor source path has no parent"))?,
18038        )?;
18039        fs::create_dir_all(
18040            database
18041                .parent()
18042                .ok_or_else(|| io::Error::other("predecessor database path has no parent"))?,
18043        )?;
18044        fs::write(&source, "pub fn predecessor() {}\n")?;
18045        let store = open_atlas_store_for_project(&database, &root)?;
18046        drop(store);
18047        let predecessor = rusqlite::Connection::open(&database)?;
18048        drop_native_worktree_identity_schema(&predecessor)?;
18049        predecessor.execute_batch(
18050            "DROP TABLE project_root_identity;
18051             DROP TABLE IF EXISTS graph_identity_rejections;
18052             UPDATE metadata SET value = '19' WHERE key = 'schema_version';",
18053        )?;
18054        drop(predecessor);
18055
18056        let expected_root = canonical_project_root(&root)?;
18057        let expected_database = ProjectAtlasMcpServer::projectatlas_db_path(&expected_root);
18058        let canonical = ProjectAtlasMcpServer::project_state_from_nearest_indexed_path(&source)?
18059            .ok_or_else(|| io::Error::other("canonical nearest predecessor was not found"))?;
18060        let lexical =
18061            ProjectAtlasMcpServer::project_state_from_nearest_lexical_indexed_path(&source)?
18062                .ok_or_else(|| io::Error::other("lexical nearest predecessor was not found"))?;
18063        for state in [canonical, lexical] {
18064            require(
18065                state.root == expected_root && state.db_path == expected_database,
18066                "nearest predecessor routing changed the canonical root or database path",
18067            )?;
18068        }
18069        Ok(())
18070    }
18071
18072    #[cfg(windows)]
18073    #[test]
18074    fn nearest_routing_recanonicalizes_case_only_root_rename()
18075    -> Result<(), Box<dyn std::error::Error>> {
18076        let temp = tempfile::tempdir()?;
18077        let original = temp.path().join("NearestCaseRoot");
18078        let staging = temp.path().join("NearestCaseRootStaging");
18079        let renamed = temp.path().join("nearestcaseroot");
18080        let source = original.join("src").join("lib.rs");
18081        fs::create_dir_all(
18082            source
18083                .parent()
18084                .ok_or_else(|| io::Error::other("case-only nearest source has no parent"))?,
18085        )?;
18086        let database = original.join(".projectatlas").join("projectatlas.db");
18087        fs::create_dir_all(
18088            database
18089                .parent()
18090                .ok_or_else(|| io::Error::other("case-only nearest database has no parent"))?,
18091        )?;
18092        fs::write(&source, "pub fn nearest_case_only() {}\n")?;
18093        drop(open_atlas_store_for_project(&database, &original)?);
18094
18095        fs::rename(&original, &staging)?;
18096        fs::rename(&staging, &renamed)?;
18097        let renamed_source = renamed.join("src").join("lib.rs");
18098        let expected_root = canonical_project_root(&renamed)?;
18099        let expected_database = ProjectAtlasMcpServer::projectatlas_db_path(&expected_root);
18100        let canonical =
18101            ProjectAtlasMcpServer::project_state_from_nearest_indexed_path(&renamed_source)?
18102                .ok_or_else(|| {
18103                    io::Error::other("canonical nearest case-only root was not found")
18104                })?;
18105        let lexical = ProjectAtlasMcpServer::project_state_from_nearest_lexical_indexed_path(
18106            &renamed_source,
18107        )?
18108        .ok_or_else(|| io::Error::other("lexical nearest case-only root was not found"))?;
18109        for state in [canonical, lexical] {
18110            require(
18111                state.root == expected_root && state.db_path == expected_database,
18112                "nearest routing rejected a case-only root rename",
18113            )?;
18114        }
18115        Ok(())
18116    }
18117
18118    #[cfg(windows)]
18119    #[test]
18120    fn nearest_routing_rejects_case_sensitive_sibling() -> Result<(), Box<dyn std::error::Error>> {
18121        let temp = tempfile::tempdir()?;
18122        let parent = temp.path().join("nearest-case-sensitive-parent");
18123        fs::create_dir(&parent)?;
18124        let enabled = StdCommand::new("fsutil")
18125            .args(["file", "SetCaseSensitiveInfo"])
18126            .arg(&parent)
18127            .arg("enable")
18128            .status()
18129            .is_ok_and(|status| status.success());
18130        if !enabled {
18131            return Ok(());
18132        }
18133
18134        let stored_root = parent.join("Repo");
18135        let selected_root = parent.join("repo");
18136        fs::create_dir(&stored_root)?;
18137        fs::create_dir(&selected_root)?;
18138        let stored_database = stored_root.join(".projectatlas").join("projectatlas.db");
18139        let selected_database = selected_root.join(".projectatlas").join("projectatlas.db");
18140        fs::create_dir_all(
18141            stored_database
18142                .parent()
18143                .ok_or_else(|| io::Error::other("stored nearest database has no parent"))?,
18144        )?;
18145        fs::create_dir_all(
18146            selected_database
18147                .parent()
18148                .ok_or_else(|| io::Error::other("selected nearest database has no parent"))?,
18149        )?;
18150        drop(open_atlas_store_for_project(
18151            &stored_database,
18152            &stored_root,
18153        )?);
18154        fs::copy(&stored_database, &selected_database)?;
18155
18156        require(
18157            ProjectAtlasMcpServer::indexed_root_from_candidate(&selected_root).is_none()
18158                && ProjectAtlasMcpServer::indexed_root_from_lexical_candidate(&selected_root)
18159                    .is_none(),
18160            "nearest routing accepted a distinct case-sensitive sibling",
18161        )?;
18162        Ok(())
18163    }
18164
18165    #[cfg(unix)]
18166    #[test]
18167    fn mcp_init_rejects_ambiguous_predecessor_before_project_writes()
18168    -> Result<(), Box<dyn std::error::Error>> {
18169        use std::ffi::OsString;
18170        use std::os::unix::ffi::OsStringExt;
18171
18172        let temp = tempfile::tempdir()?;
18173        let raw_root = temp
18174            .path()
18175            .join(OsString::from_vec(b"mcp-raw-root-\x80".to_vec()));
18176        let replacement_root = PathBuf::from(raw_root.to_string_lossy().into_owned());
18177        let database = raw_root
18178            .join(PROJECTATLAS_DIR_NAME)
18179            .join(PROJECTATLAS_DB_FILE_NAME);
18180        fs::create_dir_all(&raw_root)?;
18181        fs::create_dir_all(
18182            database
18183                .parent()
18184                .ok_or_else(|| io::Error::other("predecessor database has no parent"))?,
18185        )?;
18186        let store = AtlasStore::open_for_project(&database, &raw_root)?;
18187        drop(store);
18188        let predecessor = rusqlite::Connection::open(&database)?;
18189        drop_native_worktree_identity_schema(&predecessor)?;
18190        predecessor.execute_batch(
18191            "DROP TABLE project_root_identity;
18192             DROP TABLE IF EXISTS graph_identity_rejections;
18193             UPDATE metadata SET value = '19' WHERE key = 'schema_version';",
18194        )?;
18195        predecessor.execute(
18196            "INSERT INTO metadata(key, value) VALUES('project_root', ?1)
18197             ON CONFLICT(key) DO UPDATE SET value = excluded.value",
18198            [&replacement_root.to_string_lossy().into_owned()],
18199        )?;
18200        drop(predecessor);
18201        fs::create_dir_all(&replacement_root)?;
18202
18203        // The startup root resolver opens the predecessor read-only. SQLite
18204        // may materialize WAL sidecars for that read path, so warm the exact
18205        // recovery route before capturing the no-mutation baseline. An
18206        // ambiguous legacy root must be refused rather than selected.
18207        require(
18208            default_mcp_project_root(&database, None).is_err(),
18209            "ambiguous predecessor was selected during startup discovery",
18210        )?;
18211        let database_before = fs::read(&database)?;
18212        let sidecars_before = ["wal", "shm", "journal"]
18213            .map(|suffix| fs::read(db_sidecar_path(&database, suffix)).ok());
18214        let raw_project_dir = raw_root.join(PROJECTATLAS_DIR_NAME);
18215        let raw_config = raw_project_dir.join(PROJECTATLAS_CONFIG_FILE_NAME);
18216        let raw_nonsource = raw_project_dir.join(MCP_NONSOURCE_FILE_NAME);
18217        let replacement_project_dir = replacement_root.join(PROJECTATLAS_DIR_NAME);
18218        let replacement_config = replacement_project_dir.join(PROJECTATLAS_CONFIG_FILE_NAME);
18219        let replacement_nonsource = replacement_project_dir.join(MCP_NONSOURCE_FILE_NAME);
18220        let server = ProjectAtlasMcpServer::new(
18221            database.clone(),
18222            None,
18223            "ambiguous-predecessor".to_string(),
18224            false,
18225        );
18226        let startup = ProjectAtlasMcpServer::startup_project_state(database.clone(), None);
18227        require(
18228            startup.root == canonical_project_root(&raw_root)?,
18229            "MCP startup did not select the raw native predecessor root",
18230        )?;
18231        require(
18232            startup.root != canonical_project_root(&replacement_root)?,
18233            "MCP startup selected the replacement-character candidate",
18234        )?;
18235        let result = server.atlas_init(Parameters(AtlasInitParams {
18236            project_path: None,
18237            worktree: None,
18238            no_scan: Some(true),
18239            force_rescan: Some(false),
18240            text_index_max_bytes: None,
18241        }));
18242        require(
18243            result.contains("project-root identity"),
18244            &format!("ambiguous predecessor init returned an unexpected result: {result}"),
18245        )?;
18246        require(
18247            fs::read(&database)? == database_before
18248                && ["wal", "shm", "journal"]
18249                    .map(|suffix| fs::read(db_sidecar_path(&database, suffix)).ok())
18250                    == sidecars_before,
18251            "ambiguous predecessor init changed database or sidecar state",
18252        )?;
18253        require(
18254            !replacement_project_dir.exists()
18255                && !replacement_config.exists()
18256                && !replacement_nonsource.exists(),
18257            "ambiguous predecessor init wrote replacement-root project state",
18258        )?;
18259        require(
18260            !raw_config.exists() && !raw_nonsource.exists(),
18261            "ambiguous predecessor init wrote raw-root project state",
18262        )?;
18263        Ok(())
18264    }
18265
18266    #[test]
18267    fn mcp_init_rejects_current_wrong_root_before_project_writes()
18268    -> Result<(), Box<dyn std::error::Error>> {
18269        let temp = tempfile::tempdir()?;
18270        let selected_root = temp.path().join("selected-root");
18271        let bound_root = temp.path().join("bound-root");
18272        let database = temp.path().join("external-projectatlas.db");
18273        let config_path = selected_root.join("external-config/config.toml");
18274        fs::create_dir_all(&selected_root)?;
18275        fs::create_dir_all(&bound_root)?;
18276        let persisted_identity = {
18277            let store = AtlasStore::open_for_project(&database, &bound_root)?;
18278            store.project_root_identity()?
18279        };
18280        drop(AtlasStore::open_read_only_for_project(
18281            &database,
18282            &bound_root,
18283        )?);
18284        let database_before = fs::read(&database)?;
18285        let sidecars_before = ["wal", "shm", "journal"]
18286            .map(|suffix| fs::read(crate::runtime::db_sidecar_path(&database, suffix)).ok());
18287        let selected_project_dir = selected_root.join(PROJECTATLAS_DIR_NAME);
18288        let config_parent = config_path
18289            .parent()
18290            .ok_or_else(|| io::Error::other("selected config has no parent"))?;
18291        let server = ProjectAtlasMcpServer::new(
18292            database.clone(),
18293            Some(config_path.clone()),
18294            "current-wrong-root".to_string(),
18295            false,
18296        );
18297        *server
18298            .project_state
18299            .write()
18300            .map_err(|_poisoned| io::Error::other("MCP project state lock poisoned"))? =
18301            McpProjectState {
18302                root: selected_root,
18303                db_path: database.clone(),
18304                config_path: Some(config_path.clone()),
18305                worktree: None,
18306            };
18307
18308        let result = server.atlas_init(Parameters(AtlasInitParams {
18309            project_path: None,
18310            worktree: None,
18311            no_scan: Some(true),
18312            force_rescan: Some(false),
18313            text_index_max_bytes: None,
18314        }));
18315        require(
18316            result.contains("does not match selected root"),
18317            &format!("current wrong-root MCP init returned an unexpected result: {result}"),
18318        )?;
18319        require(
18320            !selected_project_dir.exists() && !config_parent.exists() && !config_path.exists(),
18321            "current wrong-root MCP init created selected project or config state",
18322        )?;
18323        require(
18324            fs::read(&database)? == database_before
18325                && ["wal", "shm", "journal"].map(|suffix| {
18326                    fs::read(crate::runtime::db_sidecar_path(&database, suffix)).ok()
18327                }) == sidecars_before,
18328            "current wrong-root MCP init changed database or sidecar state",
18329        )?;
18330        let reopened = AtlasStore::open_read_only_for_project(&database, &bound_root)?;
18331        require(
18332            reopened.project_root_identity()? == persisted_identity,
18333            "current binding changed after wrong-root MCP init",
18334        )?;
18335        Ok(())
18336    }
18337
18338    #[cfg(windows)]
18339    #[test]
18340    fn mcp_init_rejects_predecessor_wrong_root_before_project_writes()
18341    -> Result<(), Box<dyn std::error::Error>> {
18342        let temp = tempfile::tempdir()?;
18343        let selected_root = temp.path().join("selected-predecessor-root");
18344        let bound_root = temp.path().join("bound-predecessor-root");
18345        let database = temp.path().join("external-predecessor.db");
18346        let config_path = selected_root.join("external-config/config.toml");
18347        fs::create_dir_all(&selected_root)?;
18348        fs::create_dir_all(&bound_root)?;
18349        drop(AtlasStore::open_for_project(&database, &bound_root)?);
18350        {
18351            let connection = rusqlite::Connection::open(&database)?;
18352            drop_native_worktree_identity_schema(&connection)?;
18353            connection.execute_batch(
18354                "DROP TABLE project_root_identity;
18355                 DROP TABLE IF EXISTS graph_identity_rejections;
18356                 UPDATE metadata SET value = '19' WHERE key = 'schema_version';",
18357            )?;
18358        }
18359        read_legacy_project_root_candidate_read_only(&database)?;
18360        let database_before = fs::read(&database)?;
18361        let sidecars_before = ["wal", "shm", "journal"]
18362            .map(|suffix| fs::read(crate::runtime::db_sidecar_path(&database, suffix)).ok());
18363        let selected_project_dir = selected_root.join(PROJECTATLAS_DIR_NAME);
18364        let config_parent = config_path
18365            .parent()
18366            .ok_or_else(|| io::Error::other("selected predecessor config has no parent"))?;
18367        let server = ProjectAtlasMcpServer::new(
18368            database.clone(),
18369            Some(config_path.clone()),
18370            "predecessor-wrong-root".to_string(),
18371            false,
18372        );
18373        *server
18374            .project_state
18375            .write()
18376            .map_err(|_poisoned| io::Error::other("MCP project state lock poisoned"))? =
18377            McpProjectState {
18378                root: selected_root,
18379                db_path: database.clone(),
18380                config_path: Some(config_path.clone()),
18381                worktree: None,
18382            };
18383
18384        let result = server.atlas_init(Parameters(AtlasInitParams {
18385            project_path: None,
18386            worktree: None,
18387            no_scan: Some(true),
18388            force_rescan: Some(false),
18389            text_index_max_bytes: None,
18390        }));
18391        require(
18392            result.contains("does not match selected root"),
18393            &format!("predecessor wrong-root MCP init returned an unexpected result: {result}"),
18394        )?;
18395        require(
18396            !selected_project_dir.exists() && !config_parent.exists() && !config_path.exists(),
18397            "predecessor wrong-root MCP init created selected project or config state",
18398        )?;
18399        require(
18400            fs::read(&database)? == database_before
18401                && ["wal", "shm", "journal"].map(|suffix| {
18402                    fs::read(crate::runtime::db_sidecar_path(&database, suffix)).ok()
18403                }) == sidecars_before,
18404            "predecessor wrong-root MCP init changed database or sidecar state",
18405        )?;
18406        Ok(())
18407    }
18408
18409    #[cfg(unix)]
18410    #[test]
18411    fn nearest_root_rejects_ambiguous_predecessor_without_mutation()
18412    -> Result<(), Box<dyn std::error::Error>> {
18413        fn sidecar_bytes(database: &Path) -> [Option<Vec<u8>>; 3] {
18414            ["wal", "shm", "journal"].map(|suffix| fs::read(db_sidecar_path(database, suffix)).ok())
18415        }
18416
18417        fn directory_inventory(path: &Path) -> Result<Vec<String>, Box<dyn std::error::Error>> {
18418            let mut names = fs::read_dir(path)?
18419                .map(|entry| entry.map(|entry| entry.file_name().to_string_lossy().into_owned()))
18420                .collect::<Result<Vec<_>, _>>()?;
18421            names.sort();
18422            Ok(names)
18423        }
18424
18425        fn snapshot(
18426            database: &Path,
18427        ) -> Result<(Vec<u8>, [Option<Vec<u8>>; 3], Vec<String>), Box<dyn std::error::Error>>
18428        {
18429            let parent = database
18430                .parent()
18431                .ok_or_else(|| io::Error::other("predecessor database has no parent"))?;
18432            Ok((
18433                fs::read(database)?,
18434                sidecar_bytes(database),
18435                directory_inventory(parent)?,
18436            ))
18437        }
18438
18439        let temp = tempfile::tempdir()?;
18440        let raw_root = temp
18441            .path()
18442            .join(std::ffi::OsString::from_vec(b"nearest-raw-\x80".to_vec()));
18443        let replacement_root = temp.path().join("nearest-raw-�");
18444        let raw_database = raw_root.join(".projectatlas").join("projectatlas.db");
18445        let replacement_database = replacement_root
18446            .join(".projectatlas")
18447            .join("projectatlas.db");
18448        fs::create_dir_all(
18449            raw_database
18450                .parent()
18451                .ok_or_else(|| io::Error::other("raw predecessor database has no parent"))?,
18452        )?;
18453        fs::create_dir_all(
18454            replacement_database.parent().ok_or_else(|| {
18455                io::Error::other("replacement predecessor database has no parent")
18456            })?,
18457        )?;
18458        let store = open_atlas_store_for_project(&raw_database, &raw_root)?;
18459        drop(store);
18460        let predecessor = rusqlite::Connection::open(&raw_database)?;
18461        drop_native_worktree_identity_schema(&predecessor)?;
18462        predecessor.execute_batch(
18463            "DROP TABLE project_root_identity;
18464             DROP TABLE IF EXISTS graph_identity_rejections;
18465             UPDATE metadata SET value = '19' WHERE key = 'schema_version';",
18466        )?;
18467        drop(predecessor);
18468        fs::copy(&raw_database, &replacement_database)?;
18469
18470        // A read-only WAL opener may materialize its sidecars. Warm the exact
18471        // recovery path before capturing the no-mutation baseline.
18472        for database in [&raw_database, &replacement_database] {
18473            require(
18474                matches!(
18475                    read_legacy_project_root_candidate_read_only(database),
18476                    Err(DbError::ProjectRootIdentityMissing)
18477                ),
18478                "ambiguous predecessor candidate was exposed during warm-up",
18479            )?;
18480        }
18481
18482        for (root, database) in [
18483            (&raw_root, &raw_database),
18484            (&replacement_root, &replacement_database),
18485        ] {
18486            let before = snapshot(database)?;
18487            require(
18488                ProjectAtlasMcpServer::indexed_root_from_candidate(root).is_none()
18489                    && ProjectAtlasMcpServer::indexed_root_from_lexical_candidate(root).is_none(),
18490                "ambiguous predecessor was admitted by nearest routing",
18491            )?;
18492            require(
18493                snapshot(database)? == before,
18494                "ambiguous predecessor detection changed database or sidecar state",
18495            )?;
18496        }
18497
18498        let collision_raw_root = temp
18499            .path()
18500            .join(std::ffi::OsString::from_vec(b"nearest-repo\\name".to_vec()));
18501        let collision_slash_root = temp.path().join("nearest-repo").join("name");
18502        let collision_raw_database = collision_raw_root
18503            .join(PROJECTATLAS_DIR_NAME)
18504            .join(PROJECTATLAS_DB_FILE_NAME);
18505        let collision_slash_database = collision_slash_root
18506            .join(PROJECTATLAS_DIR_NAME)
18507            .join(PROJECTATLAS_DB_FILE_NAME);
18508        fs::create_dir_all(&collision_raw_root)?;
18509        fs::create_dir_all(
18510            collision_slash_database
18511                .parent()
18512                .ok_or_else(|| io::Error::other("slash collision database has no parent"))?,
18513        )?;
18514        drop(open_atlas_store_for_project(
18515            &collision_raw_database,
18516            &collision_raw_root,
18517        )?);
18518        let collision_predecessor = rusqlite::Connection::open(&collision_raw_database)?;
18519        collision_predecessor.execute(
18520            "UPDATE metadata SET value = ?1 WHERE key = 'project_root'",
18521            [collision_slash_root.to_string_lossy().into_owned()],
18522        )?;
18523        drop_native_worktree_identity_schema(&collision_predecessor)?;
18524        collision_predecessor.execute_batch(
18525            "DROP TABLE project_root_identity;
18526             DROP TABLE IF EXISTS graph_identity_rejections;
18527             UPDATE metadata SET value = '19' WHERE key = 'schema_version';",
18528        )?;
18529        drop(collision_predecessor);
18530        fs::copy(&collision_raw_database, &collision_slash_database)?;
18531        for database in [&collision_raw_database, &collision_slash_database] {
18532            require(
18533                matches!(
18534                    read_legacy_project_root_candidate_read_only(database),
18535                    Err(DbError::ProjectRootIdentityMissing)
18536                ),
18537                "slash-colliding predecessor candidate was exposed during warm-up",
18538            )?;
18539        }
18540        for (root, database) in [
18541            (&collision_raw_root, &collision_raw_database),
18542            (&collision_slash_root, &collision_slash_database),
18543        ] {
18544            let before = snapshot(database)?;
18545            require(
18546                ProjectAtlasMcpServer::indexed_root_from_candidate(root).is_none()
18547                    && ProjectAtlasMcpServer::indexed_root_from_lexical_candidate(root).is_none(),
18548                "slash-colliding predecessor was admitted by nearest routing",
18549            )?;
18550            require(
18551                snapshot(database)? == before,
18552                "slash-colliding predecessor detection changed database or sidecar state",
18553            )?;
18554        }
18555        Ok(())
18556    }
18557}