Skip to content

feat: vera/proposeEdit enforced edit workflow (#222 Phase F1)#721

Merged
aallan merged 4 commits into
mainfrom
feat/222-phase-f1-propose-edit
Jun 11, 2026
Merged

feat: vera/proposeEdit enforced edit workflow (#222 Phase F1)#721
aallan merged 4 commits into
mainfrom
feat/222-phase-f1-propose-edit

Conversation

@aallan

@aallan aallan commented Jun 11, 2026

Copy link
Copy Markdown
Owner

Summary

Phase F1 of the issue 222 skill layer: vera/proposeEdit, the first enforced workflow method — the whole edit → verify → apply sequence runs server-side as one LSP request, so an agent cannot apply an unverified edit. Applying is the final step of verifying.

Per the Phase F design comment (2026-06-11):

  • New vera/lsp/workflows.pypropose_edit() is the pure decision (speculative verify via the Phase E machinery + the gate); apply_propose_edit() is the effectful orchestration. The gate: apply iff the proof delta has no newly_undischarged obligations AND the proposed state has no error diagnostics; force: true overrides both, loudly (the response still carries the full delta).
  • On apply: a workspace/applyEdit server→client request (the client owns the buffer — the server round-trips the edit rather than silently diverging), canonical DocumentStore update, re-analysis + diagnostics republish. The client's echoed didChange replays from the warm discharge cache — the pre-warming Phase E was designed for.
  • On refuse: canonical state untouched — same isolation guarantee as vera/speculativeEdit.
  • Unopened-URI edge: full-document replacement uses an exact end position computed from the document's line index (UTF-16 transcoded); for a URI the client never opened, the LSP clamp-sentinel range (clients clamp out-of-range positions to document end).
  • Handler registered in create_server(); wire glue only.

Verification gate semantics

Locking: the decision runs under analysis_lock (one Z3 session, strictly serialised); the apply path releases before analyze_and_publish re-acquires — threading.Lock is not reentrant, and the second pass replays from cache by construction.

Tests (12 new; 4,284 total)

  • Gate: clean edit applies; strengthening edit applies (newly_discharged must not block); breaking edit refused (violated nat_sub surfaced); non-compiling edit refused; force overrides the proof gate AND the error gate with the delta still reported.
  • Wiring (structural fake server — transport-free per the established pattern): apply round-trip pins the exact full-document range, store text/version, and republish; refuse touches no canonical state (store, analyses identity, no applyEdit); unopened-URI clamp sentinel.
  • full_document_range goldens: trailing-newline virtual line; UTF-16 end column on the astral-plane fixture.

Release prep (v0.0.166)

Version across all 6 tracked sites, CHANGELOG section + compare links, ROADMAP regains a Phase F row (issue 222 is open again and must be tracked; the F3 PR removes it), TESTING.md/README counts, module map (lsp/ 829 → 1007 lines), site assets regenerated.

Part of the Phase F arc (F1 → F2 strengthenContract → F3 addEffect); this PR intentionally does not affect the issue's open state.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added Phase F1: vera/proposeEdit — an enforced edit → verify → apply workflow with gating based on verification results and an explicit force override.
  • Documentation

    • Bumped project version to 0.0.166; updated changelog, README status, roadmap metrics and module map to reflect the new phase and test counts.
  • Tests

    • Expanded LSP test coverage with a new suite validating proposeEdit gating, apply behaviour, parameter handling and range/encoding edge cases.

The first skill-layer workflow: edit -> verify -> apply as one LSP
method. The proposed text runs through the Phase E speculative verify;
the gate applies iff the proof delta has no newly_undischarged
obligations and no error diagnostics (force: true overrides both,
loudly). On apply: workspace/applyEdit, canonical DocumentStore
update, diagnostics republish; the client's echoed didChange replays
from the warm discharge cache. On refuse, canonical state untouched.

New vera/lsp/workflows.py separates the pure decision (propose_edit)
from the effectful orchestration (apply_propose_edit); 12 new tests
(gate, force overrides, wiring against a structural fake server,
full-document-range goldens). Release prep for v0.0.166; ROADMAP
regains a Phase F row while the reopened issue is in flight.

Co-Authored-By: Claude <[email protected]>
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c504db93-3cf2-40f9-9882-f55292ab266a

📥 Commits

Reviewing files that changed from the base of the PR and between 876f54b and a87ac45.

📒 Files selected for processing (5)
  • README.md
  • ROADMAP.md
  • TESTING.md
  • tests/test_lsp.py
  • vera/lsp/server.py

📝 Walkthrough

Walkthrough

Phase F1 adds vera/proposeEdit: a server-enforced edit→verify→apply LSP workflow. It implements propose_edit (speculative gate), full_document_range (exact UTF-16 or sentinel), and apply_propose_edit (atomic apply under analysis_lock). Tests and docs updated; package version bumped to 0.0.166.

Changes

Phase F1 proposeEdit Skill-Layer Workflow

Layer / File(s) Summary
proposeEdit workflow implementation and contract
vera/lsp/workflows.py
propose_edit gates on proof-delta obligations and diagnostic count, full_document_range computes exact whole-document LSP ranges using cached UTF-16 line indexing and sentinel maximal ranges for unopened docs, and apply_propose_edit orchestrates the flow under analysis_lock with conditional workspace edits, document store mutations, and diagnostics republish.
LSP server feature handler wiring
vera/lsp/server.py
Phase F docs expanded; _param, _require_str, and _force_param helpers added; apply_propose_edit imported and wired as the vera/proposeEdit LSP handler; speculativeEdit parameter extraction unified to strict validation.
Phase F1 test suites for gate, wiring, and range logic
tests/test_lsp.py
Adds _FakeServer, TestProposeEditGate, TestProposeEditWiring, TestParamExtraction, and TestFullDocumentRange covering gating, force override, apply wiring (single full-document workspace edit), document-store/version updates, republish behaviour, param extraction edge-cases, and UTF-16 end-column goldens.
Release version and documentation updates
pyproject.toml, vera/__init__.py, CHANGELOG.md, README.md, ROADMAP.md, TESTING.md, vera/README.md
Version 0.0.165 → 0.0.166; changelog entry for Phase F1; project metrics updated (test count reported as 4,291); module map and testing docs updated to reflect Phase F coverage.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • aallan/vera#720: The Phase-E speculative-verification and proof-delta machinery (speculative_edit, proof_delta) that propose_edit extends.
  • aallan/vera#719: Adds earlier LSP server plumbing (VerificationSession, analysis_lock, diagnostics publish) used by apply_propose_edit.

Suggested labels

compiler, tests, docs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.16% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: vera/proposeEdit enforced edit workflow (#222 Phase F1)' directly and precisely summarises the main change: implementation of an enforced edit verification and application workflow for the vera/proposeEdit handler as part of Phase F1 of issue #222.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/222-phase-f1-propose-edit

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented Jun 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.90909% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.96%. Comparing base (d2e11ae) to head (a87ac45).

Files with missing lines Patch % Lines
vera/lsp/server.py 77.27% 5 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main     #721   +/-   ##
=======================================
  Coverage   90.95%   90.96%           
=======================================
  Files          69       70    +1     
  Lines       24676    24727   +51     
  Branches      292      292           
=======================================
+ Hits        22444    22492   +48     
- Misses       2225     2228    +3     
  Partials        7        7           
Flag Coverage Δ
javascript 61.40% <ø> (ø)
python 94.62% <90.90%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/test_lsp.py`:
- Around line 703-757: Add a failing-client branch in the tests by extending
_FakeServer.workspace_apply_edit to simulate applied=False and then add a test
case in TestProposeEditWiring that calls apply_propose_edit and asserts the
client-refusal path: ensure out["applied"] is False, server.applied_edits
remains empty, server.published stays [], the store (server.store.get(URI))
still contains SPEC_BASE with original version, and server.analyses[URI] is
unchanged; reference the existing apply_propose_edit call pattern used in
test_refuse_path_touches_nothing to mirror expectations for the client-declines
scenario.

In `@vera/lsp/server.py`:
- Around line 177-182: The crash occurs because "getattr(params, 'text', None)
or params.get('text')" evaluates params.get(...) even when params has no .get
and text == "" (falsy), causing AttributeError; update the uri and text
extraction to first check for .get and prefer params.get(...) with a fallback to
getattr, or if .get is missing use getattr only (e.g. if hasattr(params, 'get'):
uri = params.get('uri', getattr(params, 'uri', None)) else: uri =
getattr(params, 'uri', None); do the same for text), leaving the existing force
handling and the final call to apply_propose_edit(server, uri, text,
bool(force)) unchanged so apply_propose_edit always receives a proper str for
text.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: dd881936-7d9b-4738-bffc-cc51ac844787

📥 Commits

Reviewing files that changed from the base of the PR and between d2e11ae and bdcb8e2.

⛔ Files ignored due to path filters (5)
  • docs/index.html is excluded by !docs/**
  • docs/index.md is excluded by !docs/**
  • docs/llms-full.txt is excluded by !docs/**
  • docs/llms.txt is excluded by !docs/**
  • uv.lock is excluded by !**/*.lock, !uv.lock
📒 Files selected for processing (10)
  • CHANGELOG.md
  • README.md
  • ROADMAP.md
  • TESTING.md
  • pyproject.toml
  • tests/test_lsp.py
  • vera/README.md
  • vera/__init__.py
  • vera/lsp/server.py
  • vera/lsp/workflows.py

Comment thread tests/test_lsp.py
Comment thread vera/lsp/server.py Outdated
CodeRabbit flagged the getattr-or-get pattern in both custom handlers:
a falsy-but-present value (text="", a legitimate replace-with-empty-
document) fell through to .get on pygls' attribute-style params and
raised AttributeError. Both vera/proposeEdit and vera/speculativeEdit
now extract via _param(), which decides membership with a sentinel,
never truthiness; regression tests cover the SimpleNamespace
empty-text case.

Also pins the documented fire-and-forget applyEdit semantics: the
fake server now returns a resolved Future carrying the client's
verdict, and a client-refusal test asserts canonical state is NOT
rolled back (the response's applied reports the gate verdict; a
declining client re-converges on its next full-sync didChange).

Co-Authored-By: Claude <[email protected]>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@vera/lsp/server.py`:
- Line 197: The current assignment force = bool(_param(params, "force")) treats
strings like "false" as true; replace this with an explicit strict boolean parse
using the raw value from _param(params, "force") (e.g., let v = _param(params,
"force") and set force = True if v is the boolean True or if str(v).lower() in
{"true","1","yes"} else False) so only legitimate truthy boolean representations
enable the force path; update the code that reads the force variable (the
assignment using _param and any downstream checks) to use this strict parse.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b29b0565-6f65-4141-882b-4b607819a60b

📥 Commits

Reviewing files that changed from the base of the PR and between bdcb8e2 and 4e6db5e.

📒 Files selected for processing (6)
  • README.md
  • ROADMAP.md
  • TESTING.md
  • tests/test_lsp.py
  • vera/lsp/server.py
  • vera/lsp/workflows.py

Comment thread vera/lsp/server.py Outdated
CodeRabbit flagged bool(_param(params, "force")): generic truthiness
makes a malformed payload like force="false" (string) bypass the
verification gate — the one bit whose whole purpose is to be hard to
skip, failing in the open direction. _force_param() now engages only
on boolean True; strings, numbers, and other malformed values fail
closed to a refused edit with an explanatory delta.

Co-Authored-By: Claude <[email protected]>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@vera/lsp/server.py`:
- Around line 208-212: The handler must validate that params contain non-empty
strings for "uri" and "text" before delegating to apply_propose_edit; update the
code that calls _param (the variables uri and text) to perform strict type and
emptiness checks (e.g., isinstance/str and len>0) and short-circuit by returning
a refused response shape if validation fails. Keep using the same helpers
(_param, _force_param) and the same call to apply_propose_edit(server, uri,
text, _force_param(params)) only when validation passes; do not propagate None
or non-str values into apply_propose_edit.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7c621dfc-e8ac-4ac3-b1ce-015c42d9ceb8

📥 Commits

Reviewing files that changed from the base of the PR and between 4e6db5e and 876f54b.

📒 Files selected for processing (5)
  • README.md
  • ROADMAP.md
  • TESTING.md
  • tests/test_lsp.py
  • vera/lsp/server.py

Comment thread vera/lsp/server.py Outdated
…round 3)

Missing or non-string uri/text in vera/proposeEdit and
vera/speculativeEdit payloads previously flowed into the parse
pipeline as None and surfaced as opaque JSON-RPC internal errors.
_require_str() now fails closed at the protocol boundary with
JsonRpcInvalidParams (-32602), the JSON-RPC-native refusal; the
empty string stays valid (present value, replace-with-empty-
document). Tests pin both directions.

Co-Authored-By: Claude <[email protected]>
@aallan aallan merged commit 4059c25 into main Jun 11, 2026
27 checks passed
@aallan aallan deleted the feat/222-phase-f1-propose-edit branch June 11, 2026 12:53
@aallan aallan mentioned this pull request Jun 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant