Skip to content

feat(lsp,language): wire provider-driven rename through product surfaces - #498

Open
e54-bot wants to merge 7 commits into
mainfrom
feat/156-provider-mutation
Open

e54-bot wants to merge 7 commits into
mainfrom
feat/156-provider-mutation

Conversation

@e54-bot

@e54-bot e54-bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Wires provider-owned mutation (#156) into the two remaining product surfaces, over the shared driver path:

  • textDocument/rename in wright-lsp — routes through LanguageService::rename, which runs semantic_rename over the open document set via the session's provider flow: provider-computed edits → Wright version/precondition checks → lpp/validateEdits → edited-project recheck → versioned WorkspaceEdit.documentChanges. Refusals (unopen/unsupported documents, unconfigured providers, provider and validation refusals) answer RequestFailed (-32803) with the structured code preserved in error.data.code. --opy-provider <PATH> pins an explicit provider executable.
  • Freshness contract — applied renames return versioned documentChanges: each TextDocumentEdit carries the document version the provider computed and Wright re-validated, so a client that advanced its buffer while the rename was pending can reject the edit. renameProvider is advertised only when the client negotiates workspace.workspaceEdit.documentChanges; without it a rename request is refused (rename-unversioned-workspace-edit) rather than served an unversioned changes map a stale buffer could silently absorb.
  • Project-scoped post-edit check — the request document set stays language-wide (only the provider can compute project membership), but the post-edit check verdict is scoped to the documents the provider actually edited plus the position document, so an unrelated open document cannot block a valid rename. validate_transaction keeps the whole caller set as the declared project.
  • MCP tool surface — providerSemanticRename/providerValidateEdit are listed as wright_provider_semantic_rename/wright_provider_validate_edit (they were reachable via serve's generic request op but invisible to MCP agents).
  • Shared path — run_provider_flow moves from ToolService to CompilerSession so CLI/service and language surfaces use one provider lifecycle (spawn → initialize → flow → graceful shutdown). LanguageService::with_config is the configuration seam for provider executables/registries.
  • No textual search/replace anywhere: every non-applied outcome is an explicit structured refusal.

Evidence

  • cargo fmt --all -- --check, cargo clippy --workspace --all-targets --all-features -- -D warnings, cargo test --workspace --all-targets --all-features (with WRIGHT_OPY_PROVIDER and LPP_MOCK_PROVIDER set): clean.
  • Live LSP e2e against the built opy-provider: textDocument/rename on scoreBank in defs.opy returned a cross-document WorkspaceEdit covering declaration + 3 references across two files (UTF-16 correct, tab-indented line handled); a rename on a non-symbol position returned the provider's structured rename.noSymbolAtPosition refusal.
  • WRIGHT_OPY_PROVIDER-gated regressions: rename_returns_versioned_document_changes_scoped_to_the_project sends a rename, changes the buffer to v2 before reading the response, and asserts versioned documentChanges carrying v1 alongside an unrelated broken #!include document open in the same language; an_unrelated_broken_open_document_does_not_block_a_project_rename pins the same scoping at the language-service boundary.
  • Driver regressions: rename_post_edit_check_drops_unrelated_open_document_errors and validate_transaction_check_scope_covers_the_whole_caller_set pin the check-scope asymmetry.
  • Live MCP e2e: wright serve --transport mcp → wright_provider_semantic_rename returned ok: true with the same 4-edit validated transaction.
  • Independent review round applied: projectRoot sent as a file URI, DocumentSet filtered to the target language, partial-edit drops made explicit refusals, --opy-provider requires its value.

Test plan

  • Workspace gates (fmt/clippy/test) green
  • Live rename through real provider via LSP and MCP
  • Refusal coverage at every gate (unopen, unsupported, unconfigured, provider refusal, unversioned client)
  • Versioned documentChanges + pending-buffer-change regression
  • Project-scoped post-edit check regression

textDocument/rename now runs the same provider-owned mutation path as the
CLI and agent surfaces: the provider computes edits over the open
document set, Wright verifies versions and source preconditions, the
provider validates the transaction, and the edited project is rechecked
before a WorkspaceEdit is returned. Unopen or unsupported documents,
unconfigured providers, and provider or validation refusals answer with
a RequestFailed error whose error.data.code carries the structured
refusal code; no textual fallback exists.

run_provider_flow moves from ToolService to CompilerSession so the
language service shares the one provider lifecycle path. wright-lsp
gains --opy-provider for an explicit provider executable, and the MCP
surface advertises providerSemanticRename/providerValidateEdit as
wright_provider_semantic_rename/wright_provider_validate_edit.

Closes #156
…params

edit_range re-implemented span_to_range's 1-based-char-col to UTF-16 conversion with a subtly different line split: splitting on a newline char keeps the carriage return while str::lines drops it, so a column at end-of-line on a CRLF file converted differently. Both converters now share document::line_col_position. parse_params unwrapped the params Option, so a request without params panicked the server loop; it now returns the same Err as malformed params.
parse_params returned Err for missing or malformed params, but every call site propagated it out of the dispatch loop, so a single bad request still ended the session without a wire response. read_params now answers requests with -32602 carrying the original id and skips malformed notifications (which get no response by spec), then the loop continues.
A textDocument/rename sent without an id is a notification: it gets no
response and no provider call.

@Teakowa Teakowa left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One reproducible stale-edit safety defect blocks acceptance. See the inline finding.

Comment thread crates/wright-lsp/src/main.rs

@Teakowa Teakowa left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two correctness blockers remain in the LSP rename path.

Comment thread crates/wright-language/src/service.rs
Comment thread crates/wright-lsp/src/main.rs Outdated
…t check

Review on #498 flagged two defects in the provider-driven rename:

- Applied renames returned WorkspaceEdit.changes, which cannot carry the
  document version the provider validated. A client whose buffer moved
  while the rename was pending would apply a stale edit without detecting
  it, violating the documented freshness contract. SourceTextEdit now
  carries the validated version, the LSP adapter answers with
  WorkspaceEdit.documentChanges (TextDocumentEdits tagged with
  OptionalVersionedTextDocumentIdentifier), and rename is only advertised
  and served when the client negotiates
  workspace.workspaceEdit.documentChanges; otherwise the request is
  refused (rename-unversioned-workspace-edit) rather than risk an
  unversioned edit.

- The post-edit check failed the mutation on error diagnostics from any
  supplied document, so an unrelated broken open document could block a
  valid target-project rename. The supplied set stays language-wide (only
  the provider can compute project membership), but the check verdict is
  now scoped to the documents the provider actually edited plus the
  position document; validate_transaction keeps the whole caller set
  because its document set is the declared project.

Regressions: driver-level scripted-provider tests pin the scope behavior,
and WRIGHT_OPY_PROVIDER-gated tests at the language-service and LSP
boundaries cover a real rename alongside an unrelated broken document,
versioned documentChanges, and a buffer change while the rename is
pending.
Teakowa
Teakowa previously requested changes Oct 4, 2026

@Teakowa Teakowa left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two previous correctness blockers are fixed. One verification blocker remains: the new real-provider regressions do not run in CI.

Comment thread crates/wright-lsp/tests/lsp.rs
Review on #498 noted the WRIGHT_OPY_PROVIDER-gated rename regressions self-skip in CI: Product integration installed the pinned OPY provider only after its test steps, and the reusable quality job has no provider at all.

The job now pins the provider version and its store in job-level env, installs before the test step that consumes it, and runs the wright-language/wright-lsp suites with WRIGHT_OPY_PROVIDER pointed at the pinned binary — a stale-edit or project-scope regression now fails the job instead of reporting success via early return.
…regressions

The previous step pointed the WRIGHT_OPY_PROVIDER-gated tests at the released 0.1.38 distribution, which predates lpp/rename and refused with capability-unavailable in CI. Following the LPP-integration pattern, the job now checks out the pinned opy-rs commit the tests were validated against and builds opy-provider from source; the pin advances through normal deps: bumps as the provider releases. The release-pin install and compile smoke check stay unchanged — they exercise the distribution path itself.
@Teakowa
Teakowa dismissed their stale review October 4, 2026 09:08

Retracted: requiring Wright CI to run these regressions against a pinned real OPY provider was the wrong verification boundary. Wright should verify its own LPP/edit/LSP contract hermetically; real OPY compatibility should track the owner implementation rather than a historical snapshot.

@Teakowa Teakowa left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correction to my previous review: do not make Wright's rename regressions depend on a pinned OPY implementation. Remove the new cross-repository snapshot coupling and keep Wright-owned behavior hermetically testable.

Comment thread .github/workflows/ci.yml
# so they build the pinned opy-rs commit the tests were validated
# against instead of self-skipping. The pin advances through normal
# `deps:` bumps as the provider releases.
- name: Checkout opy-rs

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new pinned opy-rs checkout is the wrong verification boundary. It freezes Wright's product regression against one historical owner implementation, so later OPY releases (including a future 1.0) can change without this gate exercising them. Remove this checkout/build/test coupling. Keep the Wright-owned guarantees deterministic inside Wright: the driver scoping regression already uses a scripted provider; cover the LSP versioned-documentChanges conversion/capability behavior with a hermetic contract/mock or a pure adapter test. Real OPY end-to-end compatibility should track the current owner implementation on the compatibility/owner surface, not a commit pinned into Wright CI.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

2 participants