diff --git a/rust/contracts/application-boundary.md b/rust/contracts/application-boundary.md index 5d41ea90..4758c4c9 100644 --- a/rust/contracts/application-boundary.md +++ b/rust/contracts/application-boundary.md @@ -27,7 +27,7 @@ no runtime JSON loading, package download or write to installed resources occurs The locale registry preserves each platform's persisted identifiers and default language. Explicit fallback chains resolve shared entries during generation; all existing platform catalogs and presentation-owned bilingual copy use this source. Translation coverage is not a -capability claim. [Format fixtures](../fixtures/i18n/format-v1.json) bind generation +capability claim. [Format fixtures](https://github.com/1lck/Lithe-IDEA/blob/preview/shared/i18n/fixtures/format-v1.json) bind generation to native positional formatting, literal percent signs and unmodified argument data. ## Feature Contracts diff --git a/rust/contracts/rust-core-api.md b/rust/contracts/rust-core-api.md index 37636689..0d6cd826 100644 --- a/rust/contracts/rust-core-api.md +++ b/rust/contracts/rust-core-api.md @@ -273,7 +273,7 @@ The ACP Agent ABI and the `agent.*` commands were removed together with the Agent conversation feature. `lithe_agent_open_json`, `lithe_agent_send_json` and `lithe_agent_close` no longer exist, and Core starts no adapter, proxies no model call and keeps no conversation history. Rust Core keeps only deterministic -AI configuration parsing (`ai.configuration.*`) plus MCP configuration planning. +AI configuration parsing (`ai.configuration.*`). The removed ABI text, fixtures and the `lithe-agent-host` crate remain available in this repository's history. @@ -341,6 +341,388 @@ subsystem: `mcp.parseConfiguration`, `mcp.parseRegistry`, `mcp.featuredRegistry` server (`lithe-mcp` plus `rust/fixtures/ide-api/mcp-protocol.json`) is unaffected and stays in `lithe-ide-host`. +Every command name `CoreCommand::parse` accepts has exactly one row below. The +`protocol::command` tests compare this table with the parser, so adding, renaming +or removing a command fails until the table changes in the same commit. + +| Command | Purpose | +| --- | --- | +| `core.ping` | Verify the ABI and protocol version | +| `ideHost.control` | Control the native IDE capability broker | +| `ai.commitInputBudget` | Budget diff prefixes before crossing the native boundary | +| `ai.planCommit` | Plan a credential-free commit message request | +| `ai.decodeCommitMessage` | Decode a provider commit message response | +| `ai.configuration.sources` | Name the native files of one AI CLI configuration source | +| `ai.configuration.parse` | Parse AI CLI configuration into credential-free metadata | +| `ai.pullRequest.plan` | Plan a credential-free pull request description request | +| `ai.pullRequest.decode` | Decode a strict pull request title and description | +| `ai.inlineEdit.plan` | Plan a credential-free inline edit request | +| `ai.inlineEdit.decode` | Decode the replacement text of an inline edit response | +| `plugins.declarative.validate` | Validate and normalize a declarative plugin manifest | +| `plugins.nativeUI.validateData` | Validate native plugin UI data with declaration identity and node budgets | +| `plugins.web.open` | Register a plugin page for a web tool window | +| `plugins.web.close` | Forget a plugin page and lists its calls to cancel | +| `plugins.web.resource` | Decide how a plugin page request is served | +| `plugins.web.document` | Prepend the policy and bridge to a page document | +| `plugins.web.request` | Check one bridge message of a plugin page | +| `plugins.web.reply` | Frame the host's answer to a bridge action | +| `plugins.web.event` | Frame an event for a plugin page | +| `plugins.declarative.runPlan` | Expand a declarative plugin's run template for one workspace file | +| `plugins.declarative.discoverTests` | List a declarative plugin's workspace and file test items | +| `plugins.declarative.testPlan` | Expand a declarative plugin's test template for a workspace, file, or test case | +| `plugins.declarative.languageServerLaunchPlan` | Expand a declarative plugin's language-server, build-provider, VCS-provider, backend, or debug-adapter launch with host directories and architecture | +| `plugins.languageServers.decodeLaunch` | Bound the answer of a plugin backend that resolved a language server's launch and return the plan the host starts | +| `plugins.languageServers.stateRetention` | Choose which inactive per-workspace state directories of a plugin language server the host deletes | +| `plugins.contentProviders.decode` | Bound the answer of a plugin content provider function and return the document text | +| `debug.adapters.startArguments` | Check one debug start against a plugin's `debugAdapters[]` entry and return its DAP launch arguments | +| `debug.validateAdapterEndpoint` | Validate an IDE-to-DAP TCP endpoint and classify whether remote consent is required; performs no DNS or network I/O | +| `debug.adapters.executablePlan` | Say where to look for the program a launch debugs: the user's setting and the adapter's candidate paths | +| `debug.transport.policy` | Return the default byte-stream deadlines, queue budgets and the session `initialize` deadline; creates no state | +| `debug.transport.handle` | Apply one platform event to an IDE-to-adapter byte-stream lifecycle and return the effects the platform runs next | +| `debug.transport.release` | Forget the lifecycle of a stopped byte stream once the platform dropped its handles | +| `plugins.declarative.resolveSettings` | Merge a plugin's stored global and project setting layers into effective values with their source | +| `plugins.settings.catalog` | Project enabled plugin settings pages from installed manifests | +| `plugins.settings.navigation` | Project searched settings navigation without platform policy | +| `plugins.settings.delivery` | Route committed preference effects using native ownership facts | +| `plugins.declarative.settingsState` | Read complete plugin preference documents into Core-normalized rows, sources, and bounded native read plans | +| `plugins.declarative.settingsActionPlan` | Resolve an installed settings action and its native confirmation requirement | +| `plugins.installed.select` | Select numeric installed versions from native facts without reading paths | +| `plugins.declarative.editSettings` | Validate one atomic setting edit batch and return replacement text while preserving unrelated plugins and dormant values | +| `plugins.declarative.workspaceConfiguration` | Expand a language server's `workspaceConfiguration` with the merged setting values | +| `plugins.declarative.decodeGrammar` | Validate independent static editor resources | +| `plugins.editorActions.decode` | Validate independent action registration, collection or click results | +| `plugins.hostApi.describe` | Describe backend host methods and their required grants from Core's method table | +| `plugins.hostApi.authorize` | Check a method, service and current session grants using Core's capability rules; the host still checks session and resource ownership | +| `plugins.editorProviders.decode` | Validate editor provider registrations and provider function results | +| `keymap.decodeImport` | Validate a normalized shortcut import against the host's action catalog | +| `keymap.planImportSources` | Plan the selected provider's declared shortcut source inputs | +| `keymap.planImportBindings` | Plan conflicts and default-key displacement for a shortcut import | +| `debug.startOrContinue` | Decide whether a combined debug shortcut starts, continues or does nothing | +| `toolchains.registry` | Revalidate complete enabled plugin manifests, `{plugins:[{manifest}]}`, into a deterministic tool-definition/provider registry; the host supplies no origin evidence. A short tool ID defined by several enabled plugins belongs to none of them: none of those definitions is registered, the rest register as usual, and `conflicts: [{tool, pluginIDs}]` (sorted by tool, `pluginIDs` sorted) reports each contested tool. A repeated plugin ID rejects the batch. No native host capability claim | +| `toolchains.decodeReply` | Validate an inert toolchain query/reply pair against bound provider identity, generation, declared tools and host-native filesystem evidence; no transport or native I/O | +| `toolchains.failureReply` | Build the JSON-RPC error reply for a query that missed its deadline or found no backend, so the host passes it to `toolchains.decodeReply` like a provider's own error; cancellation has none | +| `toolchains.resolvePins` | Validate a captured local document and find the nearest ancestor directory pin, retaining dormant and legacy entries without probing or writing | +| `toolchains.editPins` | Apply one bounded batch of directory pin edits to a captured local document without writing native files | +| `toolchains.pinStoragePlan` | Declare bounded workspace-native pin and ignore sources | +| `toolchains.decodePinDocument` | Validate a complete native source or supply the absent-source default | +| `toolchains.editPinSources` | Plan deterministic pin and Git ignore source edits without native I/O | +| `toolchains.resolveConsumerPreferences` | Validate an inert consumer preference document and resolve one tool's explicit Run, consumer, directory or workspace choice while keeping workspace/consumer inheritance symbolic; no native files or launch authority | +| `toolchains.editConsumerPreferences` | Edit or reset one exact consumer/directory tool preference; return the document and effective input without native writes | +| `toolchains.resolve` | Resolve the fixed/project/global/automatic layers from one generation-bound snapshot, retaining unmet requirements, unavailable providers with their cause, conflicts and terminal fixed-value failures | +| `toolchains.checkVersion` | Match captured output with an enabled plugin definition's version expression and compare generic numeric components with an optional minimum; never starts a probe | +| `toolchains.discoveryPlan` | Return platform-filtered definition-owned scan declarations, layouts, probes, PATH entries, login-shell home variables and budgets; never scans native directories | +| `toolchains.candidates` | List one tool's login-shell homes and host-scanned installations with checked executables, versions or skip reasons, and the automatic-layer choice; never starts a probe | +| `toolchains.instantiateLaunch` | Expand an enabled tool definition's `launch` template for a selected executable, entry, runtime arguments and arguments; a part the template cannot carry is `plugin.toolchain.launchMismatch`; performs no I/O | +| `run.decodeStart` | Validate a plugin backend's `lithe/host/run/start` params against its manifest and the workspace, and return the normalized start with temporary-file paths substituted | +| `debug.decodeBackendConfiguration` | Validate a plugin backend's `lithe/host/debug/start` params against its manifest and return the qualified adapter, request, label and configuration | +| `outputLinks.match` | Find the links plugin `outputLinkMatchers` declare in Run or Test console lines, as UTF-16 ranges with candidate paths or a backend function to resolve them | +| `outputLinks.decodeResolution` | Check a matcher's `resolve` function reply and return the 1-based file location to open, or `null` | +| `build.detect` | Find every build-system instance the enabled plugins declare in a workspace, with models that need no process | +| `build.projectOptions` | Validate a native team's persisted Build options | +| `build.resolveStoredOptions` | Merge checked team and machine-local Build preferences without I/O | +| `build.editStoredOption` | Edit one declared option in a machine-local Build document without I/O | +| `build.invocationPlan` | List a build system's executable candidates, runtime dependencies, and version probe for one platform | +| `build.checkToolVersion` | Decide whether a build tool's or runtime's version output meets the declared minimum | +| `build.resolveOptions` | Check stored build options against the declaration and return effective values, choices, and the fingerprint | +| `build.modelPlan` | Expand the command that prints a command-sourced build model, or the request a build provider answers | +| `build.taskPlan` | Expand one declared or model task for a workspace, project, or target | +| `build.testPlan` | Plan a test run for a scope, suite, or case and say where its reports will be | +| `build.prepareDebugPlan` | Plan same-plugin native debug preparation | +| `build.decodeDebugPreparation` | Validate an existing instance-contained native debug program and bounded process facts | +| `build.resolveModelInputs` | Validate enabled producer facts and project a bounded public model snapshot for a declared consumer | +| `build.configurationPlan` | Plan a read of the provider's non-sensitive native configuration | +| `build.configurationEditPlan` | Plans format-independent provider editing into private host staging | +| `build.decodeConfigurationEdit` | Validates the receipt without native document bytes | +| `build.decodeConfiguration` | Validate native source identities, raw/effective values and XML bindings | +| `build.prepareRunPlan` | Plan Provider-owned application/service/task preparation from a model target | +| `build.decodeRunPreparation` | Validate portable launch data before the native Run host starts a process | +| `build.testDiscoveryPlan` | Plan opt-in Provider test discovery for an instance | +| `build.decodeTestDiscovery` | Validate, deduplicate and order Provider test selectors | +| `build.dependencyPlan` | Expand the command that prints a project's dependency tree, or the request a build provider answers | +| `build.decodeModel` | Decode model output or a provider result into projects, targets, tasks, content roots, option values, and run entries | +| `build.decodeTestResults` | Decode a test run's reports and output into per-case results and a summary | +| `build.decodeDiagnostics` | Decode build output into workspace file diagnostics | +| `build.decodeDependencies` | Decode dependency output or a provider result into one project's bounded tree | +| `build.syncFingerprint` | Digest the files a build model depends on, to tell whether it is stale | +| `build.resolveTestRunOutcome` | Reduce a test run's termination, selected scope and decoded summary to one tool-neutral verdict | +| `build.mergeTestOutcomes` | Merge a run's test result ids into the stored history inside the host-stated id/prefix scope | +| `vcs.detectRoots` | Find the VCS roots of a workspace from static markers, with registered providers, ambiguous directories, and applied mappings | +| `vcs.ownerOf` | Assign workspace paths to the detected root that owns each | +| `vcs.decodeReply` | Validate a VCS provider's reply and return the normalized result | +| `project.generatorCatalog` | List the project generators of the given plugins, with the plugins that provide each requirement | +| `project.generatorForm` | Evaluate a generator's form: derived defaults, visibility, problems, and active requirements | +| `project.generationPlan` | Expand a filled-in form into the files, commands, and archive downloads that create a project | +| `project.checkName` | Check that a name can be a new project's directory name | +| `project.extractArchive` | Extract a downloaded project archive into the staging directory, refusing unsafe entries | +| `plugins.marketplace.validateIndex` | Validate a marketplace index and report each plugin's availability for one platform | +| `plugins.marketplace.resolveSource` | Resolve a user-entered marketplace source to the index URL to fetch | +| `plugins.marketplace.installReview` | List a package's permissions and programs, and those not yet approved | +| `community.discourse.auth.begin` | Create an ephemeral RSA-OAEP authorization session and return the Discourse browser URL | +| `community.discourse.auth.complete` | Decrypt, validate, and consume one Discourse user API key callback | +| `community.discourse.topics` | List normalized latest or top topic summaries | +| `community.discourse.topic` | Read one topic with ordered, sanitized post HTML | +| `community.discourse.categories` | List normalized visible categories | +| `community.discourse.search` | Search normalized topics and sanitized posts | +| `community.discourse.auth.revoke` | Revoke the current Discourse user API key | +| `workspace.snapshot` | Enumerate visible workspace nodes and relative file paths; `nestedWorktrees` lists linked worktrees checked out inside their own repository, which are left out of the tree, files, and search index; the built-in hidden names are skipped without being read, and include `node_modules`, Python virtual environments (`.venv`, `venv`), `__pycache__` and `*.pyc` files; `includeTree: false` returns only `files` and a childless `root`; `files` (and the tree, when included) keep the lowercase traversal order that bounded consumers such as Windows Quick Open rely on, not the natural display order of `workspace.listDirectories` | +| `workspace.releaseIndexing` | Retire a policy and clear index/cache data at project close; accepts `root`, optional `ownerId`, and that owner's last observed `revision`, returns `released`; an old close must not retire a new generation | +| `workspace.configureIndexing` | Install a process-local, canonical-root indexing policy before workspace consumers run; returns a monotonic revision, effective directory reasons and Git failure state; see `rust/fixtures/workspace/indexing-policy-v1.json` | +| `workspace.refreshIndexing` | Refresh external Git decisions for a live `root`, `ownerId`, `revision` lease without changing preferences or release tokens; returns lazy `IndexingStatus` | +| `workspace.filterChanges` | Filter an array of workspace-relative paths, or `{ paths, hiddenFilePatterns }` whose `!` entries turn default exclusions off, against Core's default build/cache exclusions; preserve order, spelling, and the empty root path. Native event loss bypasses filtering. Hosts relativize to the most specific explicit watch root and retain explicitly watched files | +| `workspace.listDirectories` | Return one visible level of children for each requested workspace-relative directory (`""` is the root); missing, hidden, symlinked, or nested-worktree paths are omitted, and each listing reports the nested worktrees it left out. Child directories have `children: []` only when a shallow probe proves them visibly empty; otherwise children remain omitted (unloaded), with no recursive tree allocation. Children list directories first, then names in IDEA's natural file-name order (digit runs compare by value, case-insensitive, `-` next to `_`; fixture `rust/fixtures/workspace/file-name-order-v1.json`) | +| `workspace.orderEntries` | Order natively enumerated directory siblings | +| `workspace.rankNames` | Pure ranking of candidate names that the platform enumerates itself (`{ query, candidates: [{ name, path? }], context: { openPaths, recentPaths }, limitPerTier? }`). Use it only for lists Core cannot index: Windows remote/WSL roots, symbol lists, navigator lists. Roots Core indexes go through `workspace.searchEverywhere`, so an inventory is never sent back to Core. Returns `matches: [{ index, degree, tier, nameMatchRanges }]` ordered by degree, open/recent tier, recency, then file-name order (fixture `rust/fixtures/search/rank-names-v1.json`) | +| `workspace.planDuplicate` | Name the copy of one project item next to its source (`foo.txt` → `foo copy.txt`, then `foo copy 2.txt`) from the sibling names the host has seen to exist | +| `workspace.planPaste` | Name pasted project items in one destination directory: a free name is kept, a taken one gets the copy suffix, and earlier items of the same paste count as taken | +| `workspace.reduceDirectoryAction` | Apply "set role", "exclude" or "cancel exclusion" to a root's persisted directory rules and return the complete state to persist | +| `workspace.relocateDirectoryRules` | Move (or drop, after a delete) the directory rule keys of a renamed, moved or deleted subtree, optionally into another workspace root's rules | +| `workspace.repositories` | Discover deterministic Git repository roots for an opened workspace | +| `workspace.recentProjects.reduce` | Apply one record, remove, pin, import, or normalize action to the recent project list and return the retained order | +| `workspace.searchIndex.warm` | Build or reuses the workspace search index | +| `workspace.searchIndex.update` | Apply changed paths to the search index | +| `workspace.searchIndex.invalidate` | Drop a workspace's cached search index | +| `search.actions` | Match host-localized action text without workspace I/O | +| `workspace.search` | Search visible file names and UTF-8 text files | +| `workspace.searchEverywhere` | Search file paths, contents, and symbols | +| `workspace.replacePreview` | Return deterministic replacement lines and complete replacement text | +| `workspace.replaceText` | Plan replacement against a host-captured text snapshot without writing | +| `workspace.searchTextBatch` | Search provider-captured text snapshots using workspace matching rules | +| `file.read` | Read a text file using a workspace-relative path; optional `encoding`, see [File encodings](#file-encodings) | +| `file.write` | Write a text file using a workspace-relative path; optional `encoding` and `bom` | +| `text.decode` | Decode `{ bytes }` (standard base64 of bytes the host read) like `file.read`, optional `encoding`; returns `{ text, encoding, hasBom, lossy, identity }` with the same 32 MB, plain-text and detection rules | +| `text.encode` | Encode `{ text }` like `file.write`, optional `encoding` and `bom`; returns `{ bytes, encoding, hasBom, identity }` with `bytes` in standard base64. Nothing is written, and an unmappable character fails with `encoding_unmappable` as `file.write` does | +| `document.lifecycle` | Reduce a shared document save, external-change, or conflict event without reading text or disk | +| `document.classifyText` | Classify decoded Unicode text independently of its filename | +| `document.planSaveBatch` | Select the open documents a Save All or save-before-launch batch writes | +| `document.reduceSaveBatch` | Reduce save batch member outcomes and live documents into one batch status | +| `history.record` | Store a versioned text snapshot and metadata | +| `history.entries` | List valid history entries for one file or a workspace | +| `history.content` | Read a stored history snapshot by relative storage path | +| `history.relocate` | Move retained history for a file or directory subtree after a rename or move | +| `history.planEvent` | Plan which snapshots an editing event (save, auto-save, restore, manual, discarded unsaved text, external change, before rename, before batch replace) records and under which reason | +| `history.rename` | Set or clear a user-visible label on a history entry | +| `history.delete` | Delete one history entry and its snapshot | +| `history.ignorePreview` | Validate local history ignore rules and report which rule skips a sample path | +| `history.usage` | Report one project's allocated history size and the storage cap | +| `history.clear` | Delete every snapshot of one project's history | +| `diff.compareTexts` | Compare two host-supplied texts line by line into paired side-by-side rows and unified lines | +| `execution.resolveEnvironment` | Merge native `inherited` string values and `overrides` into `{ environment }`; string overrides win. Core names no tool: a tool's runtime-home variable reaches a launch through the selected toolchain installation (`toolchains.*`), which the host applies before this merge, or as a literal override from the plugin that owns the toolchain. A structured `{ toolchain, property }` value the host could not resolve to text has no defined value and contributes nothing. `caseInsensitive: true` canonicalizes Windows keys to uppercase | +| `execution.resolveSearchPath` | Preserve existing path precedence, sort platform candidates by priority and identity, and deduplicate using the requested case policy | +| `processLifecycle.resolve` | Decide how each process or resource of one Lithe instance ends | +| `processLifecycle.inventory` | Group a process snapshot of one Lithe instance into terminals, Agent tasks and background services | +| `processLifecycle.settle` | Turn confirmation answers into what ends, what is kept and the updated settings | +| `markdown.render` | Render and sanitizes shared Markdown | +| `keymap.parseContext` | Compile a bounded boolean shortcut context | +| `keymap.evaluateContext` | Evaluate a precompiled shortcut context | +| `keymap.analyzeConflicts` | Find conflicts among the final layered shortcut bindings | +| `keymap.modifierGestureStep` | Advance the double-Shift gesture recognizer by one event. The rule itself lives in `lithe-core-rules` so the Windows frontend can run it synchronously | +| `editor.workbenchState.reduce` | Apply one editor-group state transition: open, close, move, pin, preview, merge or normalize. The rule lives in `lithe-core-rules` so the Windows frontend can run it synchronously inside store updates | +| `workspace.classifyVisibility` | Classify a batch of workspace-relative paths as hidden or visible. The rule lives in `lithe-core-rules` so the Windows frontend can run it synchronously | +| `workspace.visibilityDefaults` | Return the built-in hidden-path lists (from `lithe-core-rules`) | +| `workbench.chromeLayout.reduce` | Apply one transition to the rearrangeable workbench chrome: stripe, title bar and status bar button placement and open tool panels (from `lithe-core-rules`) | +| `terminal.parseLinkTarget` | Resolve terminal link candidates into file or external targets | +| `debug.createSession` | Create a transport-neutral DAP session and return its initialize frame | +| `debug.launch` | Queue a launch or attach request, including during initialization | +| `debug.steppingFilters` | Return adapter defaults or normalize portable stepping filters | +| `debug.relocateBreakpoints` | Move source breakpoints across one exact UTF-16 editor replacement | +| `debug.planVariablePage` | Plan the first bounded `variables` page of one scope or structured variable from its reported counts | +| `debug.reduceVariablePage` | Merge one returned variable page into its cursor and decide whether the container is exhausted | +| `debug.planDisassemblyWindow` | Plan the first `disassemble` request of the window around the instruction pointer or one listed edge | +| `debug.reduceDisassemblyPage` | Reduce one `disassemble` reply to the next bounded recovery request, a deduplicated page, or the adapter's failure | +| `debug.setBreakpoints` | Replace and deterministically order one source's DAP breakpoints | +| `debug.setExceptionBreakpoints` | Replace and deterministically order one session's exception filters | +| `debug.setFunctionBreakpoints` | Replace and deterministically order one session's named function breakpoints | +| `debug.setInstructionBreakpoints` | Replace and deterministically order one session's machine-instruction breakpoints | +| `debug.dataBreakpointInfo` | Resolve an adapter-owned data breakpoint identity for a paused variable or field | +| `debug.setDataBreakpoints` | Replace and deterministically order one session's resolved data breakpoints | +| `debug.setBreakpointsMuted` | Mute or unmutes every breakpoint of one debug session | +| `debug.planMemoryWindow` | Place the memory view's readMemory window at an address | +| `debug.locateMemoryWindow` | Find the byte read at in one readMemory reply | +| `debug.setVariable` | Replace one visible variable value in its adapter-owned parent container | +| `debug.cancelOperation` | Cancel or time out one pending operation and ignore its late response | +| `debug.execute` | Submit continue, pause, next, step-in, or step-out control | +| `debug.inspect` | Request normalized threads, frames, scopes, variables, or evaluation | +| `debug.receive` | Reduce base64-encoded bytes received from a platform-owned DAP transport | +| `debug.runInTerminalResponse` | Complete one adapter-requested native terminal launch | +| `debug.startDebuggingResponse` | Complete one adapter-requested child session start | +| `debug.disconnect` | Begin the DAP disconnect handshake without closing the native transport | +| `debug.destroySession` | Remove a session and its whole child tree after the platform closes its native transport; the response lists `destroyedChildSessionIds` (leaves first) whose transports the platform must also close | +| `lsp.applyTextEdits` | Apply LSP UTF-16 text edits with range validation | +| `lsp.planWorkspaceEdit` | Validate language-server workspace edit targets against the workspace and open documents, and order the commit | +| `lsp.documentIdentity` | Resolve a diagnostic URI or path strictly inside its workspace | +| `lsp.classifyNavigationTarget` | Classify a language-server jump target as workspace, external read-only or rejected | +| `lsp.declarationOrUsages` | Decide whether a go-to-declaration gesture opens the declaration or shows its usages | +| `editor.lineEdit` | Apply a deterministic line-level text transform and return the replacement plus selection to restore | +| `editor.find` | Find text or plans bounded replacements in a borrowed document | +| `editor.preferences.resolve` | Resolve persisted editor presentation preferences | +| `editor.recent.reduce` | Reduce and queries bounded recent-file history | +| `editor.recovery.decide` | Decide how a native checkpoint should recover | +| `keymap.schemes.reduce` | Reduce named shortcut schemes | +| `editor.planTextEdits` | Validate and order a plugin document edit batch into UTF-16 offsets with the document's line ending | +| `editor.parseNavigationInput` | Parse a bounded line or line:column input into zero-based editor coordinates | +| `editor.navigationHistory.reduce` | Reduce one bounded back/forward navigation history event and return the next state plus the location to open | +| `editor.lineCommentToken` | Resolve the line comment token for a file extension or language id | +| `network.proxyPolicy` | Normalize the stored proxy settings into the policy a network stack must apply (mode, server, credentials, exclusions) | +| `lsp.plainSnippet` | Convert LSP snippet insert text into plain editor text | +| `lsp.builtinCompletions` | Return lightweight current-file identifier completions | +| `lsp.builtinHover` | Return lightweight current-symbol hover text | +| `lsp.builtinNavigation` | Return lightweight current-file definition/reference locations | +| `lsp.mergeCompletions` | Choose which candidates survive when several prioritized providers complete at one position | +| `lsp.activationPolicy` | Decide whether a language server may start automatically after failures, using a platform monotonic clock | +| `lsp.problems.query` | Filter, order, and group Problems tool-window diagnostics and count them by severity | +| `lsp.startServer` | Start one Rust-owned process/session and begin initialization | +| `lsp.stopServer` | Gracefully shut down a session, with a bounded force-stop fallback | +| `lsp.updateWorkspaceConfiguration` | Replace the plugin configuration a running plugin-owned session answers `workspace/configuration` with, and announce it | +| `lsp.documentUri` | Convert a native absolute path into a canonical file URI | +| `lsp.syncDocument` | Open a document or apply a full-text or incremental `didChange` with monotonic versions | +| `lsp.saveDocument` | Report a confirmed disk save with its exact saved text, honoring server save options | +| `lsp.workspaceFilesChanged` | Publish normalized created, changed, or deleted workspace files to one session | +| `lsp.closeDocument` | Close a document and clear its diagnostics | +| `lsp.request` | Submit a typed semantic request and return an opaque operation ID | +| `lsp.buildRequest` | Forward one planned `lithe/build/*` request to a session that negotiated the build-provider protocol | +| `lsp.vcsRequest` | Send one VCS operation, planned by Core, to a session that negotiated the VCS-provider protocol | +| `lsp.toolchainRequest` | Send one `toolchains/query` to a plugin backend that negotiated the provider | +| `lsp.keymapImportRequest` | Send one shortcut import request to a negotiated plugin provider | +| `lsp.functionRequest` | Invoke one registered plugin function on a session its plugin started | +| `lsp.functionProvider` | Select a registered function provider strictly inside one workspace | +| `lsp.configureFunctionRelay` | Replace the enabled plugins and live workspace contexts Core's plugin-to-plugin function relay checks calls against | +| `lsp.hostResponse` | Answer one host-service request a plugin backend sent | +| `lsp.hostNotify` | Send one host notification to a plugin backend that negotiated it | +| `lsp.pluginServerRequest` | Send one request of a plugin backend to the plugin's own language server, under the plugin's declared session policy | +| `lsp.pluginServerNotify` | Send one notification of a plugin backend to the plugin's own language server | +| `lsp.cancelOperation` | Cancel one pending semantic operation | +| `lsp.pollEvents` | Drain ordered typed lifecycle/feature/diagnostic/result/log events | +| `lsp.setDetailedDiagnostics` | Toggle upstream verbose trace for one session | +| `lsp.waitEvents` | Block until queued events exist or a timeout elapses, then drain them | +| `lsp.destroyServer` | Remove a terminal session handle from the registry | +| `runConfig.inspect` | Inspect `.lithe` run documents, versions, and staleness without writing files | +| `runConfig.projectToolchain` | Read saved project tool paths for one Provider root, with the nearest path override | +| `runConfig.generate` | Regenerate detected run configurations | +| `runConfig.argumentText` | Format a batch of exact argument vectors as reversible editor text | +| `runConfig.resolve` | Merge generated, project, and local layers and return diagnostics | +| `runConfig.selectContextConfiguration` | Choose a matching effective configuration for a provider-supplied source and entry identity | +| `runConfig.decideTitleAction` | Decide what a press on the title bar's Run or Debug button does | +| `runConfig.updateOptions` | Apply typed option edits and return an updated project or local document | +| `runConfig.updateToolPreferences` | Replace only generic preferences in a captured local Run document, preserving legacy defaults and unrelated metadata | +| `runConfig.saveEditorChanges` | Prepare the local and optional project documents for one editor save | +| `runConfig.createUserConfiguration` | Validate a typed user configuration and return an updated document | +| `runConfig.createLaunchPlan` | Project one effective configuration into a platform-neutral Run or Debug plan | +| `runConfig.providerLaunchInputs` | Resolve user choices for a provider-owned launch | +| `git.status` | Resolve the repository, current branch, and working-tree changes | +| `git.untrackedDirectory` | List one level of a summarized untracked directory: its files and subdirectory summaries | +| `git.commitState` | Read exact HEAD, symbolic branch and index preconditions for a workspace commit | +| `git.workspaceCommitPrepare` | Build a reviewed multi-repository plan | +| `git.workspaceCommitStep` | Execute one guarded workspace commit/push | +| `git.classifyMetadataChanges` | Classify a bounded page of coalesced watcher paths without filesystem access | +| `git.watchContext` | Resolve the repository and absolute Git metadata roots needed by native file watchers | +| `git.worktrees` | Return deterministic registered-worktree metadata without scanning each checkout | +| `git.pullRequestContext` | Resolve worktree-aware PR branch defaults, publication state, and uncommitted-change state | +| `git.command` | Execute one argument-based Git operation and return its arguments, streams, exit code, and ordered subprocess invocations | +| `git.repositoryRoot` | Resolve the repository root containing a workspace path without acquiring repository write coordination | +| `git.resolveFileContext` | Resolve a file to its owning Git checkout and relative path | +| `git.workspaceStatusMerge` | Plan publication of fresh, retained, and unavailable repository snapshots | +| `git.write` | Validate and execute shared Git mutations such as stage, commit, branch, checkout, remote sync, clone, and stash | +| `git.rollbackPlan` | Plan whole-file rollback from explicit Git status identities without reading or changing a repository | +| `git.rollbackPrepare` | Recheck selected identities against the repository and return rollback steps | +| `git.prepareCommitSelection` | Prepare and verify the real indexes for explicitly selected changes | +| `git.reconcileDeletedSelection` | Clear missing selected staged additions after filesystem deletion | +| `git.changelists` | Reduce local ChangeList metadata and project membership and commit scope | +| `git.shelf` | Capture or applies one guarded Shelf layer | +| `git.commitMessageContext` | Capture bounded AI commit evidence and recheck its Git state | +| `git.consolePresentation` | Pure IDEA-style configuration/progress folds, empty-output notices and search ranges over retained diagnostic snapshots | +| `git.remoteUrl` | Read a configured remote URL silently; a missing remote returns a null URL | +| `git.fetchPlan` | Validate Fetch choices; optionally inspect a repository to expand enabled per-remote commands | +| `git.executable` | Resolve the executable Git will run without requiring a repository | +| `git.executionInspect` | Inspect executable capabilities, configuration provenance and effective Fetch preferences | +| `git.executionConfigure` | Explicitly save or clear one allowlisted value in a selected config scope | +| `git.authRespond` | Answer or cancel a live authentication challenge once | +| `remote.probe` | Check that an SSH connection reaches a supported host | +| `remote.authRespond` | Answer or cancel a prompt relayed by `remote.probe` or `remote.connect` once | +| `remote.connect` | Open a connection, installing the shipped server when needed | +| `remote.readDir` | List a directory over an open remote connection | +| `remote.disconnect` | Close an open connection and its OpenSSH process tree | +| `remote.closeAll` | Close every open connection; the host calls it at application exit | +| `remote.readFile` | Read a file over an open remote connection | +| `remote.writeFile` | Save a file over an open remote connection | +| `remote.hostRequest` | Perform a non-streaming operation on an existing workspace host | +| `remote.runProcess` | Own a remote process and relays output until it exits or is cancelled | +| `remote.captureProcess` | Capture a short remote process with explicit time and output budgets | +| `remote.runTerminal` | Own a remote PTY and relays output until it exits or is cancelled | +| `remote.runWatch` | Own a host watch and relays changes until cancellation or root removal | +| `remote.uploadFile` | Stream a native source into integrity-checked host file staging | +| `remote.hostInfo` | Return cached handshake capabilities of one connection | +| `git.historyRewritePreview` | Review undo, message edit, squash, or drop with complete messages, eligibility, and an immutable checkout expectation | +| `git.remotes` | Return configured fetch remote names and URLs | +| `git.tags` | Return tag names, tag object IDs, target IDs, dates, and annotations | +| `git.parseCheckoutMetadata` | Interpret bounded HEAD text without filesystem access | +| `git.parseGitdirPointer` | Interpret a bounded gitdir pointer without filesystem access | +| `git.historyActionCapabilities` | Read history menu hints from actual HEAD and the bounded first-parent chain | +| `git.rebasePreview` | Resolve the complete local linear range strictly after a selected unchanged base | +| `git.repositorySetup` | Inspect repository/unborn state and scoped/effective commit identity; see `git-repository-setup.md` | +| `git.initialize` | Initialize a directory outside existing repositories without staging or committing | +| `git.configureIdentity` | Save or clear one local/global `user.name` or `user.email` override | +| `git.rebaseStart` | Start a reviewed native interactive rebase with persisted messages and recovery identity | +| `git.rebaseSession` | Read the latest owned rebase session and distinguish edit/conflict pauses from completion | +| `git.rebaseControl` | Continue, skip, or abort an identified session, optionally amending an edit pause | +| `git.patchExport` | Export a lossless UTF-8 exchange patch | +| `git.patchPreview` | Review a patch with a forward applicability check | +| `git.patchApply` | Apply a patch tied to its reviewed snapshot | +| `git.diff` | Produce a structured working-tree, index, reference, commit, or stash patch | +| `git.parsePatch` | Parse a complete saved patch into Core-owned per-file documents | +| `git.apply` | Apply or check a patch in `stage`, `unstage`, `discard`, or Shelf restore mode | +| `git.planDiffBlocks` | Plan commit-review blocks for one file; Core owns every block patch | +| `git.applyDiffBlock` | Include, exclude or roll back one planned block under the write lease | +| `git.stageGutterChange` | Stage one editor gutter change with its original bytes | +| `git.history` | Return the legacy combined reference snapshot and first bounded commit page | +| `git.historyQuery` | Compare bounded commit date identities with explicit absolute time bounds | +| `git.references` | Return deterministic refs, recent local branches, ahead/behind state, and effective Git identity without scanning commit history | +| `git.historyPage` | Return one bounded commit page, author date, UTC offset, optional Unix timestamp, and an opaque continuation cursor | +| `git.historyCursorClose` | Release an unfinished incremental history cursor and its Git process | +| `git.historyLocate` | Append bounded history pages until one commit is found and report the budget, window cursor, and exhaustion | +| `git.branchNamePreview` | Clean and validate a new branch name against already loaded references without running Git | +| `git.repositoryColorAssignments` | Assign color slots to a bounded page of ordered repository identities | +| `git.updatePlan` | Route a branch Update and gate a Pull from facts the host collected: blockers, strategy default, whether to ask, and the expected checkout | +| `git.historyWindow` | Plan a history refresh window, page merge, and selection retention over commit identities | +| `git.graphProjection` | Project a loaded history window into commit-graph lanes, color IDs, routes, print elements, and labels, optionally filtered | +| `git.normalizeChangeTargets` | Drop targets already covered by a selected untracked summary | +| `git.rebasePlanPreview` | Validate an interactive rebase draft and derives its messages | +| `git.referenceTree` | Group one kind of loaded references into an ordered tree | +| `git.pushPreview` | Resolve a local branch push destination and the bounded commits not present on that remote base | +| `git.commit` | Return one structured commit by revision with its full message body | +| `git.commitFiles` | Return files changed by one commit | +| `git.comparison` | Return files changed between a reference and the working tree | +| `git.stashes` | Return structured stash references and messages | +| `git.checkoutPreflight` | Return local paths that would block switching to a reference | +| `git.pullPreflight` | Report the configured upstream, ahead/behind counts, divergence, tracked local changes, and the strategy Git is configured for, without fetching | +| `git.integrationPreflight` | Return local paths that block a merge, rebase, cherry-pick, or revert | +| `git.conflictMarkers` | Return staged text files that still contain conflict markers | +| `git.operationState` | Report an interrupted merge, rebase, cherry-pick, or revert and its conflicted paths | +| `git.blame` | Return structured line blame metadata | +| `git.lineHistory` | Trace which commits touched a one-based snapshot line range of one file | +| `github.parseRemote` | Parse a canonical GitHub HTTPS or SSH remote into owner/name | +| `github.requestPlan` | Validate one GitHub operation and produce a trusted platform HTTP request plan | +| `github.normalizeResponse` | Normalize raw GitHub JSON and HTTP status into deterministic data or a stable error | +| `github.deviceAuthorizationTransition` | Plan the next device-authorization action, polling interval, and deadline-bounded wait | +| `github.pullRequestCheckoutPlan` | Plan a PR's safe local branch and report an existing-ref conflict | +| `git.pullRequestCheckoutPreview` | Inspect and review a non-destructive PR checkout plan | +| `git.pullRequestCheckout` | Recheck and execute a reviewed PR checkout without resetting an existing branch | +| `diagnostics.redactText` | Redact credentials, tokens, and home-directory paths from diagnostic-bundle text | +| `diagnostics.prepareLogEvent` | Normalize a bounded application event and recursively redact structured fields | +| `diagnostics.queryLogs` | Merge and filter bounded host-collected module log tails | +| `diagnostics.recordPluginOperation` | Record host-observed declarative contribution outcomes | +| `diagnostics.drainCoreEvents` | Atomically take process-owned Core log facts and their dropped count | +| `diagnostics.configureCoreEvents` | Set the DEBUG admission gate for Core log facts from host detailed-diagnostics state | +| `diagnostics.encodeLogBatch` | Encode independent structured v2 log events with per-item errors | +| `diagnostics.parseLogFilter` | Parse bounded filter expressions and UTF-16 diagnostic ranges | +| `diagnostics.composeLogFilter` | Compose a filter predicate preserving unrelated terms | +| `diagnostics.formatLogRecords` | Validate and format canonical redacted records for copying | +| `diagnostics.queryLogsV2` | Query structured v2 and legacy v1 native log tails | +| `diagnostics.planLogCollection` | Plan bounded native log-tail reads from raw file metadata | +| `diagnostics.planLogRetention` | Plan log cleanup from host-owned file inventories | +| `diagnostics.resolveLogChannel` | Validate channels and resolve safe session-relative destinations | +| `diagnostics.resolveEngineLogSubject` | Resolve the same routing segment and channel used by engine startup, without I/O | +| `diagnostics.buildManifest` | Shape a deterministic diagnostic bundle manifest from host-gathered environment and file facts | + Workspace paths in responses are relative and use `/` separators. Line numbers are one-based. `git.status.repositoryRoot` may be an absolute path when the opened workspace is a subdirectory of the repository; all Git change paths are @@ -3327,6 +3709,27 @@ natively; this command only shapes and sorts them. The additive top-level `redaction:"core-v2"` identifies the shared redaction boundary; schemaVersion remains 1. Windows consumes the original fields and may ignore this marker. +### AI configuration + +`ai.configuration.sources` takes `{source, home, environment}` (`source` is +`codex` or `claude`, `home` an absolute path, `environment` only the directory +overrides `CODEX_HOME` / `CLAUDE_CONFIG_DIR`) and returns `{files}`: the native +files `{file, path}` the platform should read. `ai.configuration.parse` takes +`{source, files, environment, allowIncomplete}` with the file texts keyed +`config`, `auth`, `settings`, `credentials` and `root`, each at most 1 MiB, and +returns `null` when nothing is configured, otherwise `{model, provider, issue, +reasoningEffort, hasCredential, credential, unsupportedAuthentication}`. The +response never contains a secret: `credential` is a locator +(`{kind: "environment", name}` or `{kind: "file", file, path}`) that the host +resolves natively against the same text or environment. Malformed input returns +`invalid_request` with a fixed code (`AI_COMMIT_INVALID_CONFIG`, +`AI_CONFIGURATION_INVALID_AUTHENTICATION`, `AI_CONFIGURATION_SMART_QUOTES`, +`AI_CONFIGURATION_TOO_LARGE`) and never echoes input. `allowIncomplete: true` is +for editor drafts: it ignores the environment, accepts blank or partial text and +keeps missing endpoint/model fields empty. The shared cases are in +`rust/fixtures/ai/cli-configuration-v1.json`. It replaces the former +`agent.parseProviderConfiguration`. + ### Declarative plugins The `plugins.declarative.*` commands implement @@ -3772,6 +4175,8 @@ and the directories are absolute host paths used only as substitution text. | --- | --- | --- | | `build.detect` | `{ workspaceRoot, plugins: [{ manifest }], options?: { : { : value } }, instanceOptions?: { : { : { : value } } }, instanceDefaults?: { : { : value } }, emptySelections?: { : { : [optionID] } } }` | `detection`: `{ instances, rejected: [{ index, error }], shadowed: [{ pluginID, buildSystemID, by }] }`; each instance is `{ id, pluginID, buildSystemID, displayName, root, markers, modelSource, optionFingerprint, options, droppedOptions }` plus `model` or `modelError` for `static` and `hostIntegration` sources; a `provider` source has neither. `options` contains accepted stored values after Core scope/root merging, including explicit empty selections; `droppedOptions` names rejected stored values without rewriting the source document. | | `build.projectOptions` | `{ document }`, host-read persisted v1 Build options, without a manifest | `document`: validated storage with version 1 and systems; options, instance defaults, root values, explicit empty selections and saved custom choices are retained. Missing system fields normalize to empty maps; malformed, unsupported or oversized documents reject with `plugin.build.invalidOptionsDocument`. This pure operation never reads or writes a file and does not replace declared option resolution. | +| `build.resolveStoredOptions` | `{ document, local: { version: 1, document, emptySelections?: { : { : [optionID] } } } }`, without a manifest | `storage: { document, local, emptySelections }`: checked team/local merge; local values override matching keys, unrelated systems/roots/choices stay intact. No filesystem I/O. Local documents are machine/workspace preferences, not team files or plugin-native configuration. Their storage syntax is [build-local-options-v1.schema.json](build-local-options-v1.schema.json), with additional cross-field membership and byte limits checked by Core. | +| `build.editStoredOption` | `{ document, local, root, optionID, value, modelValues? }` in the standard manifest/build-system envelope | `edit: { local, options }`: changes one declared local preference. `null` removes the local override and restores team/declaration inheritance. Invalid or partially accepted edits reject before persistence. Workspace multi-selections preserve sibling-only values; explicit per-root empty markers apply only to workspace multi-select declarations. Platforms compare both native source revisions, atomically write only the local document and read back. | | `build.invocationPlan` | `{ root, platform: "macos" \| "linux" \| "windows" }` | `invocation`: `{ buildSystemID, root, executables, environmentInputs, runtimeDependencies, version, minimumVersion }` with project-file paths joined to `root`; every runtime dependency with a known probe, host-managed ones included, carries `version: { arguments }` | | `build.checkToolVersion` | `{ runtime?, output }` | `version`: `{ satisfied, version? }`; `runtime` checks that runtime dependency instead of the tool | | `build.resolveModelInputs` | `{revision, sources: [{manifest, instances: [{buildSystemID, root, state, model?, options?}]}]}` from installed enabled current-workspace sources | `modelInputs`: version 1 public snapshot with exact optional producer slots, state, normalized projects/targets/content roots, configuration fingerprint and explicitly shared options; no tasks/run/settings/private functions; see the [plugin contract](https://github.com/1lck/Lithe-IDEA/blob/preview/shared/plugins/contract/v1/README.md#optional-read-only-model-inputs) | diff --git a/rust/fixtures/i18n/format-v1.json b/rust/fixtures/i18n/format-v1.json deleted file mode 100644 index 26a36794..00000000 --- a/rust/fixtures/i18n/format-v1.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "schemaVersion": 1, - "parameters": { "name": "string", "count": "integer" }, - "arguments": ["name", "count"], - "cases": [ - { - "name": "Reordered parameters and literal percent", - "template": "100% {count} 个:{name}", - "nativeTemplate": "100%% %2$lld 个:%1$@", - "stringValue": "中文/%@/{count}", - "integerValue": 9000000000, - "expected": "100% 9000000000 个:中文/%@/{count}" - }, - { - "name": "Repeated parameter and zero count", - "template": "{name}: {count} files ({name})", - "nativeTemplate": "%1$@: %2$lld files (%1$@)", - "stringValue": "feature/100%", - "integerValue": 0, - "expected": "feature/100%: 0 files (feature/100%)" - } - ] -} diff --git a/rust/fixtures/i18n/native-abi-v1.json b/rust/fixtures/i18n/native-abi-v1.json deleted file mode 100644 index 75991068..00000000 --- a/rust/fixtures/i18n/native-abi-v1.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "schemaVersion": 1, - "parameters": { "rows": "unsignedInteger", "elapsed": "integer" }, - "macos": { "formats": { "rows": "llu", "elapsed": "d" } }, - "arguments": ["rows", "elapsed"], - "template": "{rows} rows affected in {elapsed} ms", - "nativeTemplate": "%1$llu rows affected in %2$d ms", - "unsignedValue": "18446744073709551615", - "integerValue": 2147483647, - "expected": "18446744073709551615 rows affected in 2147483647 ms" -} diff --git a/rust/fixtures/phpunit-project/.gitignore b/rust/fixtures/phpunit-project/.gitignore deleted file mode 100644 index c84ab0ca..00000000 --- a/rust/fixtures/phpunit-project/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/vendor/ -/composer.lock -/.phpunit.result.cache diff --git a/rust/fixtures/phpunit-project/composer.json b/rust/fixtures/phpunit-project/composer.json deleted file mode 100644 index d7667b3f..00000000 --- a/rust/fixtures/phpunit-project/composer.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "lithe/phpunit-fixture", - "require-dev": { "phpunit/phpunit": "9.6.22" }, - "scripts": { "test": ["@php vendor/bin/phpunit"] } -} diff --git a/rust/fixtures/phpunit-project/phpunit.xml b/rust/fixtures/phpunit-project/phpunit.xml deleted file mode 100644 index ddf7f01b..00000000 --- a/rust/fixtures/phpunit-project/phpunit.xml +++ /dev/null @@ -1 +0,0 @@ -tests diff --git a/rust/fixtures/phpunit-project/src/Store.php b/rust/fixtures/phpunit-project/src/Store.php deleted file mode 100644 index 1f61a213..00000000 --- a/rust/fixtures/phpunit-project/src/Store.php +++ /dev/null @@ -1,6 +0,0 @@ -values[] = $value; } - public function values(): array { return $this->values; } -} diff --git a/rust/fixtures/phpunit-project/tests/StoreTest.php b/rust/fixtures/phpunit-project/tests/StoreTest.php deleted file mode 100644 index 63a5e973..00000000 --- a/rust/fixtures/phpunit-project/tests/StoreTest.php +++ /dev/null @@ -1,12 +0,0 @@ -add('a'); $store->add('b'); - self::assertSame(['a', 'b'], $store->values()); - } - public function testKeepsInsertionOrderWithDuplicates(): void { - $store = new Store(); $store->add('a'); $store->add('a'); - self::assertSame(['a', 'a'], $store->values()); - } -} diff --git a/rust/lithe-core/src/protocol/command.rs b/rust/lithe-core/src/protocol/command.rs index d261453f..02619e61 100644 --- a/rust/lithe-core/src/protocol/command.rs +++ b/rust/lithe-core/src/protocol/command.rs @@ -1223,6 +1223,78 @@ impl CoreCommand { #[cfg(test)] mod tests { use super::CoreCommand; + use std::collections::BTreeSet; + use std::path::Path; + + /// Returns the command names that `CoreCommand::parse` matches, read from + /// this file so the test cannot drift from the parser it checks. + fn parsed_command_names(source: &str) -> Vec { + let body = source + .split_once("pub fn parse(value: &str) -> Option {") + .and_then(|(_, rest)| rest.split_once("_ => None,")) + .map(|(body, _)| body) + .expect("CoreCommand::parse body"); + body.lines() + .filter_map(|line| line.trim().strip_prefix('"')) + .filter_map(|line| line.split_once("\" =>")) + .map(|(name, _)| name.to_owned()) + .collect() + } + + /// Returns the first-column names of the `| Command | Purpose |` table. + fn documented_command_names(document: &str) -> Vec { + document + .lines() + .skip_while(|line| !line.starts_with("| Command | Purpose |")) + .skip(2) + .take_while(|line| line.starts_with("| `")) + .map(|line| { + line["| `".len()..] + .split_once('`') + .expect("backticked command name") + .0 + .to_owned() + }) + .collect() + } + + #[test] + fn api_command_table_lists_exactly_the_parsed_commands() { + let crate_root = Path::new(env!("CARGO_MANIFEST_DIR")); + let source = std::fs::read_to_string(crate_root.join("src/protocol/command.rs")) + .expect("read command.rs"); + let document = std::fs::read_to_string(crate_root.join("../contracts/rust-core-api.md")) + .expect("read rust-core-api.md"); + let parsed = parsed_command_names(&source); + let documented = documented_command_names(&document); + + assert!( + parsed.len() > 300, + "parser scan found {} names", + parsed.len() + ); + for name in &parsed { + assert!( + CoreCommand::parse(name).is_some(), + "scanned {name} does not parse" + ); + } + let unique: BTreeSet<_> = documented.iter().collect(); + assert_eq!( + unique.len(), + documented.len(), + "duplicate rows in the command table" + ); + + let parsed: BTreeSet<_> = parsed.iter().collect(); + let undocumented: Vec<_> = parsed.difference(&unique).collect(); + let unknown: Vec<_> = unique.difference(&parsed).collect(); + assert!( + undocumented.is_empty() && unknown.is_empty(), + "rust-core-api.md command table is out of date: missing {undocumented:?}, \ + not parsed {unknown:?}" + ); + } #[test] fn parses_semantic_lsp_runtime_commands() {