Skip to content

Implement Double-Lock safety harness for weaverd with atomic commits#34

Merged
leynos merged 18 commits intomainfrom
terragon/implement-double-lock-safety-x8eg4y
Dec 11, 2025
Merged

Implement Double-Lock safety harness for weaverd with atomic commits#34
leynos merged 18 commits intomainfrom
terragon/implement-double-lock-safety-x8eg4y

Conversation

@leynos
Copy link
Copy Markdown
Owner

@leynos leynos commented Dec 8, 2025

Summary

  • Introduces a comprehensive Double-Lock safety harness for weaverd to validate CLI/daemon edits before committing to disk.
  • Adds in-memory transaction model with text edits, per-file edits, and atomic commit semantics.
  • Provides pluggable lock backends (syntactic/semantic) and test doubles to support unit and behavioural tests.
  • Adds tests and behaviour-driven scenarios to exercise success, failure (syntactic/semantic), and backend-unavailable paths.
  • Updates repository docs to reflect the new safety harness architecture and design considerations.

Changes

Safety Harness Core

  • New module: crates/weaverd/src/safety_harness (mod, error, edit, locks, transaction, verification).
  • Core types:
    • Position, TextRange, TextEdit, FileEdit for representing edits in a zero-based, LSP-like coordinate space.
    • VerificationContext to hold original and modified content in-memory during verification.
  • Lock traits:
    • SyntacticLock: validates syntactic integrity of modified files.
    • SemanticLock: validates semantic correctness against a baseline via a diagnostic backend.
  • Placeholder backends (for now):
    • PlaceholderSyntacticLock and PlaceholderSemanticLock to enable harness flow without requiring external parsers or LSP servers.
  • Configurable test doubles:
    • ConfigurableSyntacticLock and ConfigurableSemanticLock to drive pass/fail/unavailable scenarios in tests and BDD specs.
  • Edits application:
    • apply_edits to mutate in-memory content by applying text edits (with line/column offsets), using reverse-order application to keep offsets stable.
  • Atomic commit support:
    • Atomic write path using temporary files to ensure multi-file edits either fully succeed or leave the filesystem untouched.

Transaction Engine

  • New: crates/weaverd/src/safety_harness/transaction.rs
    • EditTransaction to collect edits, generate in-memory modified content, run syntactic and semantic locks, and commit when both pass.
    • TransactionOutcome to represent Committed, SyntacticLockFailed, SemanticLockFailed, or NoChanges.
    • read_file, write_file_atomic, and commit_changes helpers for robust file I/O behavior.

Verification & Validation

  • New: crates/weaverd/src/safety_harness/verification.rs
    • VerificationContext, SyntacticLock, SemanticLock traits.
    • Placeholder and configurable lock implementations for testing.
    • ConfigurableSyntacticLock and ConfigurableSemanticLock for deterministic test scenarios.
  • New: crates/weaverd/src/safety_harness/locks.rs
    • SyntacticLockResult and SemanticLockResult enums with passed/failed variants and diagnostics
  • New: crates/weaverd/src/safety_harness/error.rs
    • SafetyHarnessError and supporting types (LockPhase, VerificationFailure) with Display/Error impls

Tests & Behavioural Scenarios

  • New safety harness behavioural tests: crates/weaverd/src/tests/safety_harness_behaviour.rs
  • Feature file: crates/weaverd/tests/features/safety_harness.feature covering end-to-end harness workflows (success, failure, backend unavailable, empty-transaction, multi-file commits, new-file creation).
  • Extended unit tests validating core data structures, edits, and helper logic.

Public API Exposure

  • Wire safety_harness into weaverd/lib.rs: pub mod safety_harness;

Documentation

  • Roadmap/docs updated to reflect Double-Lock harness implementation and atomic edit strategy.
  • Users guide expanded with a dedicated Double-Lock safety harness section detailing two-phase verification, in-memory application, and atomic commits.
  • Weaver design docs updated with implementation details for the Double-Lock architecture and error reporting.

Why

  • This establishes a safe, testable, and extensible foundation for validating code edits before they touch the filesystem, reducing risk of corruption from agent-driven changes. The approach mirrors a two-phase verification analogue to ensure structural (syntactic) correctness before semantic/diagnostic validation.

Testing Plan

  • Run unit tests: cargo test -p weaverd
  • Run all tests (including safety harness tests): cargo test --all
  • Run behavioural tests (BDD style) via existing test harness: cargo test -p weaverd --features safety_harness
  • Validate new feature file is parsed by the CI test suite as part of the weaverd crate tests

Notes & Future Work

  • Real Tree-sitter based syntactic parsing and LSP-based semantic diagnostics to replace the placeholders is planned for a future iteration.
  • Additional error reporting improvements (contextual file/line information, aggregated failure summaries) can be added to SafetyHarnessError.
  • Consider exposing a higher-level CLI flag to enable/disable the Double-Lock harness for debugging in non-production environments.

🌿 Generated by Terry


ℹ️ Tag @terragon-labs to ask questions and address PR feedback

📎 Task: https://www.terragonlabs.com/task/0e88c363-d494-4907-bd35-1913ad42fca8

…ctional edits

Introduce a comprehensive safety harness in `weaverd` that wraps all `act` commands with a two-phase verification process before committing file changes. The Double-Lock harness enforces:

1. Syntactic Lock: Parses modified files to ensure valid syntax trees, catching structural errors early.
2. Semantic Lock: Uses a configured language server to verify no new errors or high-severity warnings are introduced.

Partial changes are prohibited; only fully verified edits are atomically applied to disk. The harness operates on in-memory file buffers to maintain filesystem integrity.

This includes core types (`FileEdit`, `TextEdit`), verification context, transaction management, structured error types, placeholder lock implementations for syntactic and semantic phases, configurable test doubles, and comprehensive behavioral tests with BDD feature specifications.

Documentation and design docs are updated to reflect the new safety harness feature, marking progress in the project roadmap.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Dec 8, 2025

Warning

Rate limit exceeded

@leynos has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 5 minutes and 43 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between b037eab and 67e9334.

📒 Files selected for processing (5)
  • crates/weaverd/src/safety_harness/edit.rs (1 hunks)
  • crates/weaverd/src/safety_harness/mod.rs (1 hunks)
  • crates/weaverd/src/safety_harness/transaction/tests.rs (1 hunks)
  • crates/weaverd/src/safety_harness/verification/apply.rs (1 hunks)
  • crates/weaverd/src/tests/safety_harness_behaviour.rs (1 hunks)

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Double-Lock safety harness for two-phase verification (syntactic and semantic validation) of file edits with atomic commit semantics.
    • Introduced structured error reporting with location-tracked verification failures.
  • Tests

    • Added comprehensive BDD-style test scenarios and utilities for safety harness verification.
  • Documentation

    • Updated design documentation and user guide describing safety harness architecture and capabilities.

✏️ Tip: You can customize this high-level summary in your review settings.

Walkthrough

Expose a new public module weaverd::safety_harness that provides file-edit primitives, structured errors, lock-result enums, verification traits and test doubles, an in‑memory EditTransaction with two‑phase syntactic/semantic validation and atomic commits, plus BDD tests and documentation updates.

Changes

Cohort / File(s) Summary
Dependency & module exposure
crates/weaverd/Cargo.toml, crates/weaverd/src/lib.rs
Add dev-dependency derive_more = "1.0" with features as_ref, deref; expose new public module safety_harness.
Edit data structures
crates/weaverd/src/safety_harness/edit.rs
Add Position, TextRange, ReplacementText, TextEdit, FileEdit with constructors, accessors, mutators and unit tests.
Error model
crates/weaverd/src/safety_harness/error.rs
Add VerificationFailure and SafetyHarnessError types with builders, accessors, Display/thiserror integration and tests.
Lock result types
crates/weaverd/src/safety_harness/locks.rs
Add SyntacticLockResult and SemanticLockResult enums (Passed / Failed{ failures }) with helpers and tests.
Transaction execution
crates/weaverd/src/safety_harness/transaction.rs
Add EditTransaction<'a> and TransactionOutcome implementing two‑phase validation (syntactic then semantic), in‑memory edit application, atomic per‑file writes via temp files/rename, helpers and extensive tests.
Verification traits & helpers
crates/weaverd/src/safety_harness/verification.rs, crates/weaverd/src/safety_harness/verification/apply.rs, crates/weaverd/src/safety_harness/verification/test_doubles.rs
Add VerificationContext, SyntacticLock and SemanticLock traits, placeholder/configurable test locks, apply_edits and line/column→offset helpers, compute_line_start_offsets, line_column_to_offset, and tests.
Module aggregation
crates/weaverd/src/safety_harness/mod.rs
Add module root wiring and public re‑exports for edit types, errors, lock results, transaction types and verification traits.
Test infrastructure
crates/weaverd/src/tests/mod.rs, crates/weaverd/src/tests/safety_harness_behaviour.rs, crates/weaverd/src/tests/safety_harness_types.rs, crates/weaverd/src/safety_harness/transaction/tests.rs
Add BDD test scaffolding (SafetyHarnessWorld), typed test NewTypes (FileName, FileContent, TextPattern, DiagnosticMessage), and comprehensive transaction tests and helpers.
BDD feature spec
crates/weaverd/tests/features/safety_harness.feature
Add Gherkin scenarios covering successful commits, syntactic/semantic failures, backend unavailability, empty transactions, multi‑file atomicity and new file creation.
Documentation
docs/roadmap.md, docs/users-guide.md, docs/weaver-design.md
Mark roadmap items complete; add user‑guide and design sections describing the Double‑Lock harness, atomic commit strategy, and future LSP document sync work.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant U as User
    participant T as EditTransaction
    participant FS as FileSystem
    participant VC as VerificationContext
    participant SY as SyntacticLock
    participant SE as SemanticLock

    U->>T: add_edit(...) / execute()
    T->>FS: read_file(path) for each FileEdit
    FS-->>T: original content
    T->>VC: build context + apply_edits() (in‑memory)
    VC-->>T: modified contents
    T->>SY: validate(&context)
    alt syntactic fails
        SY-->>T: Failed(failures)
        T-->>U: TransactionOutcome::SyntacticLockFailed
    else syntactic passes
        SY-->>T: Passed
        T->>SE: validate(&context)
        alt semantic fails or unavailable
            SE-->>T: Failed(failures) or Err(SemanticBackendUnavailable)
            T-->>U: TransactionOutcome::SemanticLockFailed or Err
        else semantic passes
            SE-->>T: Passed
            T->>FS: prepare temp files -> atomic rename for each modified file
            FS-->>T: commit success
            T-->>U: TransactionOutcome::Committed(files_modified)
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Inspect crates/weaverd/src/safety_harness/transaction.rs for atomic write correctness, two‑phase commit and rollback semantics.
  • Validate crates/weaverd/src/safety_harness/verification.rs and verification/apply.rs for line/column → byte‑offset calculations and UTF‑8 boundary handling.
  • Review apply_edits reverse‑order application, position validation and EditApplicationError construction.
  • Verify error typing and propagation across error.rs, locks.rs and consumers of VerificationFailure / SafetyHarnessError.
  • Review tests in crates/weaverd/src/tests/* and crates/weaverd/src/safety_harness/* for coverage and brittle assumptions (filesystem timing, temp file handling).

Poem

🔐 Two locks stand guard, one then two,
Edits ride memory, then rename true,
Fail fast or commit as one,
Files stay whole when checks are done,
Harness latched — transactions through.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: implementing a Double-Lock safety harness with atomic commits. It is concise, avoids vague terms, and directly reflects the primary objective of the changeset.
Description check ✅ Passed The description comprehensively covers the changeset across multiple sections (safety harness core, transaction engine, verification, tests, docs). It explains the 'why' and testing plan, and is directly related to the implementation details throughout the PR.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Dec 8, 2025

Reviewer's Guide

Introduces a new public safety_harness module in weaverd that models in-memory text/file edits, runs pluggable syntactic and semantic locks in a Double-Lock flow, and atomically commits successful multi-file transactions, with comprehensive error types, tests, and documentation updates.

Sequence diagram for Double-Lock EditTransaction execution

sequenceDiagram
    actor Agent
    participant CLI as weaver_CLI
    participant Daemon as weaverd
    participant Harness as EditTransaction
    participant SynLock as SyntacticLock
    participant SemLock as SemanticLock
    participant FS as FileSystem

    Agent->>CLI: issue_act_command
    CLI->>Daemon: send_edit_request
    Daemon->>Harness: create_EditTransaction
    Daemon->>Harness: add_edits(FileEdit)
    Daemon->>Harness: execute()

    alt no_file_edits
        Harness-->>Daemon: TransactionOutcome_NoChanges
        Daemon-->>CLI: respond_no_changes
        CLI-->>Agent: report_no_changes
    else has_file_edits
        loop for_each_FileEdit
            Harness->>FS: read_file(path)
            FS-->>Harness: original_content_or_empty
            Harness->>Harness: apply_edits(original, FileEdit)
            Harness->>Harness: update_VerificationContext
        end

        Harness->>SynLock: validate(VerificationContext)
        alt syntactic_lock_failed
            SynLock-->>Harness: SyntacticLockResult_Failed
            Harness-->>Daemon: TransactionOutcome_SyntacticLockFailed
            Daemon-->>CLI: report_syntactic_failure
            CLI-->>Agent: display_diagnostics
        else syntactic_lock_passed
            SynLock-->>Harness: SyntacticLockResult_Passed
            Harness->>SemLock: validate(VerificationContext)
            alt semantic_backend_unavailable
                SemLock-->>Harness: SafetyHarnessError_SemanticBackendUnavailable
                Harness-->>Daemon: Err(SafetyHarnessError)
                Daemon-->>CLI: report_backend_unavailable
                CLI-->>Agent: display_backend_error
            else semantic_lock_failed
                SemLock-->>Harness: Ok(SemanticLockResult_Failed)
                Harness-->>Daemon: TransactionOutcome_SemanticLockFailed
                Daemon-->>CLI: report_semantic_failure
                CLI-->>Agent: display_diagnostics
            else both_locks_pass
                SemLock-->>Harness: Ok(SemanticLockResult_Passed)
                loop for_each_modified_path
                    Harness->>FS: write_file_atomic(path, content)
                    FS-->>Harness: ok
                end
                Harness-->>Daemon: TransactionOutcome_Committed(files_modified)
                Daemon-->>CLI: report_success(files_modified)
                CLI-->>Agent: confirm_commit
            end
        end
    end
Loading

Class diagram for safety_harness core edit and verification types

classDiagram
    class Position {
        +u32 line
        +u32 column
        +new(line u32, column u32) Position
    }

    class TextRange {
        +Position start
        +Position end
        +new(start Position, end Position) TextRange
        +point(position Position) TextRange
    }

    class ReplacementText {
        -String value
        +new(text String) ReplacementText
        +empty() ReplacementText
        +as_str() &str
        +into_inner() String
    }

    class TextEdit {
        -TextRange range
        -String new_text
        +with_replacement(range TextRange, text ReplacementText) TextEdit
        +new(range TextRange, new_text String) TextEdit
        +from_positions_typed(start Position, end Position, text ReplacementText) TextEdit
        +from_positions(start Position, end Position, new_text String) TextEdit
        +from_coords(start_line u32, start_column u32, end_line u32, end_column u32, new_text String) TextEdit
        +insert_at(position Position, text ReplacementText) TextEdit
        +insert(line u32, column u32, text String) TextEdit
        +delete_range(start Position, end Position) TextEdit
        +delete(start_line u32, start_column u32, end_line u32, end_column u32) TextEdit
        +start_line() u32
        +start_column() u32
        +end_line() u32
        +end_column() u32
        +new_text() &str
    }

    class FileEdit {
        -PathBuf path
        -Vec~TextEdit~ edits
        +new(path PathBuf) FileEdit
        +add_edit(edit TextEdit) void
        +with_edits(path PathBuf, edits Vec~TextEdit~) FileEdit
        +path() &PathBuf
        +edits() &Vec~TextEdit~
        +is_empty() bool
    }

    Position --> TextRange : used_by
    TextRange --> TextEdit : used_by
    ReplacementText --> TextEdit : used_by
    TextEdit --> FileEdit : part_of

    class VerificationContext {
        -HashMap~PathBuf, String~ original_content
        -HashMap~PathBuf, String~ modified_content
        +new() VerificationContext
        +add_original(path PathBuf, content String) void
        +add_modified(path PathBuf, content String) void
        +original(path &PathBuf) Option~&String~
        +modified(path &PathBuf) Option~&String~
        +modified_paths() Iterator
        +modified_files() Iterator
        +modified_count() usize
        +is_empty() bool
    }

    class SyntacticLock {
        <<interface>>
        +validate(context &VerificationContext) SyntacticLockResult
    }

    class SemanticLock {
        <<interface>>
        +validate(context &VerificationContext) Result~SemanticLockResult, SafetyHarnessError~
    }

    class PlaceholderSyntacticLock {
        +validate(context &VerificationContext) SyntacticLockResult
    }

    class PlaceholderSemanticLock {
        +validate(context &VerificationContext) Result~SemanticLockResult, SafetyHarnessError~
    }

    class ConfigurableSyntacticLock {
        -Vec~VerificationFailure~ failures
        +passing() ConfigurableSyntacticLock
        +failing(failures Vec~VerificationFailure~) ConfigurableSyntacticLock
        +validate(context &VerificationContext) SyntacticLockResult
    }

    class ConfigurableSemanticLock {
        -Vec~VerificationFailure~ failures
        -Option~String~ error
        +passing() ConfigurableSemanticLock
        +failing(failures Vec~VerificationFailure~) ConfigurableSemanticLock
        +unavailable(message String) ConfigurableSemanticLock
        +validate(context &VerificationContext) Result~SemanticLockResult, SafetyHarnessError~
    }

    SyntacticLock <|.. PlaceholderSyntacticLock
    SyntacticLock <|.. ConfigurableSyntacticLock
    SemanticLock <|.. PlaceholderSemanticLock
    SemanticLock <|.. ConfigurableSemanticLock

    class SyntacticLockResult {
        <<enum>>
        +Passed
        +Failed(failures Vec~VerificationFailure~)
        +passed() bool
        +failures() Option~&[VerificationFailure]~
    }

    class SemanticLockResult {
        <<enum>>
        +Passed
        +Failed(failures Vec~VerificationFailure~)
        +passed() bool
        +failures() Option~&[VerificationFailure]~
    }

    class LockPhase {
        <<enum>>
        +Syntactic
        +Semantic
    }

    class VerificationFailure {
        -PathBuf file
        -Option~u32~ line
        -Option~u32~ column
        -String message
        +new(file PathBuf, message String) VerificationFailure
        +at_location(line u32, column u32) VerificationFailure
        +file() &PathBuf
        +line() Option~u32~
        +column() Option~u32~
        +message() &str
    }

    class SafetyHarnessError {
        <<enum>>
        +VerificationFailed(phase LockPhase, count usize, failures Vec~VerificationFailure~)
        +FileReadError(path PathBuf, message String)
        +FileWriteError(path PathBuf, message String)
        +EditApplicationError(path PathBuf, message String)
        +SemanticBackendUnavailable(message String)
        +SyntacticBackendUnavailable(message String)
        +syntactic_failed(failures Vec~VerificationFailure~) SafetyHarnessError
        +semantic_failed(failures Vec~VerificationFailure~) SafetyHarnessError
        +file_read(path PathBuf, error std_io_Error) SafetyHarnessError
        +file_write(path PathBuf, error std_io_Error) SafetyHarnessError
        +failures() Option~&[VerificationFailure]~
        +lock_phase() Option~LockPhase~
    }

    VerificationFailure --> SyntacticLockResult : used_in
    VerificationFailure --> SemanticLockResult : used_in
    VerificationFailure --> SafetyHarnessError : used_in
    LockPhase --> SafetyHarnessError : used_in

    class TransactionOutcome {
        <<enum>>
        +Committed(files_modified usize)
        +SyntacticLockFailed(failures Vec~VerificationFailure~)
        +SemanticLockFailed(failures Vec~VerificationFailure~)
        +NoChanges
        +committed() bool
        +files_modified() Option~usize~
    }

    class EditTransaction {
        -Vec~FileEdit~ file_edits
        -&SyntacticLock syntactic_lock
        -&SemanticLock semantic_lock
        +new(syntactic_lock &SyntacticLock, semantic_lock &SemanticLock) EditTransaction
        +add_edit(edit FileEdit) void
        +add_edits(edits IntoIterator~Item=FileEdit~) void
        +execute() Result~TransactionOutcome, SafetyHarnessError~
    }

    EditTransaction --> FileEdit : aggregates
    EditTransaction --> VerificationContext : builds
    EditTransaction --> SyntacticLock : uses
    EditTransaction --> SemanticLock : uses
    EditTransaction --> TransactionOutcome : returns
    EditTransaction --> SafetyHarnessError : returns
    TransactionOutcome --> VerificationFailure : contains

    class safety_harness_mod {
        +FileEdit
        +Position
        +ReplacementText
        +TextEdit
        +TextRange
        +LockPhase
        +SafetyHarnessError
        +VerificationFailure
        +SemanticLockResult
        +SyntacticLockResult
        +EditTransaction
        +TransactionOutcome
        +ConfigurableSemanticLock
        +ConfigurableSyntacticLock
        +PlaceholderSemanticLock
        +PlaceholderSyntacticLock
        +SemanticLock
        +SyntacticLock
        +VerificationContext
    }

    safety_harness_mod ..> EditTransaction
    safety_harness_mod ..> FileEdit
    safety_harness_mod ..> VerificationContext
    safety_harness_mod ..> SafetyHarnessError
Loading

File-Level Changes

Change Details Files
Add core Double-Lock safety_harness module with edit modeling, verification context, lock traits, and public re-exports.
  • Define Position, TextRange, ReplacementText, TextEdit, and FileEdit types to model LSP-style text edits per file.
  • Implement VerificationContext to hold original/modified file contents and expose query helpers.
  • Define SyntacticLock and SemanticLock traits plus placeholder and configurable (test-double) implementations.
  • Expose the safety_harness module publicly from weaverd::lib and document its purpose in crate-level docs.
crates/weaverd/src/safety_harness/mod.rs
crates/weaverd/src/safety_harness/edit.rs
crates/weaverd/src/safety_harness/verification.rs
crates/weaverd/src/lib.rs
Implement EditTransaction engine with atomic multi-file commit and transaction outcomes.
  • Introduce EditTransaction builder that collects FileEdit values, applies edits in-memory, runs syntactic then semantic locks, and commits on success.
  • Define TransactionOutcome enum with Committed, SyntacticLockFailed, SemanticLockFailed, and NoChanges variants plus helpers.
  • Implement apply_edits, read_file (supporting new-file creation), write_file_atomic, and commit_changes helpers for robust IO and atomic renames.
  • Add unit tests covering happy path, syntactic/semantic failures, backend-unavailable error propagation, new-file creation, and multi-file transactions.
crates/weaverd/src/safety_harness/transaction.rs
Introduce structured error and lock result types for verification failures and backend issues.
  • Add LockPhase and VerificationFailure types with Display implementations and accessors for file/line/column/message.
  • Define SafetyHarnessError enum for verification failures, IO errors, edit application errors, and backend unavailability, with helper constructors and accessors.
  • Define SyntacticLockResult and SemanticLockResult enums with passed/failed variants plus helpers to access failures.
  • Add unit tests validating error formatting, phase tagging, and lock result helpers.
crates/weaverd/src/safety_harness/error.rs
crates/weaverd/src/safety_harness/locks.rs
Add BDD-style behavioural test harness and feature specs for the safety harness.
  • Create SafetyHarnessWorld test fixture encapsulating temp dir, configurable locks, pending edits, and transaction outcome.
  • Implement Given/When/Then step functions to compose scenarios for success, syntactic failure, semantic failure, backend-unavailable, empty transaction, multi-file atomicity, and new-file creation.
  • Introduce type-safe newtypes (FileName, FileContent, TextPattern, DiagnosticMessage) to reduce stringly-typed test code.
  • Register safety harness tests in the weaverd test module tree and add derive_more as a dev-dependency for the newtypes.
crates/weaverd/src/tests/safety_harness_behaviour.rs
crates/weaverd/src/tests/safety_harness_types.rs
crates/weaverd/tests/features/safety_harness.feature
crates/weaverd/src/tests/mod.rs
crates/weaverd/Cargo.toml
Update documentation and roadmap to describe and mark completion of the Double-Lock safety harness and atomic edits.
  • Extend users-guide with a Double-Lock safety harness section covering two-phase verification, in-memory application, atomic commits, error reporting, backend unavailability, and placeholder limitations.
  • Add design doc section detailing implementation decisions, core types, lock traits, placeholder implementations, test doubles, atomic strategy, and structured error reporting.
  • Mark Double-Lock safety harness logic and atomic edits as completed in the roadmap.
  • Document the harness in weaverd crate-level docs to explain the two-phase validation of act commands.
docs/users-guide.md
docs/weaver-design.md
docs/roadmap.md
crates/weaverd/src/lib.rs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

codescene-delta-analysis[bot]

This comment was marked as outdated.

@macroscopeapp
Copy link
Copy Markdown

macroscopeapp Bot commented Dec 8, 2025

Add Double-Lock safety harness with atomic commit coordinator to weaverd for syntactic and semantic validation

Introduce a safety_harness module that applies edits in memory, runs syntactic then semantic validation, and commits file changes atomically. Provide EditTransaction::execute with two-phase write/rollback, structured results and errors, edit utilities, verification context, and test doubles. Add extensive unit and BDD tests and update docs. Note: test newtypes in tests/safety_harness_types.rs contain stray '+' tokens that break compilation.

📍Where to Start

Start with the transaction flow in EditTransaction::execute in transaction.rs, then review edit application in verification/apply.rs and error/result types in mod.rs.


Macroscope summarized 67e9334.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Dec 10, 2025

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

Bumpy Road Ahead

crates/weaverd/src/safety_harness/verification.rs: line_column_to_offset

What lead to degradation?

line_column_to_offset has 2 blocks with nested conditional logic. Any nesting of 2 or deeper is considered. Threshold is 2 blocks per function

Why does this problem occur?

A Bumpy Road is a function that contains multiple chunks of nested conditional logic inside the same function. The deeper the nesting and the more bumps, the lower the code health.
A bumpy code road represents a lack of encapsulation which becomes an obstacle to comprehension. In imperative languages there’s also an increased risk for feature entanglement, which leads to complex state management. CodeScene considers the following rules for the code health impact: 1) The deeper the nested conditional logic of each bump, the higher the tax on our working memory. 2) The more bumps inside a function, the more expensive it is to refactor as each bump represents a missing abstraction. 3) The larger each bump – that is, the more lines of code it spans – the harder it is to build up a mental model of the function. The nesting depth for what is considered a bump is levels of conditionals.

How to fix it?

Bumpy Road implementations indicate a lack of encapsulation. Check out the detailed description of the Bumpy Road code health issue.
A Bumpy Road often suggests that the function/method does too many things. The first refactoring step is to identify the different possible responsibilities of the function. Consider extracting those responsibilities into smaller, cohesive, and well-named functions. The EXTRACT FUNCTION refactoring is the primary response.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Dec 10, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the verification.rs file to examine the line_column_to_offset function
cat crates/weaverd/src/safety_harness/verification.rs

Length of output: 13183


@leynos, I've analyzed the line_column_to_offset function. The nested conditional logic can be refactored by extracting two helper functions, each with a single, clear responsibility.

Suggested Fix

Replace the current line_column_to_offset function with this refactored version:

/// Converts a line and column pair to a byte offset in the original text.
fn line_column_to_offset(lines: &[&str], line: u32, column: u32) -> Option<usize> {
    let line_idx = line as usize;
    if line_idx > lines.len() {
        return None;
    }

    let line_start_offset = calculate_line_start_offset(lines, line_idx)?;
    add_validated_column_offset(lines, line_idx, column, line_start_offset)
}

/// Calculates the byte offset to the start of the specified line.
fn calculate_line_start_offset(lines: &[&str], target_line_idx: usize) -> Option<usize> {
    let mut offset: usize = 0;
    for (idx, &line_content) in lines.iter().enumerate() {
        if idx == target_line_idx {
            break;
        }
        offset = offset.checked_add(line_content.len())?;
        offset = offset.checked_add(1)?; // newline character
    }
    Some(offset)
}

/// Validates the column offset and adds it to the line start offset.
fn add_validated_column_offset(
    lines: &[&str],
    line_idx: usize,
    column: u32,
    line_start_offset: usize,
) -> Option<usize> {
    let col_offset = column as usize;
    if line_idx < lines.len() {
        let line_content = lines.get(line_idx)?;
        if col_offset > line_content.len() {
            return None;
        }
    }
    line_start_offset.checked_add(col_offset)
}

AI Coding Agent Prompt

Refactor the `line_column_to_offset` function in `crates/weaverd/src/safety_harness/verification.rs` to eliminate nested conditional logic by extracting two helper functions:

1. Extract the for-loop logic that calculates the byte offset to the start of a target line into a new function called `calculate_line_start_offset(lines: &[&str], target_line_idx: usize) -> Option<usize>`.

2. Extract the column validation and offset addition logic into a new function called `add_validated_column_offset(lines: &[&str], line_idx: usize, column: u32, line_start_offset: usize) -> Option<usize>`.

3. Simplify the main `line_column_to_offset` function to orchestrate these two helper functions, keeping only the initial bounds check and delegating the rest to the extracted functions.

Place the two new helper functions immediately after the `line_column_to_offset` function in the same file. Ensure all existing tests continue to pass.

This refactoring improves code health by:

  • Reducing nesting depth and cognitive load
  • Encapsulating distinct responsibilities in well-named functions
  • Making each function easier to understand, test, and maintain

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Dec 10, 2025

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

crates/weaverd/src/tests/safety_harness_behaviour.rs

Comment on file

//! Behavioural tests for the Double-Lock safety harness.

❌ New issue: String Heavy Function Arguments
In this module, 49.1% of all arguments to its 29 functions are strings. The threshold for string arguments is 39.0%

Use NewTypes to model domain values and eliminate "integer soup". Reach for newt-hype when introducing many homogeneous wrappers that share behaviour; add small shims such as From<&str> and AsRef<str> for string-backed wrappers. For path-centric wrappers implement AsRef<Path> alongside into_inner() and to_path_buf(); avoid attempting impl From<Wrapper> for PathBuf because of the orphan rule. Prefer explicit tuple structs whenever bespoke validation or tailored trait surfaces are required, customising Deref, AsRef, and TryFrom per type. Use the-newtype when defining traits and needing blanket implementations that apply across wrappers satisfying Newtype + AsRef/AsMut<Inner>, or when establishing a coherent internal convention that keeps trait forwarding consistent without per-type boilerplate. Combine approaches: lean on newt-hype for the common case, tuple structs for outliers, and the-newtype to unify behaviour when you own the trait definitions.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Dec 10, 2025

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

Excess Number of Function Arguments

crates/weaverd/src/safety_harness/edit.rs: TextEdit.from_coords

What lead to degradation?

TextEdit.from_coords has 5 arguments, max arguments = 4

Why does this problem occur?

Functions with many arguments indicate either a) low cohesion where the function has too many responsibilities, or b) a missing abstraction that encapsulates those arguments.
The threshold for the Rust language is 4 function arguments.

How to fix it?

Start by investigating the responsibilities of the function. Make sure it doesn't do too many things, in which case it should be split into smaller and more cohesive functions. Consider the refactoring INTRODUCE PARAMETER OBJECT to encapsulate arguments that refer to the same logical concept.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Dec 10, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the safety_harness_behaviour.rs file to analyze string arguments
cat crates/weaverd/src/tests/safety_harness_behaviour.rs

Length of output: 10684


@leynos, I've analysed the test module. The string-heavy arguments stem from five distinct semantic domains being represented as primitive strings: file names, file content, text patterns for edits, expected content for assertions, and diagnostic messages. Introduce NewTypes to eliminate this "string soup" and make the domain model explicit.

Suggested Fix

Create a new module crates/weaverd/src/tests/safety_harness_types.rs with these domain-specific NewTypes:

//! Domain types for safety harness behavioural tests.

use std::ops::Deref;
use std::path::{Path, PathBuf};

/// A file name used in test scenarios.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct FileName(String);

impl FileName {
    pub fn as_str(&self) -> &str {
        &self.0
    }

    pub fn to_path(&self, base: &Path) -> PathBuf {
        base.join(&self.0)
    }
}

impl From<String> for FileName {
    fn from(s: String) -> Self {
        Self(s.trim_matches('"').to_string())
    }
}

impl From<&str> for FileName {
    fn from(s: &str) -> Self {
        Self(s.trim_matches('"').to_string())
    }
}

impl AsRef<str> for FileName {
    fn as_ref(&self) -> &str {
        &self.0
    }
}

impl Deref for FileName {
    type Target = str;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

/// Content of a file in test scenarios.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FileContent(String);

impl FileContent {
    pub fn as_str(&self) -> &str {
        &self.0
    }

    pub fn as_bytes(&self) -> &[u8] {
        self.0.as_bytes()
    }
}

impl From<String> for FileContent {
    fn from(s: String) -> Self {
        Self(s.trim_matches('"').to_string())
    }
}

impl From<&str> for FileContent {
    fn from(s: &str) -> Self {
        Self(s.trim_matches('"').to_string())
    }
}

impl AsRef<str> for FileContent {
    fn as_ref(&self) -> &str {
        &self.0
    }
}

impl Deref for FileContent {
    type Target = str;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

/// A text pattern for search, replace, or assertion operations.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TextPattern(String);

impl TextPattern {
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl From<String> for TextPattern {
    fn from(s: String) -> Self {
        Self(s.trim_matches('"').to_string())
    }
}

impl From<&str> for TextPattern {
    fn from(s: &str) -> Self {
        Self(s.trim_matches('"').to_string())
    }
}

impl AsRef<str> for TextPattern {
    fn as_ref(&self) -> &str {
        &self.0
    }
}

impl Deref for TextPattern {
    type Target = str;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

/// A diagnostic message for lock configuration.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiagnosticMessage(String);

impl DiagnosticMessage {
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl From<String> for DiagnosticMessage {
    fn from(s: String) -> Self {
        Self(s.trim_matches('"').to_string())
    }
}

impl From<&str> for DiagnosticMessage {
    fn from(s: &str) -> Self {
        Self(s.trim_matches('"').to_string())
    }
}

impl AsRef<str> for DiagnosticMessage {
    fn as_ref(&self) -> &str {
        &self.0
    }
}

impl Deref for DiagnosticMessage {
    type Target = str;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

Then update the method signatures in SafetyHarnessWorld:

fn create_file(&mut self, name: &FileName, content: &FileContent) { /* ... */ }
fn file_path(&self, name: &FileName) -> PathBuf { /* ... */ }
fn read_file(&self, name: &FileName) -> String { /* ... */ }
fn add_replacement_edit(&mut self, name: &FileName, old: &TextPattern, new: &TextPattern) { /* ... */ }
fn add_creation_edit(&mut self, name: &FileName, content: &FileContent) { /* ... */ }

And update all BDD step functions (e.g.):

#[given("a source file {name} with content {content}")]
fn given_source_file(world: &RefCell<SafetyHarnessWorld>, name: FileName, content: FileContent) {
    world.borrow_mut().create_file(&name, &content);
}

#[given("a syntactic lock that fails with {message}")]
fn given_syntactic_fails(world: &RefCell<SafetyHarnessWorld>, message: DiagnosticMessage) {
    let failure = VerificationFailure::new(PathBuf::from("test"), message.as_str());
    world.borrow_mut().syntactic_lock = ConfigurableSyntacticLock::failing(vec![failure]);
}

#[when("an edit replaces {old} with {new}")]
fn when_edit_replaces(world: &RefCell<SafetyHarnessWorld>, old: TextPattern, new: TextPattern) {
    world.borrow_mut().add_replacement_edit(&"test.txt".into(), &old, &new);
    world.borrow_mut().execute_transaction();
}

AI Coding Agent Prompt

Create domain-specific NewTypes to eliminate string-heavy function arguments in `crates/weaverd/src/tests/safety_harness_behaviour.rs`.

Step 1: Create a new file `crates/weaverd/src/tests/safety_harness_types.rs` containing five NewType wrappers:
- `FileName` – wraps file name strings
- `FileContent` – wraps file content strings  
- `TextPattern` – wraps text patterns for search/replace/assertion
- `DiagnosticMessage` – wraps diagnostic messages for lock configuration

For each NewType:
- Use a tuple struct: `pub struct TypeName(String);`
- Implement `From<String>` and `From<&str>` that call `trim_matches('"')` on the input
- Implement `AsRef<str>` returning a reference to the inner string
- Implement `Deref` with `Target = str`
- Add an `as_str(&self) -> &str` method returning `&self.0`
- For `FileContent`, additionally implement `as_bytes(&self) -> &[u8]` returning `self.0.as_bytes()`
- For `FileName`, additionally implement `to_path(&self, base: &Path) -> PathBuf` returning `base.join(&self.0)`

Step 2: Add `mod safety_harness_types;` and `use safety_harness_types::*;` to `crates/weaverd/src/tests/mod.rs`.

Step 3: In `crates/weaverd/src/tests/safety_harness_behaviour.rs`:
- Import the new types: `use super::safety_harness_types::*;`
- Update all method signatures in `SafetyHarnessWorld` to accept the appropriate NewType references instead of `&str`:
  * `create_file(&mut self, name: &FileName, content: &FileContent)`
  * `file_path(&self, name: &FileName) -> PathBuf`
  * `read_file(&self, name: &FileName) -> String`
  * `add_replacement_edit(&mut self, name: &FileName, old: &TextPattern, new: &TextPattern)`
  * `add_creation_edit(&mut self, name: &FileName, content: &FileContent)`
- Update all BDD step functions (those annotated with `#[given]`, `#[when]`, `#[then]`) to accept owned NewType parameters instead of `String`:
  * Replace `name: String` with `name: FileName`
  * Replace `content: String` with `content: FileContent`
  * Replace `old: String` and `new: String` with `old: TextPattern` and `new: TextPattern`
  * Replace `expected: String` with `expected: TextPattern`
  * Replace `message: String` with `message: DiagnosticMessage`
- Remove all `.trim_matches('"')` calls from step function bodies; this is now handled by the `From` implementations
- Update method invocations to pass references: `&name`, `&content`, `&old`, `&new`, `&expected`
- Use `.as_str()` when constructing `VerificationFailure` or other types expecting `&str`
- For hardcoded string literals like `"test.txt"`, convert using `.into()`: `&"test.txt".into()`

Step 4: Run `cargo test -p weaverd` to ensure all tests pass after the refactoring.

This refactoring eliminates string soup, reduces the string argument percentage below 39%, and makes the test domain model explicit and type-safe.

This refactoring achieves:

  • Type safety: Compile-time guarantees that file names aren't confused with content or patterns
  • Intent clarity: Function signatures document the semantic role of each string
  • Reduced cognitive load: Readers immediately understand what each parameter represents
  • Zero runtime cost: NewTypes are optimised away via Deref and inlining

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Dec 10, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the edit.rs file to examine the TextEdit.from_coords function
cat crates/weaverd/src/safety_harness/edit.rs

Length of output: 6361


@leynos, I've analysed the TextEdit.from_coords function. The five arguments naturally group into two Position objects (start and end coordinates), which can be encapsulated to reduce the argument count to three.

Suggested Fix

Introduce a from_positions method that accepts Position objects as parameter objects, then deprecate from_coords:

/// Builds a text edit from explicit positions.
///
/// This convenience constructor accepts Position values directly when callers
/// do not want to create an intermediate [`TextRange`] value.
#[must_use]
pub const fn from_positions(start: Position, end: Position, new_text: String) -> Self {
    Self::new(TextRange::new(start, end), new_text)
}

/// Builds a text edit from explicit positions.
///
/// This convenience constructor accepts position coordinates directly when
/// callers do not want to create intermediate [`Position`] and [`TextRange`]
/// values. The argument count is intentionally above the clippy threshold to
/// match LSP conventions.
///
/// # Deprecated
/// Use [`Self::from_positions`] instead to reduce argument count.
#[must_use]
#[deprecated(since = "0.2.0", note = "use `from_positions` instead")]
#[allow(clippy::too_many_arguments, reason = "matches LSP coordinate convention")]
pub const fn from_coords(
    start_line: u32,
    start_column: u32,
    end_line: u32,
    end_column: u32,
    new_text: String,
) -> Self {
    Self::from_positions(
        Position::new(start_line, start_column),
        Position::new(end_line, end_column),
        new_text,
    )
}

Update the delete method to use from_positions:

/// Creates a deletion spanning the given range.
#[must_use]
pub const fn delete(
    start_line: u32,
    start_column: u32,
    end_line: u32,
    end_column: u32,
) -> Self {
    Self::from_positions(
        Position::new(start_line, start_column),
        Position::new(end_line, end_column),
        String::new(),
    )
}

AI Coding Agent Prompt

Refactor the `TextEdit` implementation in `crates/weaverd/src/safety_harness/edit.rs` to introduce a parameter object pattern:

1. Add a new `from_positions` method to the `TextEdit` impl block that accepts three arguments: `start: Position`, `end: Position`, and `new_text: String`. This method should construct a TextRange from the two Position arguments and delegate to `TextEdit::new`.

2. Mark the existing `from_coords` method as deprecated with `#[deprecated(since = "0.2.0", note = "use `from_positions` instead")]` and update its implementation to delegate to the new `from_positions` method.

3. Update the `delete` method to call `from_positions` instead of `from_coords`, constructing Position objects from its coordinate arguments.

4. Place the new `from_positions` method immediately before the `from_coords` method in the source file.

5. Update the doc comment for `from_coords` to include a "# Deprecated" section that directs users to `from_positions`.

Ensure all existing tests continue to pass after the refactoring.

This refactoring improves code health by:

  • Introducing the INTRODUCE PARAMETER OBJECT pattern to encapsulate related coordinates
  • Reducing the argument count from 5 to 3 in the new method
  • Providing a migration path via deprecation rather than breaking existing code
  • Leveraging existing Position and TextRange abstractions consistently

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Dec 10, 2025

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

crates/weaverd/src/safety_harness/verification.rs

Comment on lines +278 to +303

fn line_column_to_offset(lines: &[&str], line: u32, column: u32) -> Option<usize> {
    let line_idx = line as usize;
    if line_idx > lines.len() {
        return None;
    }

    // Calculate offset to the start of the target line
    let mut offset: usize = 0;
    for (idx, &line_content) in lines.iter().enumerate() {
        if idx == line_idx {
            break;
        }
        offset = offset.checked_add(line_content.len())?;
        offset = offset.checked_add(1)?; // newline character
    }

    // Add column offset
    let col_offset = column as usize;
    if line_idx < lines.len() {
        let line_content = lines.get(line_idx)?;
        if col_offset > line_content.len() {
            return None;
        }
    }
    offset.checked_add(col_offset)
}

❌ New issue: Bumpy Road Ahead
line_column_to_offset has 2 blocks with nested conditional logic. Any nesting of 2 or deeper is considered. Threshold is 2 blocks per function

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Dec 10, 2025

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

crates/weaverd/src/safety_harness/transaction.rs

Comment on lines +230 to +251

    fn successful_transaction_commits_changes() {
        let dir = TempDir::new().expect("create temp dir");
        let path = temp_file(&dir, "test.txt", "hello world");

        let edit = FileEdit::with_edits(
            path.clone(),
            vec![TextEdit::from_coords(0, 0, 0, 5, "greetings".to_string())],
        );

        let syntactic = ConfigurableSyntacticLock::passing();
        let semantic = ConfigurableSemanticLock::passing();

        let mut transaction = EditTransaction::new(&syntactic, &semantic);
        transaction.add_edit(edit);

        let outcome = transaction.execute().expect("should succeed");
        assert!(outcome.committed());
        assert_eq!(outcome.files_modified(), Some(1));

        let content = fs::read_to_string(&path).expect("read file");
        assert_eq!(content, "greetings world");
    }

❌ New issue: Code Duplication
The module contains 5 functions with similar structure: tests.handles_new_file_creation,tests.semantic_backend_error_propagates,tests.semantic_failure_prevents_commit,tests.successful_transaction_commits_changes and 1 more functions

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Dec 10, 2025

Tip

For best results, initiate chat on the files or code changes.

@leynos, I've already provided a comprehensive fix for this exact issue earlier in our conversation. My previous response included:

  1. Refactored code that extracts two helper functions (calculate_line_start_offset and add_validated_column_offset) to eliminate the nested conditional logic
  2. Complete AI coding agent prompt with step-by-step instructions for applying the refactoring
  3. Explanation of how the changes improve code health

You can find my detailed response above in this thread, where I addressed the same Bumpy Road issue for the line_column_to_offset function.

If you need any clarification on the suggested refactoring, have questions about the helper function design, or would like me to explain any part of the solution in more detail, I'm happy to help!

Is there a specific aspect of the previous fix you'd like me to elaborate on, or are you encountering any issues applying it?

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Dec 10, 2025

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

crates/weaverd/src/safety_harness/edit.rs

Comment on file

//! Types representing file edits and modifications.

❌ New issue: Primitive Obsession
In this module, 37.5% of all function arguments are primitive types, threshold = 30.0%

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Dec 10, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the transaction.rs file to analyze the code duplication
cat crates/weaverd/src/safety_harness/transaction.rs

Length of output: 13423


@leynos, I've analysed the test module. The five test functions share identical setup boilerplate: creating a TempDir, creating test files, constructing FileEdit with TextEdit, configuring locks, building and executing a transaction. The variation points are lock configuration and assertions. Extract a test builder to eliminate this duplication.

Suggested Fix

Add a test builder helper at the beginning of the tests module:

/// Builder for transaction test scenarios.
struct TransactionTestBuilder {
    dir: TempDir,
    files: Vec<(PathBuf, String)>,
    edits: Vec<FileEdit>,
}

impl TransactionTestBuilder {
    fn new() -> Self {
        Self {
            dir: TempDir::new().expect("create temp dir"),
            files: Vec::new(),
            edits: Vec::new(),
        }
    }

    fn with_file(mut self, name: &str, content: &str) -> Self {
        let path = self.dir.path().join(name);
        let mut file = fs::File::create(&path).expect("create temp file");
        file.write_all(content.as_bytes()).expect("write temp file");
        self.files.push((path, content.to_string()));
        self
    }

    fn with_new_file_path(mut self, name: &str) -> Self {
        let path = self.dir.path().join(name);
        self.files.push((path, String::new()));
        self
    }

    fn with_replacement_edit(mut self, file_idx: usize, start_col: u32, end_col: u32, text: &str) -> Self {
        let path = &self.files[file_idx].0;
        let edit = FileEdit::with_edits(
            path.clone(),
            vec![TextEdit::from_coords(0, 0, 0, end_col, text.to_string())],
        );
        self.edits.push(edit);
        self
    }

    fn with_insert_edit(mut self, file_idx: usize, text: &str) -> Self {
        let path = &self.files[file_idx].0;
        let edit = FileEdit::with_edits(
            path.clone(),
            vec![TextEdit::insert(0, 0, text.to_string())],
        );
        self.edits.push(edit);
        self
    }

    fn execute_with_locks(
        mut self,
        syntactic: &dyn SyntacticLock,
        semantic: &dyn SemanticLock,
    ) -> (TransactionOutcome, Vec<PathBuf>) {
        let mut transaction = EditTransaction::new(syntactic, semantic);
        for edit in self.edits.drain(..) {
            transaction.add_edit(edit);
        }
        let outcome = transaction.execute().expect("execute transaction");
        let paths = self.files.into_iter().map(|(p, _)| p).collect();
        (outcome, paths)
    }

    fn file_path(&self, idx: usize) -> &PathBuf {
        &self.files[idx].0
    }
}

Then refactor the tests:

#[test]
fn successful_transaction_commits_changes() {
    let builder = TransactionTestBuilder::new()
        .with_file("test.txt", "hello world")
        .with_replacement_edit(0, 0, 5, "greetings");

    let syntactic = ConfigurableSyntacticLock::passing();
    let semantic = ConfigurableSemanticLock::passing();
    let (outcome, paths) = builder.execute_with_locks(&syntactic, &semantic);

    assert!(outcome.committed());
    assert_eq!(outcome.files_modified(), Some(1));
    let content = fs::read_to_string(&paths[0]).expect("read file");
    assert_eq!(content, "greetings world");
}

#[test]
fn syntactic_failure_prevents_commit() {
    let builder = TransactionTestBuilder::new()
        .with_file("test.txt", "hello world")
        .with_replacement_edit(0, 0, 5, "greetings");

    let path = builder.file_path(0).clone();
    let failures = vec![VerificationFailure::new(path.clone(), "syntax error")];
    let syntactic = ConfigurableSyntacticLock::failing(failures);
    let semantic = ConfigurableSemanticLock::passing();
    let (outcome, paths) = builder.execute_with_locks(&syntactic, &semantic);

    assert!(matches!(outcome, TransactionOutcome::SyntacticLockFailed { .. }));
    let content = fs::read_to_string(&paths[0]).expect("read file");
    assert_eq!(content, "hello world");
}

#[test]
fn semantic_failure_prevents_commit() {
    let builder = TransactionTestBuilder::new()
        .with_file("test.txt", "hello world")
        .with_replacement_edit(0, 0, 5, "greetings");

    let path = builder.file_path(0).clone();
    let failures = vec![VerificationFailure::new(path.clone(), "type error")];
    let syntactic = ConfigurableSyntacticLock::passing();
    let semantic = ConfigurableSemanticLock::failing(failures);
    let (outcome, paths) = builder.execute_with_locks(&syntactic, &semantic);

    assert!(matches!(outcome, TransactionOutcome::SemanticLockFailed { .. }));
    let content = fs::read_to_string(&paths[0]).expect("read file");
    assert_eq!(content, "hello world");
}

#[test]
fn semantic_backend_error_propagates() {
    let builder = TransactionTestBuilder::new()
        .with_file("test.txt", "hello world")
        .with_replacement_edit(0, 0, 5, "greetings");

    let syntactic = ConfigurableSyntacticLock::passing();
    let semantic = ConfigurableSemanticLock::unavailable("LSP crashed");
    
    let mut transaction = EditTransaction::new(&syntactic, &semantic);
    for edit in builder.edits {
        transaction.add_edit(edit);
    }
    let result = transaction.execute();
    
    assert!(result.is_err());
    assert!(matches!(
        result.unwrap_err(),
        SafetyHarnessError::SemanticBackendUnavailable { .. }
    ));
    let content = fs::read_to_string(builder.file_path(0)).expect("read file");
    assert_eq!(content, "hello world");
}

#[test]
fn handles_new_file_creation() {
    let builder = TransactionTestBuilder::new()
        .with_new_file_path("new_file.txt")
        .with_insert_edit(0, "new content");

    let path = builder.file_path(0).clone();
    assert!(!path.exists());

    let syntactic = ConfigurableSyntacticLock::passing();
    let semantic = ConfigurableSemanticLock::passing();
    let (outcome, paths) = builder.execute_with_locks(&syntactic, &semantic);

    assert!(outcome.committed());
    let content = fs::read_to_string(&paths[0]).expect("read file");
    assert_eq!(content, "new content");
}

AI Coding Agent Prompt

Eliminate code duplication in `crates/weaverd/src/safety_harness/transaction.rs` tests by introducing a test builder pattern.

Step 1: Add a `TransactionTestBuilder` struct immediately after the `temp_file` helper function in the `tests` module:
- Fields: `dir: TempDir`, `files: Vec<(PathBuf, String)>`, `edits: Vec<FileEdit>`
- `new()` constructor that creates a TempDir
- `with_file(mut self, name: &str, content: &str) -> Self` that creates a file and adds it to `files`
- `with_new_file_path(mut self, name: &str) -> Self` that adds a non-existent path to `files` with empty content
- `with_replacement_edit(mut self, file_idx: usize, start_col: u32, end_col: u32, text: &str) -> Self` that adds a FileEdit with a replacement TextEdit
- `with_insert_edit(mut self, file_idx: usize, text: &str) -> Self` that adds a FileEdit with an insert TextEdit
- `execute_with_locks(mut self, syntactic: &dyn SyntacticLock, semantic: &dyn SemanticLock) -> (TransactionOutcome, Vec<PathBuf>)` that creates a transaction, adds all edits, executes it, and returns the outcome and file paths
- `file_path(&self, idx: usize) -> &PathBuf` that returns a reference to a file path by index

Step 2: Refactor these five test functions to use the builder:
- `successful_transaction_commits_changes`: Use builder with `with_file` and `with_replacement_edit`, then `execute_with_locks`
- `syntactic_failure_prevents_commit`: Use builder, capture path via `file_path(0).clone()`, configure failing syntactic lock, execute
- `semantic_failure_prevents_commit`: Use builder, capture path via `file_path(0).clone()`, configure failing semantic lock, execute
- `semantic_backend_error_propagates`: Use builder to construct edits, but manually create transaction to test error path (since `execute_with_locks` expects success)
- `handles_new_file_creation`: Use builder with `with_new_file_path` and `with_insert_edit`

Step 3: Keep the `temp_file` function for tests that don't fit the builder pattern (e.g., `handles_multiple_files`).

Step 4: Preserve all existing assertions and test logic; only eliminate setup duplication.

Step 5: Run `cargo test -p weaverd` to verify all tests pass.

This refactoring eliminates ~80% of duplicated setup code while maintaining test readability and explicit assertions.

This refactoring achieves:

  • Reduced duplication: Common setup extracted into reusable builder
  • Maintained clarity: Assertions remain explicit in each test
  • Fluent interface: Builder pattern makes test intent clear
  • Type safety: Compiler ensures correct usage of file indices

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Dec 10, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the edit.rs file to analyze primitive type usage
cat crates/weaverd/src/safety_harness/edit.rs

Length of output: 6361


@leynos, I've analysed the edit.rs file. The primitive obsession stems from convenience methods that accept raw u32 coordinates instead of leveraging the existing Position abstraction, plus String being used directly for replacement text. Introduce Position-based method variants and a ReplacementText newtype to eliminate this code smell.

Suggested Fix

Add Position-based method variants and a ReplacementText newtype:

/// Replacement text for a text edit operation.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReplacementText(String);

impl ReplacementText {
    /// Creates new replacement text.
    #[must_use]
    pub fn new(text: String) -> Self {
        Self(text)
    }

    /// Creates empty replacement text (for deletions).
    #[must_use]
    pub const fn empty() -> Self {
        Self(String::new())
    }

    /// Returns the text as a string slice.
    #[must_use]
    pub fn as_str(&self) -> &str {
        &self.0
    }

    /// Consumes the wrapper and returns the inner String.
    #[must_use]
    pub fn into_inner(self) -> String {
        self.0
    }
}

impl From<String> for ReplacementText {
    fn from(s: String) -> Self {
        Self(s)
    }
}

impl From<&str> for ReplacementText {
    fn from(s: &str) -> Self {
        Self(s.to_string())
    }
}

impl AsRef<str> for ReplacementText {
    fn as_ref(&self) -> &str {
        &self.0
    }
}

Update TextEdit implementation to add Position-based variants:

impl TextEdit {
    /// Builds a text edit from a range and replacement text.
    #[must_use]
    pub const fn new(range: TextRange, new_text: String) -> Self {
        Self { range, new_text }
    }

    /// Builds a text edit from a range and replacement text wrapper.
    #[must_use]
    pub fn with_replacement(range: TextRange, text: ReplacementText) -> Self {
        Self {
            range,
            new_text: text.into_inner(),
        }
    }

    /// Builds a text edit from explicit positions.
    ///
    /// This is the preferred method for creating edits from position objects,
    /// reducing primitive obsession compared to coordinate-based constructors.
    #[must_use]
    pub fn from_positions(start: Position, end: Position, text: ReplacementText) -> Self {
        Self::with_replacement(TextRange::new(start, end), text)
    }

    /// Builds a text edit from explicit positions.
    ///
    /// This convenience constructor accepts position coordinates directly when
    /// callers do not want to create intermediate [`Position`] and [`TextRange`]
    /// values. The argument count is intentionally above the clippy threshold to
    /// match LSP conventions.
    ///
    /// Consider using [`Self::from_positions`] to reduce primitive obsession.
    #[must_use]
    #[allow(clippy::too_many_arguments, reason = "matches LSP coordinate convention")]
    pub const fn from_coords(
        start_line: u32,
        start_column: u32,
        end_line: u32,
        end_column: u32,
        new_text: String,
    ) -> Self {
        Self::new(
            TextRange::new(
                Position::new(start_line, start_column),
                Position::new(end_line, end_column),
            ),
            new_text,
        )
    }

    /// Creates an insertion at the specified position.
    #[must_use]
    pub fn insert_at(position: Position, text: ReplacementText) -> Self {
        Self::with_replacement(TextRange::point(position), text)
    }

    /// Creates an insertion at the specified position (coordinate-based).
    ///
    /// Consider using [`Self::insert_at`] to reduce primitive obsession.
    #[must_use]
    pub const fn insert(line: u32, column: u32, text: String) -> Self {
        Self::new(TextRange::point(Position::new(line, column)), text)
    }

    /// Creates a deletion spanning the given range.
    #[must_use]
    pub fn delete_range(start: Position, end: Position) -> Self {
        Self::from_positions(start, end, ReplacementText::empty())
    }

    /// Creates a deletion spanning the given range (coordinate-based).
    ///
    /// Consider using [`Self::delete_range`] to reduce primitive obsession.
    #[must_use]
    pub const fn delete(
        start_line: u32,
        start_column: u32,
        end_line: u32,
        end_column: u32,
    ) -> Self {
        Self::from_coords(
            start_line,
            start_column,
            end_line,
            end_column,
            String::new(),
        )
    }

    // ... rest of the impl unchanged
}

AI Coding Agent Prompt

Refactor `crates/weaverd/src/safety_harness/edit.rs` to reduce primitive obsession by introducing a `ReplacementText` newtype and Position-based method variants:

Step 1: Add the ReplacementText newtype after the TextRange impl block, before the TextEdit struct definition:
- Define `pub struct ReplacementText(String);` with Debug, Clone, PartialEq, Eq derives
- Implement constructor methods:
  * `pub fn new(text: String) -> Self`
  * `pub const fn empty() -> Self` returning `Self(String::new())`
  * `pub fn as_str(&self) -> &str` returning `&self.0`
  * `pub fn into_inner(self) -> String` returning `self.0`
- Implement `From<String>`, `From<&str>`, and `AsRef<str>` traits for ergonomic conversions

Step 2: Add Position-based method variants to TextEdit:
- Add `pub fn with_replacement(range: TextRange, text: ReplacementText) -> Self` that calls `Self { range, new_text: text.into_inner() }`
- Add `pub fn from_positions(start: Position, end: Position, text: ReplacementText) -> Self` that calls `Self::with_replacement(TextRange::new(start, end), text)`
- Add `pub fn insert_at(position: Position, text: ReplacementText) -> Self` that calls `Self::with_replacement(TextRange::point(position), text)`
- Add `pub fn delete_range(start: Position, end: Position) -> Self` that calls `Self::from_positions(start, end, ReplacementText::empty())`

Step 3: Update doc comments:
- Add "Consider using [`Self::from_positions`] to reduce primitive obsession." to `from_coords` doc comment
- Add "Consider using [`Self::insert_at`] to reduce primitive obsession." to `insert` doc comment
- Add "Consider using [`Self::delete_range`] to reduce primitive obsession." to `delete` doc comment
- Document that `from_positions`, `insert_at`, and `delete_range` are preferred for reducing primitive obsession

Step 4: Place new methods immediately before their coordinate-based counterparts:
- `with_replacement` before `new`
- `from_positions` before `from_coords`
- `insert_at` before `insert`
- `delete_range` before `delete`

Do NOT update existing test code or external callers—the coordinate-based methods remain available for backward compatibility and LSP conventions.

Run `cargo test -p weaverd` to ensure all tests pass after the refactoring.

This refactoring reduces primitive obsession by:

  • Domain modelling: ReplacementText makes the intent explicit and prevents mixing text types
  • Position abstraction: New variants leverage Position instead of bypassing it with raw u32s
  • Backward compatibility: Coordinate-based methods remain for LSP compatibility and existing code
  • Progressive migration: Guidance in doc comments encourages use of domain-rich alternatives
  • Type safety: Compile-time guarantees that replacement text is used correctly

🐰✨

     />_/>
    ( O.O )
    />📦<\
   Wrapping primitives 
   in cosy domain types!

…s tests

- Added newtypes (FileName, FileContent, TextPattern, DiagnosticMessage) to replace raw strings in tests
- Updated safety harness behaviour tests to use these newtypes improving type safety and clarity
- Refactored helper functions and test steps accordingly
- Minor improvements and extraction of utility functions in safety_harness verification code for clarity

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Dec 10, 2025

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

crates/weaverd/src/tests/safety_harness_types.rs

Comment on file

//! Domain-specific NewTypes for safety harness behavioural tests.

❌ New issue: String Heavy Function Arguments
In this module, 44.4% of all arguments to its 26 functions are strings. The threshold for string arguments is 39.0%

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Dec 10, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the safety_harness_types.rs file to analyze string argument usage
cat crates/weaverd/src/tests/safety_harness_types.rs

Length of output: 3958


@leynos, I've analysed the file. The string-heaviness stems from redundant conversion trait implementations. Each NewType has three string-accepting methods: From<String>, From<&str>, and FromStr. The FromStr implementation is redundant—it simply delegates to From<&str> without adding value in test contexts. Removing FromStr will drop the string parameter percentage from 44.4% to 36.4%, comfortably below the 39% threshold.

Suggested Fix

Remove all FromStr implementations from the four NewType structs:

//! Domain-specific NewTypes for safety harness behavioural tests.
//!
//! These types eliminate string-heavy function arguments and make the test
//! domain model explicit and type-safe.

use std::ops::Deref;
use std::path::{Path, PathBuf};

/// Wraps file name strings.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FileName(String);

impl From<String> for FileName {
    fn from(s: String) -> Self {
        Self(s.trim_matches('"').to_string())
    }
}

impl From<&str> for FileName {
    fn from(s: &str) -> Self {
        Self(s.trim_matches('"').to_string())
    }
}

impl AsRef<str> for FileName {
    fn as_ref(&self) -> &str {
        &self.0
    }
}

impl Deref for FileName {
    type Target = str;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl FileName {
    /// Returns the inner string as a string slice.
    pub fn as_str(&self) -> &str {
        &self.0
    }

    /// Joins this file name to a base path.
    pub fn to_path(&self, base: &Path) -> PathBuf {
        base.join(&self.0)
    }
}

/// Wraps file content strings.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FileContent(String);

impl From<String> for FileContent {
    fn from(s: String) -> Self {
        Self(s.trim_matches('"').to_string())
    }
}

impl From<&str> for FileContent {
    fn from(s: &str) -> Self {
        Self(s.trim_matches('"').to_string())
    }
}

impl AsRef<str> for FileContent {
    fn as_ref(&self) -> &str {
        &self.0
    }
}

impl Deref for FileContent {
    type Target = str;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl FileContent {
    /// Returns the inner string as a string slice.
    pub fn as_str(&self) -> &str {
        &self.0
    }

    /// Returns the content as bytes.
    pub fn as_bytes(&self) -> &[u8] {
        self.0.as_bytes()
    }
}

/// Wraps text patterns for search/replace/assertion.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TextPattern(String);

impl From<String> for TextPattern {
    fn from(s: String) -> Self {
        Self(s.trim_matches('"').to_string())
    }
}

impl From<&str> for TextPattern {
    fn from(s: &str) -> Self {
        Self(s.trim_matches('"').to_string())
    }
}

impl AsRef<str> for TextPattern {
    fn as_ref(&self) -> &str {
        &self.0
    }
}

impl Deref for TextPattern {
    type Target = str;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl TextPattern {
    /// Returns the inner string as a string slice.
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

/// Wraps diagnostic messages for lock configuration.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiagnosticMessage(String);

impl From<String> for DiagnosticMessage {
    fn from(s: String) -> Self {
        Self(s.trim_matches('"').to_string())
    }
}

impl From<&str> for DiagnosticMessage {
    fn from(s: &str) -> Self {
        Self(s.trim_matches('"').to_string())
    }
}

impl AsRef<str> for DiagnosticMessage {
    fn as_ref(&self) -> &str {
        &self.0
    }
}

impl Deref for DiagnosticMessage {
    type Target = str;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl DiagnosticMessage {
    /// Returns the inner string as a string slice.
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

AI Coding Agent Prompt

Remove redundant FromStr implementations from `crates/weaverd/src/tests/safety_harness_types.rs` to reduce string-heavy function arguments below the 39% threshold.

Step 1: Remove the `use std::str::FromStr;` import from the top of the file.

Step 2: Delete the entire `FromStr` implementation block for `FileName`:
- Remove the impl block starting with `impl FromStr for FileName {` and all its contents

Step 3: Delete the entire `FromStr` implementation block for `FileContent`:
- Remove the impl block starting with `impl FromStr for FileContent {` and all its contents

Step 4: Delete the entire `FromStr` implementation block for `TextPattern`:
- Remove the impl block starting with `impl FromStr for TextPattern {` and all its contents

Step 5: Delete the entire `FromStr` implementation block for `DiagnosticMessage`:
- Remove the impl block starting with `impl FromStr for DiagnosticMessage {` and all its contents

Do NOT modify any other code. The From<String> and From<&str> implementations provide all necessary conversion functionality for test contexts.

Run `cargo test -p weaverd` to verify all tests pass after the refactoring.

This refactoring achieves:

  • Reduced string parameters: From 44.4% to 36.4% (below 39% threshold)
  • Eliminated redundancy: FromStr offered no advantage over From<&str> in test code
  • Maintained ergonomics: From traits with .into() are more idiomatic than str::parse() for NewTypes
  • Simpler maintenance: Fewer trait implementations to maintain per type

🐰✨

     />_/>
    ( O.O )
    />🧹<\
   Sweeping away 
   redundant traits!

…builder

- Introduced ReplacementText newtype to reduce primitive obsession on raw strings in text edits
- Added new constructors for TextEdit using ReplacementText for ergonomic and safer API
- Exposed ReplacementText from safety_harness module
- Added TransactionTestBuilder to simplify and reduce boilerplate in transaction tests
- Refactored tests to use TransactionTestBuilder, improving readability and maintainability

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Dec 10, 2025

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

crates/weaverd/src/safety_harness/transaction.rs

Comment on lines +343 to +364

    fn syntactic_failure_prevents_commit() {
        let builder = TransactionTestBuilder::new()
            .with_file("test.txt", "hello world")
            .with_replacement_edit(0, 0, 5, "greetings");

        let path = builder.file_path(0).clone();
        let failures = vec![VerificationFailure::new(path.clone(), "syntax error")];
        let syntactic = ConfigurableSyntacticLock::failing(failures);
        let semantic = ConfigurableSemanticLock::passing();

        let (result, _, _dir) = builder.execute_with_locks(&syntactic, &semantic);
        let outcome = result.expect("should succeed");

        assert!(matches!(
            outcome,
            TransactionOutcome::SyntacticLockFailed { .. }
        ));

        // File should be unchanged
        let content = fs::read_to_string(&path).expect("read file");
        assert_eq!(content, "hello world");
    }

❌ New issue: Code Duplication
The module contains 3 functions with similar structure: tests.semantic_backend_error_propagates,tests.semantic_failure_prevents_commit,tests.syntactic_failure_prevents_commit

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Dec 10, 2025

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

crates/weaverd/src/safety_harness/transaction.rs

Comment on lines +255 to +275

        fn with_replacement_edit(
            mut self,
            file_idx: usize,
            start_col: u32,
            end_col: u32,
            text: &str,
        ) -> Self {
            let path = self.files[file_idx].0.clone();
            let edit = FileEdit::with_edits(
                path,
                vec![TextEdit::from_coords(
                    0,
                    start_col,
                    0,
                    end_col,
                    text.to_string(),
                )],
            );
            self.edits.push(edit);
            self
        }

❌ New issue: Excess Number of Function Arguments
tests.TransactionTestBuilder.with_replacement_edit has 5 arguments, max arguments = 4

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Dec 10, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the transaction.rs file to analyze the test duplication
cat crates/weaverd/src/safety_harness/transaction.rs

Length of output: 16056


@leynos, I've analysed the three test functions. They share identical setup (builder configuration) and identical assertions (file unchanged check). Extract two helper functions to eliminate this duplication whilst maintaining test clarity.

Suggested Fix

Add two helper functions in the tests module, immediately after the temp_file function:

/// Creates the standard builder setup for failure scenario tests.
fn failure_scenario_builder() -> TransactionTestBuilder {
    TransactionTestBuilder::new()
        .with_file("test.txt", "hello world")
        .with_replacement_edit(0, 0, 5, "greetings")
}

/// Asserts that the file at the given path contains the original unchanged content.
fn assert_file_unchanged(path: &PathBuf) {
    let content = fs::read_to_string(path).expect("read file");
    assert_eq!(content, "hello world");
}

Then refactor the three test functions:

#[test]
fn syntactic_failure_prevents_commit() {
    let builder = failure_scenario_builder();
    let path = builder.file_path(0).clone();
    let failures = vec![VerificationFailure::new(path.clone(), "syntax error")];
    let syntactic = ConfigurableSyntacticLock::failing(failures);
    let semantic = ConfigurableSemanticLock::passing();

    let (result, _, _dir) = builder.execute_with_locks(&syntactic, &semantic);
    let outcome = result.expect("should succeed");

    assert!(matches!(
        outcome,
        TransactionOutcome::SyntacticLockFailed { .. }
    ));

    assert_file_unchanged(&path);
}

#[test]
fn semantic_failure_prevents_commit() {
    let builder = failure_scenario_builder();
    let path = builder.file_path(0).clone();
    let failures = vec![VerificationFailure::new(path.clone(), "type error")];
    let syntactic = ConfigurableSyntacticLock::passing();
    let semantic = ConfigurableSemanticLock::failing(failures);

    let (result, _, _dir) = builder.execute_with_locks(&syntactic, &semantic);
    let outcome = result.expect("should succeed");

    assert!(matches!(
        outcome,
        TransactionOutcome::SemanticLockFailed { .. }
    ));

    assert_file_unchanged(&path);
}

#[test]
fn semantic_backend_error_propagates() {
    let builder = failure_scenario_builder();
    let path = builder.file_path(0).clone();
    let syntactic = ConfigurableSyntacticLock::passing();
    let semantic = ConfigurableSemanticLock::unavailable("LSP crashed");

    let (result, _, _dir) = builder.execute_with_locks(&syntactic, &semantic);
    assert!(result.is_err());
    assert!(matches!(
        result.unwrap_err(),
        SafetyHarnessError::SemanticBackendUnavailable { .. }
    ));

    assert_file_unchanged(&path);
}

AI Coding Agent Prompt

Eliminate code duplication in `crates/weaverd/src/safety_harness/transaction.rs` by extracting common test setup and assertion logic for three failure scenario tests.

Step 1: Add two helper functions immediately after the `temp_file` function in the `tests` module:

- `failure_scenario_builder() -> TransactionTestBuilder`: Returns a `TransactionTestBuilder::new().with_file("test.txt", "hello world").with_replacement_edit(0, 0, 5, "greetings")`

- `assert_file_unchanged(path: &PathBuf)`: Reads the file at the given path and asserts its content equals "hello world" using `fs::read_to_string(path).expect("read file")` and `assert_eq!(content, "hello world")`

Step 2: Refactor `syntactic_failure_prevents_commit`:
- Replace the builder construction with `let builder = failure_scenario_builder();`
- Replace the final file assertion block (lines starting with "// File should be unchanged") with `assert_file_unchanged(&path);`

Step 3: Refactor `semantic_failure_prevents_commit`:
- Replace the builder construction with `let builder = failure_scenario_builder();`
- Replace the final file assertion block with `assert_file_unchanged(&path);`

Step 4: Refactor `semantic_backend_error_propagates`:
- Replace the builder construction with `let builder = failure_scenario_builder();`
- Replace the final file assertion block with `assert_file_unchanged(&path);`

Preserve all lock configuration, outcome assertions, and error handling logic in each test. Only extract the common builder setup and unchanged-file assertion.

Run `cargo test -p weaverd` to verify all tests pass after the refactoring.

This refactoring achieves:

  • Reduced duplication: ~50% less duplicated code across the three tests
  • Maintained clarity: Each test's unique variation points remain explicit
  • Single responsibility: Each helper does one thing well
  • Easy maintenance: Future changes to the standard scenario require updates in one place

🐰✨

     />_/>
    ( ^.^ )
    />🔧<\
   Extracting common 
   test patterns!

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Dec 10, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the transaction.rs file to examine the with_replacement_edit method and context
cat crates/weaverd/src/safety_harness/transaction.rs

Length of output: 16056


@leynos, I've analysed the with_replacement_edit method. The three edit-related parameters (start_col, end_col, and text) logically represent a single concept: a line replacement operation. Introduce a parameter object to encapsulate these three parameters and reduce the argument count from 5 to 3.

Suggested Fix

Add a parameter object for line replacement operations in the test module:

/// Represents a replacement operation on a single line for test scenarios.
#[derive(Debug, Clone)]
struct LineReplacement {
    start_col: u32,
    end_col: u32,
    text: String,
}

impl LineReplacement {
    /// Creates a new line replacement.
    fn new(start_col: u32, end_col: u32, text: impl Into<String>) -> Self {
        Self {
            start_col,
            end_col,
            text: text.into(),
        }
    }

    /// Creates a replacement that spans the entire content (from column 0 to end_col).
    fn from_start(end_col: u32, text: impl Into<String>) -> Self {
        Self::new(0, end_col, text)
    }
}

Update the with_replacement_edit method:

/// Adds a replacement edit for the file at the given index.
fn with_replacement_edit(mut self, file_idx: usize, replacement: LineReplacement) -> Self {
    let path = self.files[file_idx].0.clone();
    let edit = FileEdit::with_edits(
        path,
        vec![TextEdit::from_coords(
            0,
            replacement.start_col,
            0,
            replacement.end_col,
            replacement.text,
        )],
    );
    self.edits.push(edit);
    self
}

Update test call sites to use the parameter object:

// Before:
.with_replacement_edit(0, 0, 5, "greetings")

// After:
.with_replacement_edit(0, LineReplacement::from_start(5, "greetings"))

AI Coding Agent Prompt

Refactor the `with_replacement_edit` method in `crates/weaverd/src/safety_harness/transaction.rs` to reduce argument count by introducing a parameter object.

Step 1: Add a `LineReplacement` struct immediately before the `TransactionTestBuilder` struct definition in the tests module:
- Define `struct LineReplacement { start_col: u32, end_col: u32, text: String }`
- Add `#[derive(Debug, Clone)]`
- Implement `fn new(start_col: u32, end_col: u32, text: impl Into<String>) -> Self` that constructs the struct with `text.into()`
- Implement `fn from_start(end_col: u32, text: impl Into<String>) -> Self` that calls `Self::new(0, end_col, text)` for the common case of replacing from column 0

Step 2: Update the `with_replacement_edit` method signature to accept the parameter object:
- Change signature from `fn with_replacement_edit(mut self, file_idx: usize, start_col: u32, end_col: u32, text: &str) -> Self`
- To: `fn with_replacement_edit(mut self, file_idx: usize, replacement: LineReplacement) -> Self`
- Update the method body to use `replacement.start_col`, `replacement.end_col`, and `replacement.text` instead of the separate parameters
- Remove the `#[allow(clippy::too_many_arguments, ...)]` attribute as it's no longer needed

Step 3: Update all call sites in the test functions to use the parameter object:
- In `successful_transaction_commits_changes`: change `.with_replacement_edit(0, 0, 5, "greetings")` to `.with_replacement_edit(0, LineReplacement::from_start(5, "greetings"))`
- In `syntactic_failure_prevents_commit`: change `.with_replacement_edit(0, 0, 5, "greetings")` to `.with_replacement_edit(0, LineReplacement::from_start(5, "greetings"))`
- In `semantic_failure_prevents_commit`: change `.with_replacement_edit(0, 0, 5, "greetings")` to `.with_replacement_edit(0, LineReplacement::from_start(5, "greetings"))`
- In `semantic_backend_error_propagates`: change `.with_replacement_edit(0, 0, 5, "greetings")` to `.with_replacement_edit(0, LineReplacement::from_start(5, "greetings"))`
- In `handles_multiple_files`: this test doesn't use the builder pattern, so leave it unchanged

Run `cargo test -p weaverd` to ensure all tests pass after the refactoring.

This refactoring improves code health by:

  • Introducing Parameter Object pattern: Encapsulates three related parameters into a cohesive unit
  • Reducing argument count: From 5 to 3, meeting the threshold requirement
  • Improving intent clarity: LineReplacement::from_start(5, "greetings") explicitly documents what the edit does
  • Type safety: The parameter object ensures start/end columns and text stay together
  • Convenience factory: The from_start method provides ergonomic construction for the common case

🐰✨

     />_/>
    ( O.O )
    />📦<\
   Bundling related 
   parameters together!

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Dec 10, 2025

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

Excess Number of Function Arguments

crates/weaverd/src/safety_harness/transaction.rs: tests.TransactionTestBuilder.with_replacement_edit

What lead to degradation?

tests.TransactionTestBuilder.with_replacement_edit has 5 arguments, max arguments = 4

Why does this problem occur?

Functions with many arguments indicate either a) low cohesion where the function has too many responsibilities, or b) a missing abstraction that encapsulates those arguments.
The threshold for the Rust language is 4 function arguments.

How to fix it?

Start by investigating the responsibilities of the function. Make sure it doesn't do too many things, in which case it should be split into smaller and more cohesive functions. Consider the refactoring INTRODUCE PARAMETER OBJECT to encapsulate arguments that refer to the same logical concept.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Dec 10, 2025

Tip

For best results, initiate chat on the files or code changes.

@leynos, I've already provided a comprehensive fix for this exact issue earlier in our conversation thread above!

My previous response included:

  1. A LineReplacement parameter object that encapsulates the three edit-related parameters (start_col, end_col, text)
  2. The refactored with_replacement_edit method that accepts this parameter object, reducing arguments from 5 to 3
  3. A complete AI coding agent prompt with detailed step-by-step instructions for applying the refactoring
  4. Updated call sites showing how to use LineReplacement::from_start(5, "greetings") instead of the raw parameters

The solution follows the INTRODUCE PARAMETER OBJECT pattern you requested and reduces the argument count below the threshold whilst maintaining type safety and improving intent clarity.

If you're encountering any issues applying the previous fix, have questions about the LineReplacement design, or would like me to explain any part of the solution in more detail, I'm happy to help! Please let me know what specific aspect you'd like clarified.

…ion-based constructors

- Deprecated `from_coords` method in favor of safer `from_positions` using Position structs
- Added `from_positions` accepting String for convenience, promoting parameter object pattern
- Renamed original `from_positions` to `from_positions_typed` to clarify usage with ReplacementText newtype
- Updated delete constructors to use new position-based methods
- Added allow deprecated annotations in tests using legacy coordinate API

This improves type safety by reducing primitive coordinate usage and encourages better construction of text edits.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
@leynos leynos changed the title Implement CLI and Daemon with Double-Lock Safety Add Double-Lock safety harness for weaverd with atomic commits Dec 10, 2025
codescene-delta-analysis[bot]

This comment was marked as outdated.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

♻️ Duplicate comments (8)
crates/weaverd/src/tests/safety_harness_behaviour.rs (1)

210-238: Add execute-if-missing guard to these Then steps.

These Then steps assume a transaction has executed, but scenarios using when_edit_replaces_in_file only queue edits without executing. Add the same guard pattern used in then_commits and then_no_changes:

 #[then("the transaction fails with a syntactic lock error")]
 fn then_syntactic_fails(world: &RefCell<SafetyHarnessWorld>) {
+    if world.borrow().outcome().is_none() {
+        world.borrow_mut().execute_transaction();
+    }
     let world = world.borrow();
     let outcome = world.outcome().expect("outcome should exist");

Apply the same pattern to then_semantic_fails and then_backend_error.

crates/weaverd/src/safety_harness/transaction.rs (4)

1-546: Extract test module to comply with 400-line limit.

At 546 lines, this file exceeds the mandatory 400-line limit. The implementation (lines 1-241) is 241 lines; the test module (lines 243-546) adds 303 lines. Extract the test module to crates/weaverd/src/safety_harness/transaction/tests.rs.


227-241: Track file existence explicitly in rollback metadata.

The rollback logic infers "new file" from original.is_empty(), which incorrectly deletes existing empty files on rollback. Store file existence as Option<String> or a was_new: bool flag alongside original content.

-fn rollback(committed: &[(PathBuf, String)]) {
+fn rollback(committed: &[(PathBuf, Option<String>)]) {
     for (path, original) in committed {
-        if original.is_empty() {
-            // File was newly created, remove it
-            let _ = std::fs::remove_file(path);
-        } else {
-            // Restore original content (best effort)
-            let _ = std::fs::write(path, original);
+        match original {
+            None => {
+                // File was newly created, remove it
+                let _ = std::fs::remove_file(path);
+            }
+            Some(content) => {
+                // Restore original content (best effort)
+                let _ = std::fs::write(path, content);
+            }
         }
     }
 }

Update commit_changes to pass Option<String> instead of defaulting to empty string.


211-225: Create parent directories before writing temporary files.

tempfile::NamedTempFile::new_in(parent) fails if the parent directory does not exist. For new files in nested directories (e.g., src/foo/bar/new.rs), add directory creation:

 fn prepare_file(
     path: &std::path::Path,
     content: &str,
 ) -> Result<tempfile::NamedTempFile, SafetyHarnessError> {
     let parent = path.parent().unwrap_or_else(|| std::path::Path::new("."));

+    fs::create_dir_all(parent)
+        .map_err(|err| SafetyHarnessError::file_write(path.to_path_buf(), err))?;
+
     let mut temp_file = tempfile::NamedTempFile::new_in(parent)
         .map_err(|err| SafetyHarnessError::file_write(path.to_path_buf(), err))?;

480-486: Replace .unwrap_err() with .expect_err().

Per coding guidelines, prefer .expect() over .unwrap() in tests. Provide a descriptive message:

         assert!(result.is_err());
         assert!(matches!(
-            result.unwrap_err(),
+            result.expect_err("expected semantic backend unavailable error"),
             SafetyHarnessError::SemanticBackendUnavailable { .. }
         ));
crates/weaverd/src/safety_harness/verification.rs (3)

1-518: Decompose the file to comply with the 400-line limit.

This file contains 518 lines and violates the mandatory 400-line maximum. The past review comment at lines 1-5 provides a clear decomposition strategy:

  • verification/context.rsVerificationContext
  • verification/traits.rsSyntacticLock, SemanticLock, placeholders
  • verification/apply.rsapply_edits and offset helpers
  • verification/test_doubles.rsConfigurableSyntacticLock, ConfigurableSemanticLock

Execute that plan immediately.

As per coding guidelines: "Files must not exceed 400 lines in length."


342-518: Add test coverage for files with trailing newlines.

The test suite lacks cases where the original content ends with \n or \r\n. The past comment at lines 234-284 explicitly requested:

  • Multi-line content ending with newline
  • Multi-line content without trailing newline
  • Edits on the last line in both scenarios

Current tests (lines 478-517) only cover content without trailing newlines. Add tests such as:

In crates/weaverd/src/safety_harness/verification.rs around lines 342 to 518,
add unit tests to the tests module that cover: (1) a multi-line string ending
with \n where an edit targets the last line, (2) a multi-line string ending
with \r\n where an edit targets the last line, and (3) a multi-line string
WITHOUT a trailing newline where an edit targets the last line; verify that
apply_edits computes offsets correctly in all three cases and that edits are
applied at the expected positions; run cargo test -p weaverd to confirm.

Based on past review comments.


403-450: Parameterise the duplicated apply_edits tests with rstest.

The three tests apply_edits_inserts_text, apply_edits_deletes_text, and apply_edits_replaces_text share identical structure. Extract the common pattern into a single parameterised test.

Apply this refactoring:

+    use rstest::rstest;
+
+    #[rstest]
+    #[case("insert", "hello world", TextEdit::insert_at(Position::new(0, 6), "beautiful "), "hello beautiful world")]
+    #[case("delete", "hello beautiful world", TextEdit::delete_range(Position::new(0, 6), Position::new(0, 16)), "hello world")]
+    #[case("replace", "fn foo() {}", TextEdit::from_positions(Position::new(0, 3), Position::new(0, 6), "bar".to_string()), "fn bar() {}")]
+    fn apply_edits_single_operation(
+        #[case] _name: &str,
+        #[case] original: &str,
+        #[case] edit: TextEdit,
+        #[case] expected: &str,
+    ) {
+        let path = PathBuf::from("test.txt");
+        let file_edit = FileEdit::with_edits(path, vec![edit]);
+        let result = apply_edits(original, &file_edit).expect("edit should succeed");
+        assert_eq!(result, expected);
+    }
+
-    #[test]
-    fn apply_edits_inserts_text() {
-        use crate::safety_harness::edit::Position;
-
-        let original = "hello world";
-        let path = PathBuf::from("test.txt");
-        let edit = FileEdit::with_edits(
-            path,
-            vec![TextEdit::insert_at(Position::new(0, 6), "beautiful ")],
-        );
-        let result = apply_edits(original, &edit).expect("edit should succeed");
-        assert_eq!(result, "hello beautiful world");
-    }
-
-    #[test]
-    fn apply_edits_deletes_text() {
-        use crate::safety_harness::edit::Position;
-
-        let original = "hello beautiful world";
-        let path = PathBuf::from("test.txt");
-        let edit = FileEdit::with_edits(
-            path,
-            vec![TextEdit::delete_range(
-                Position::new(0, 6),
-                Position::new(0, 16),
-            )],
-        );
-        let result = apply_edits(original, &edit).expect("edit should succeed");
-        assert_eq!(result, "hello world");
-    }
-
-    #[test]
-    fn apply_edits_replaces_text() {
-        use crate::safety_harness::edit::Position;
-
-        let original = "fn foo() {}";
-        let path = PathBuf::from("test.rs");
-        let edit = FileEdit::with_edits(
-            path,
-            vec![TextEdit::from_positions(
-                Position::new(0, 3),
-                Position::new(0, 6),
-                "bar".to_string(),
-            )],
-        );
-        let result = apply_edits(original, &edit).expect("edit should succeed");
-        assert_eq!(result, "fn bar() {}");
-    }

Add rstest to dev-dependencies in crates/weaverd/Cargo.toml if not already present.

In crates/weaverd/src/safety_harness/verification.rs around lines 403 to 450,
replace the three individual test functions apply_edits_inserts_text,
apply_edits_deletes_text, and apply_edits_replaces_text with a single
parameterised rstest function named apply_edits_single_operation that accepts
case parameters for name, original, edit, and expected; define three #[case]
attributes with the test data from the original tests; move the common
FileEdit creation and apply_edits call into the parameterised function body;
ensure rstest is in dev-dependencies; run cargo test -p weaverd to verify all
cases pass.

As per coding guidelines: "Replace duplicated tests with #[rstest(...)] parameterised cases in Rust."

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b2e87ed and abbec3d.

📒 Files selected for processing (6)
  • crates/weaverd/src/safety_harness/edit.rs (1 hunks)
  • crates/weaverd/src/safety_harness/error.rs (1 hunks)
  • crates/weaverd/src/safety_harness/mod.rs (1 hunks)
  • crates/weaverd/src/safety_harness/transaction.rs (1 hunks)
  • crates/weaverd/src/safety_harness/verification.rs (1 hunks)
  • crates/weaverd/src/tests/safety_harness_behaviour.rs (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Run make check-fmt, make lint, and make test before committing, which execute cargo fmt --workspace -- --check, cargo clippy --workspace --all-targets --all-features -- -D warnings, and cargo test --workspace respectively.
Clippy warnings MUST be disallowed.
Fix any warnings emitted during tests in the code itself rather than silencing them.
Where a function is too long, extract meaningfully named helper functions adhering to separation of concerns and CQRS.
Where a function has too many parameters, group related parameters in meaningfully named structs.
Where a function is returning a large error, consider using Arc to reduce the amount of data returned.
Write unit and behavioural tests for new functionality; run both before and after making any change.
Every module must begin with a module level (//!) comment explaining the module's purpose and utility.
Document public APIs using Rustdoc comments (///) so documentation can be generated with cargo doc.
Prefer immutable data and avoid unnecessary mut bindings.
Handle errors with the Result type instead of panicking where feasible.
Avoid unsafe code unless absolutely necessary and document any usage clearly.
Place function attributes after doc comments in Rust.
Do not use return in single-line functions in Rust.
Use predicate functions for conditional criteria with more than two branches in Rust.
Lints must not be silenced except as a last resort; lint rule suppressions must be tightly scoped and include a clear reason.
Prefer expect over allow in Rust.
Use rstest fixtures for shared setup in Rust tests.
Replace duplicated tests with #[rstest(...)] parameterised cases in Rust.
Prefer mockall for mocks/stubs in Rust.
Prefer .expect() over .unwrap() in Rust.
Use concat!() to combine long string literals in Rust rather than escaping newlines with a backslash.
Prefer single-line function definitions in Rust where appropriate (e.g., `pub fn new(id: u64) -> Self { ...

Files:

  • crates/weaverd/src/safety_harness/mod.rs
  • crates/weaverd/src/tests/safety_harness_behaviour.rs
  • crates/weaverd/src/safety_harness/verification.rs
  • crates/weaverd/src/safety_harness/error.rs
  • crates/weaverd/src/safety_harness/transaction.rs
  • crates/weaverd/src/safety_harness/edit.rs

⚙️ CodeRabbit configuration file

**/*.rs: * Seek to keep the cognitive complexity of functions no more than 9.

  • Adhere to single responsibility and CQRS
  • Place function attributes after doc comments.
  • Do not use return in single-line functions.
  • Move conditionals with >2 branches into a predicate function.
  • Avoid unsafe unless absolutely necessary.
  • Every module must begin with a //! doc comment that explains the module's purpose and utility.
  • Comments and docs must follow en-GB-oxendict (-ize / -yse / -our) spelling and grammar
  • Lints must not be silenced except as a last resort.
    • #[allow] is forbidden.
    • Only narrowly scoped #[expect(lint, reason = "...")] is allowed.
    • No lint groups, no blanket or file-wide suppression.
    • Include FIXME: with link if a fix is expected.
  • Where code is only used by specific features, it must be conditionally compiled or a conditional expectation for unused_code applied.
  • Use rstest fixtures for shared setup and to avoid repetition between tests.
  • Replace duplicated tests with #[rstest(...)] parameterised cases.
  • Prefer mockall for mocks/stubs.
  • Prefer .expect() over .unwrap() in tests.
  • .expect() and .unwrap() are forbidden outside of tests. Errors must be propagated.
  • Ensure that any API or behavioural changes are reflected in the documentation in docs/
  • Ensure that any completed roadmap steps are recorded in the appropriate roadmap in docs/
  • Files must not exceed 400 lines in length
    • Large modules must be decomposed
    • Long match statements or dispatch tables should be decomposed by domain and collocated with targets
    • Large blocks of inline data (e.g., test fixtures, constants or templates) must be moved to external files and inlined at compile-time or loaded at run-time.
  • Environment access (env::set_var and env::remove_var) are always unsafe in Rust 2024 and MUST be marked as such
    • For testing of functionality depending upon environment variables, dependency injection and...

Files:

  • crates/weaverd/src/safety_harness/mod.rs
  • crates/weaverd/src/tests/safety_harness_behaviour.rs
  • crates/weaverd/src/safety_harness/verification.rs
  • crates/weaverd/src/safety_harness/error.rs
  • crates/weaverd/src/safety_harness/transaction.rs
  • crates/weaverd/src/safety_harness/edit.rs
🧬 Code graph analysis (1)
crates/weaverd/src/tests/safety_harness_behaviour.rs (5)
crates/weaverd/src/safety_harness/edit.rs (11)
  • path (218-220)
  • new (23-25)
  • new (40-42)
  • new (65-67)
  • new (123-128)
  • new (198-203)
  • from_positions (135-137)
  • with_edits (212-214)
  • insert_at (143-145)
  • from (89-91)
  • from (95-97)
crates/weaverd/src/safety_harness/transaction.rs (3)
  • new (67-73)
  • new (308-314)
  • new (331-337)
crates/weaverd/src/safety_harness/verification.rs (6)
  • new (29-34)
  • passing (152-154)
  • passing (187-192)
  • failing (158-160)
  • failing (196-201)
  • unavailable (205-210)
crates/weaverd/src/safety_harness/error.rs (5)
  • new (27-34)
  • file (46-48)
  • line (52-54)
  • column (58-60)
  • message (64-66)
crates/weaverd/src/tests/safety_harness_types.rs (8)
  • from (17-19)
  • from (23-25)
  • from (54-56)
  • from (60-62)
  • from (91-93)
  • from (97-99)
  • from (123-125)
  • from (129-131)
🔍 Remote MCP Deepwiki

Summary of additional review-relevant facts (concise)

  • Scope & placement

    • New public crate module: crates/weaverd/src/safety_harness with submodules edit, error, locks, transaction, verification; exposed via pub mod safety_harness.
  • Public API surface added

    • New public types re-exported: FileEdit, Position, ReplacementText, TextEdit, TextRange, SafetyHarnessError, VerificationFailure, SemanticLockResult, SyntacticLockResult, EditTransaction, TransactionOutcome, VerificationContext, SyntacticLock, SemanticLock, and assorted lock/verification helper types. (Verify consumers for downstream uses).
  • Behavior & guarantees

    • EditTransaction implements in‑memory application of edits, two‑phase commit using temp files (prepare → atomic rename) with rollback semantics on failure; TransactionOutcome covers Committed, SyntacticLockFailed, SemanticLockFailed, NoChanges. Tests cover multi-file atomicity and new-file creation.
  • Error & diagnostic model

    • Structured error types: SafetyHarnessError enum and VerificationFailure (file + optional line/column + message) with Display impl — used to report lock failures and I/O/edit application errors.
  • Verification model & extensibility

    • Pluggable traits SyntacticLock and SemanticLock with Placeholder and Configurable test doubles provided; VerificationContext holds original/modified contents for validators. apply_edits and line/column → offset helpers included and covered by tests.
  • Tests added

    • Unit tests across new modules (edit/error/locks/transaction/verification).
    • BDD behavioural tests + feature file: crates/weaverd/src/tests/safety_harness_behaviour.rs and crates/weaverd/tests/features/safety_harness.feature exercising success, syntactic/semantic failures, backend-unavailable, empty transactions, atomic multi-file commits, rollback. New test support types in safety_harness_types.rs.
  • Docs & roadmap

    • docs/users-guide.md, docs/weaver-design.md, docs/roadmap.md updated to document the Double‑Lock safety harness, two‑phase verification, in‑memory edits, atomic commits, and future LSP doc-sync integration.
  • Build/dev impact

    • crates/weaverd/Cargo.toml adds a dev-dependency: derive_more = "1.0" with features as_ref,deref — affects dev/test builds only. Confirm CI/test matrix includes weaverd tests.

Actionable review notes (check during code review)

  • API stability: confirm no accidental changes to existing exported symbols beyond the new safety_harness re-exports.
  • Correctness of atomic commit/rollback (race conditions, temp-file placement, permission/ownership edge cases).
  • apply_edits correctness for mixed/CRLF line endings and offset computations (helpers present; verify edge-case tests pass).
  • Error propagation: ensure SafetyHarnessError variants preserve underlying std::io::Error context where needed.
  • Test reliability: BDD tests spawn daemons and depend on temporary sockets — confirm fixtures clean up and are stable in CI.
  • Docs consistency: verify user guide and design docs match the implemented behavior and public API names.

Sources

  • Repo wiki / code documentation pages (contains synthesized content from repository files): Deepwiki_read_wiki_contents (leynos/weaver).
⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Sourcery review
🔇 Additional comments (3)
crates/weaverd/src/safety_harness/mod.rs (1)

39-42: Test doubles exported in public API.

ConfigurableSemanticLock, ConfigurableSyntacticLock, PlaceholderSemanticLock, and PlaceholderSyntacticLock are test doubles. Exporting them publicly is intentional for downstream testing, but document this intent or consider a testing feature flag to gate these exports if the API surface becomes a concern.

crates/weaverd/src/safety_harness/transaction.rs (1)

405-413: LGTM - empty transaction test.

Clean test for the no-changes case with proper .expect() usage.

crates/weaverd/src/safety_harness/edit.rs (1)

235-269: LGTM - well-structured unit tests.

The tests cover insertion, deletion, and file edit tracking with clear assertions.

Comment thread crates/weaverd/src/safety_harness/edit.rs Outdated
Comment thread crates/weaverd/src/safety_harness/edit.rs
Comment thread crates/weaverd/src/safety_harness/error.rs
Comment thread crates/weaverd/src/safety_harness/error.rs
Comment thread crates/weaverd/src/safety_harness/verification.rs Outdated
Comment thread crates/weaverd/src/tests/safety_harness_behaviour.rs Outdated
Comment thread crates/weaverd/src/tests/safety_harness_behaviour.rs
@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Dec 10, 2025

@coderabbitai Have the following now been resolved?

Please address the comments from this code review:

## Overall Comments
- The `commit_changes`/`write_file_atomic` flow guarantees atomicity per file but not across multiple files: if a later file write fails, earlier files remain updated, which contradicts the documented "multi-file transaction" semantics; consider either implementing a rollback strategy or clarifying the docs to state per-file atomicity only.
- In `apply_edits`, `line_column_to_offset` and `calculate_line_start_offset` assume a newline length of 1 byte, so offsets will be wrong on CRLF (`\r\n`) files; you may want to base offsets on the original byte indices (e.g. iterating over `char_indices` or tracking actual newline sequences) instead of assuming single-byte newlines.
- The `SafetyHarnessError::VerificationFailed` variants and helpers (`syntactic_failed`/`semantic_failed`) are currently unused because transaction code returns `TransactionOutcome::{Syntactic,Semantic}LockFailed` instead; consider either wiring the transaction layer to use `VerificationFailed` for consistency or removing/reducing the unused error variants to simplify the error surface.

## Individual Comments

### Comment 1
<location> `crates/weaverd/src/safety_harness/edit.rs:69-71` </location>
<code_context>
+        Self(text)
+    }
+
+    /// Creates an empty replacement text (for deletions).
+    #[must_use]
+    pub const fn empty() -> Self {
+        Self(String::new())
+    }
</code_context>

<issue_to_address>
**issue (bug_risk):** `ReplacementText::empty` cannot be `const fn` because `String::new()` is not const

This won’t compile on stable Rust because `String::new()` isn’t allowed in a `const fn`. Either remove `const` from `empty` or refactor the API to avoid constructing a `String` in a const context (for example, return a `&'static str` and convert later). Given current usage, making `empty` a regular `fn` seems the simplest fix.
</issue_to_address>

### Comment 2
<location> `crates/weaverd/src/safety_harness/verification.rs:239-248` </location>
<code_context>
+    let lines: Vec<&str> = original.lines().collect();
</code_context>

<issue_to_address>
**issue (bug_risk):** Offset computation assumes `\n` line endings and will misbehave on `\r\n` files

Using `str::lines()` and adding a fixed `+1` per line assumes single-byte terminators; on CRLF input the offsets will drift and `replace_range` edits will be wrong. If CRLF files are in scope, derive offsets from the original string while counting actual newline lengths (e.g., scan for `
` while checking for a preceding `
`, or split with a method that preserves line-ending sizes).
</issue_to_address>

### Comment 3
<location> `crates/weaverd/src/safety_harness/edit.rs:118` </location>
<code_context>
+    new_text: String,
+}
+
+impl TextEdit {
+    /// Builds a text edit from a range and typed replacement text.
+    ///
</code_context>

<issue_to_address>
**issue (complexity):** Consider collapsing the multiple overlapping `TextEdit` constructors into a small canonical set and making `ReplacementText` an internal or optional detail to simplify the public API.

You can keep all current behaviour while shrinking the public surface and the “choice overload” quite a bit by:

1. **Collapsing overlapping constructors on `TextEdit`**
2. **Making `ReplacementText` an internal helper (or at least not required at call sites)**

### 1. Collapse the constructor set

Right now you expose both:
- typed vs primitive (`Position`/`TextRange` vs coords), and  
- `String` vs `ReplacementText`.

You can keep your preferred idioms but expose a much smaller public API by having a **single canonical constructor per operation** and making the rest private helpers or deprecated shims.

For example:

```rust
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TextEdit {
    range: TextRange,
    new_text: String,
}

impl TextEdit {
    // Core constructor: range + text
    #[must_use]
    pub fn new(range: TextRange, new_text: impl Into<String>) -> Self {
        Self {
            range,
            new_text: new_text.into(),
        }
    }

    // Canonical "from positions"
    #[must_use]
    pub fn from_positions(start: Position, end: Position, new_text: impl Into<String>) -> Self {
        Self::new(TextRange::new(start, end), new_text)
    }

    // Canonical insert: uses Position, not coordinates
    #[must_use]
    pub fn insert(position: Position, new_text: impl Into<String>) -> Self {
        Self::new(TextRange::point(position), new_text)
    }

    // Canonical delete: uses a TextRange
    #[must_use]
    pub fn delete(range: TextRange) -> Self {
        Self::new(range, String::new())
    }

    // Optionally: convenience delete from positions (just delegates)
    #[must_use]
    pub fn delete_positions(start: Position, end: Position) -> Self {
        Self::delete(TextRange::new(start, end))
    }
}
```

Then your current coordinate-based methods become **thin, deprecated shims** (or even free functions) to retain compatibility without bloating the main API:

```rust
impl TextEdit {
    #[deprecated(since = "0.2.0", note = "use `from_positions` instead")]
    #[must_use]
    pub fn from_coords(
        start_line: u32,
        start_column: u32,
        end_line: u32,
        end_column: u32,
        new_text: impl Into<String>,
    ) -> Self {
        Self::from_positions(
            Position::new(start_line, start_column),
            Position::new(end_line, end_column),
            new_text,
        )
    }

    #[deprecated(since = "0.2.0", note = "use `insert(Position, ..)` instead")]
    #[must_use]
    pub fn insert_coords(line: u32, column: u32, new_text: impl Into<String>) -> Self {
        Self::insert(Position::new(line, column), new_text)
    }

    #[deprecated(since = "0.2.0", note = "use `delete(TextRange)` or `delete_positions` instead")]
    #[must_use]
    pub fn delete_coords(
        start_line: u32,
        start_column: u32,
        end_line: u32,
        end_column: u32,
    ) -> Self {
        Self::delete(TextRange::new(
            Position::new(start_line, start_column),
            Position::new(end_line, end_column),
        ))
    }
}
```

This keeps **all** capabilities (coords vs positions, insert/delete/replace) but gives callers a clear, minimal “happy path” API to learn: `new`, `from_positions`, `insert(Position, ..)`, `delete(TextRange)`.

### 2. De-emphasise `ReplacementText` in the public API

`ReplacementText` doesn’t currently add invariants and forces duplicate methods. You can still keep it for internal clarity or future invariants, but you don’t need to expose it as a primary parameter type.

Option A – make it an internal detail:

```rust
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReplacementText(String);

impl ReplacementText {
    #[must_use]
    pub fn new(text: impl Into<String>) -> Self {
        Self(text.into())
    }

    #[must_use]
    pub fn empty() -> Self {
        Self(String::new())
    }

    #[must_use]
    pub fn into_inner(self) -> String {
        self.0
    }
}

// Not public (or pub(crate)):
pub(crate) fn replacement(text: impl Into<String>) -> ReplacementText {
    ReplacementText::new(text)
}
```

And use it only inside `TextEdit` if you like the “primitive obsession” abstraction, without it leaking into your public surface:

```rust
impl TextEdit {
    #[must_use]
    pub fn delete(range: TextRange) -> Self {
        // Internally: still uses ReplacementText
        let text = ReplacementText::empty();
        Self::new(range, text.into_inner())
    }
}
```

Option B – if you want to keep it public, remove the parallel API and accept anything that can be converted:

```rust
impl TextEdit {
    #[must_use]
    pub fn new(range: TextRange, new_text: impl Into<String>) -> Self {
        Self { range, new_text: new_text.into() }
    }

    #[must_use]
    pub fn insert(position: Position, new_text: impl Into<String>) -> Self {
        Self::new(TextRange::point(position), new_text)
    }

    #[must_use]
    pub fn from_positions(start: Position, end: Position, new_text: impl Into<String>) -> Self {
        Self::new(TextRange::new(start, end), new_text)
    }
}
```

Callers who **want** to use `ReplacementText` can pass it because it implements `Into<String>`, but you don’t need `with_replacement`, `from_positions_typed`, `insert_at`, etc.

---

These changes preserve all existing functionality, keep `FileEdit` untouched, but reduce the cognitive surface to a small, orthogonal set of constructors while still allowing your preferred patterns internally.
</issue_to_address>

### Comment 4
<location> `crates/weaverd/src/safety_harness/edit.rs:168` </location>
<code_context>
+        }
+
+        /// Adds a replacement edit for the file at the given index.
+        #[allow(
+            clippy::too_many_arguments,
+            reason = "test builder accepts explicit edit coordinates for convenience"
</code_context>

<issue_to_address>
**issue (review_instructions):** `#[allow(clippy::too_many_arguments)]` violates the lint-suppression policy; use a narrowly scoped `#[expect(..., reason = "...")]` instead or refactor the signature.

The review instructions forbid `#[allow]` and require narrowly scoped `#[expect(lint, reason = "...")]` as a last resort. Here we should either:

* Refactor `from_coords` so it no longer needs `too_many_arguments`, or
* Replace this with `#[expect(clippy::too_many_arguments, reason = "matches LSP coordinate convention")]` if we explicitly decide to keep this deprecated helper.

As written, this `#[allow]` conflicts with the lint policy.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*.rs`

**Instructions:**
Lints must not be silenced except as a last resort. `#[allow]` is forbidden. Only narrowly scoped `#[expect(lint, reason = "...")]` is allowed.

</details>
</issue_to_address>

### Comment 5
<location> `crates/weaverd/src/safety_harness/transaction.rs:284` </location>
<code_context>
+        }
+
+        /// Adds a replacement edit for the file at the given index.
+        #[allow(
+            clippy::too_many_arguments,
+            reason = "test builder accepts explicit edit coordinates for convenience"
</code_context>

<issue_to_address>
**issue (review_instructions):** The `#[allow(clippy::too_many_arguments)]` on the test builder method conflicts with the lint-suppression rules; switch to a scoped `#[expect(...)]` with a reason or adjust the API.

The guidelines disallow `#[allow]` and only permit narrowly scoped `#[expect(lint, reason = "...")]` as a last resort. For `with_replacement_edit` you can:

* Consider using a parameter object (e.g. `Position`/`TextRange`) or a small struct to avoid `too_many_arguments`, or
* If the explicit coordinates are intentional for tests, replace this with `#[expect(clippy::too_many_arguments, reason = "test builder uses explicit edit coordinates for clarity")]`.

Keeping a bare `#[allow]` here violates the specified lint policy.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*.rs`

**Instructions:**
Lints must not be silenced except as a last resort. `#[allow]` is forbidden. Only narrowly scoped `#[expect(lint, reason = "...")]` is allowed.

</details>
</issue_to_address>

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Dec 10, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the current state of the key files to verify issue resolution
echo "=== edit.rs ==="
cat crates/weaverd/src/safety_harness/edit.rs

echo -e "\n\n=== verification.rs ==="
cat crates/weaverd/src/safety_harness/verification.rs

echo -e "\n\n=== transaction.rs ==="
cat crates/weaverd/src/safety_harness/transaction.rs

echo -e "\n\n=== error.rs ==="
cat crates/weaverd/src/safety_harness/error.rs

Length of output: 47870


@leynos, Yes! All the issues you listed have been resolved.

Here's the verification summary:

Overall Comments

1. Multi-file atomicity ✅ RESOLVED

The commit_changes function in transaction.rs now implements a proper two-phase commit with rollback:

  • Phase 1 (prepare): All files written to temporary files
  • Phase 2 (commit): Atomic renames to targets
  • Rollback on failure: The rollback() function restores original content for modified files and removes newly created files
fn commit_changes(...) -> Result<(), SafetyHarnessError> {
    // Phase 1: Prepare all files (write to temps)
    let mut prepared: Vec<(PathBuf, tempfile::NamedTempFile, String)> = Vec::new();
    // ... preparation code ...
    
    // Phase 2: Commit all files (atomic renames)
    for (path, temp_file, original) in prepared {
        if let Err(err) = temp_file.persist(&path) {
            rollback(&committed);  // ← Rollback on any failure
            return Err(...);
        }
        committed.push((path, original));
    }
}

2. CRLF handling ✅ RESOLVED

The offset computation in verification.rs now correctly handles both LF and CRLF:

  • compute_line_start_offsets scans for actual \n bytes
  • line_column_to_offset detects CRLF by checking for \r before \n
  • Tests cover both CRLF and mixed line endings
let line_content_end = if line_end > 0 && content.as_bytes().get(line_end - 1) == Some(&b'\n') {
    if line_end > 1 && content.as_bytes().get(line_end - 2) == Some(&b'\r') {
        line_end - 2 // CRLF
    } else {
        line_end - 1 // LF
    }
} else {
    line_end // Last line without trailing newline
};

3. Unused error variants ✅ RESOLVED

Looking at error.rs, there are no unused VerificationFailed variants or helpers. The SafetyHarnessError enum contains only operational errors (FileReadError, FileWriteError, EditApplicationError, SemanticBackendUnavailable, SyntacticBackendUnavailable), all of which are used. Verification failures are correctly returned as TransactionOutcome variants, not errors.

Individual Comments

Comment 1: ReplacementText::empty const fn ✅ RESOLVED

The empty() method is no longer marked as const fn:

pub fn empty() -> Self {
    Self(String::new())
}

Comment 2: CRLF offset computation ✅ RESOLVED

Same as overall comment #2 above.

Comment 3: API complexity ✅ RESOLVED

The API has been significantly simplified:

  • Position-based methods only: new, from_positions, insert_at, delete_range
  • No coordinate-based methods: from_coords, insert, delete have been completely removed
  • ReplacementText remains public but is optional via impl Into<String> parameters

The cleaner API provides a single canonical way to perform each operation.

Comment 4: #[allow] in edit.rs ✅ RESOLVED

No #[allow(clippy::too_many_arguments)] attribute found anywhere in edit.rs. The coordinate-based methods that required the lint suppression have been removed entirely.

Comment 5: #[allow] in transaction.rs ✅ RESOLVED

The with_replacement_edit method now uses the LineReplacement parameter object, reducing arguments from 5 to 3:

fn with_replacement_edit(mut self, file_idx: usize, replacement: LineReplacement) -> Self

No #[allow] attribute needed or present.


Summary: All issues have been successfully addressed through refactoring. The code now has proper multi-file transaction semantics with rollback, correct CRLF handling, a cleaner API surface, and no lint suppressions. 🎯

🐰✨

     />_/>
    ( ✓.✓ )
    />✨<\
   All issues resolved!
   Clean code achieved!

…dits

Add tests to verify that:
1. Edits targeting lines past EOF are rejected with EditApplicationError
   rather than causing a panic
2. End-of-file positions (last column of last line) work correctly

The implementation already handles these cases correctly via the
line_starts.get() returning None for invalid line indices.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (2)
crates/weaverd/src/safety_harness/verification.rs (2)

1-233: Split this module; file-length exceeds 400-line guideline

Keep this file under the 400-line limit from the coding guidelines. Extract subdomains into submodules so verification.rs only wires them together.

Refactor along these lines:

  • Move VerificationContext into verification/context.rs.
  • Move SyntacticLock / SemanticLock traits and placeholder locks into verification/traits.rs.
  • Move apply_edits, compute_line_start_offsets, and line_column_to_offset into verification/apply.rs.
  • Move ConfigurableSyntacticLock and ConfigurableSemanticLock into a dedicated test-support module such as verification/test_doubles.rs.

Re-export the public surface from this root module and update use paths in transaction code and tests.


234-340: Keep CRLF-safe offset logic and add explicit trailing-newline coverage; extract newline helper

The new apply_edits / compute_line_start_offsets / line_column_to_offset pipeline now:

  • Computes line starts from raw bytes and handles both LF and CRLF correctly.
  • Applies edits from bottom to top so offsets remain valid.
  • Rejects past-EOF lines cleanly via EditApplicationError.

Retain this structure, but strengthen and simplify it:

  • Add unit tests that cover multi-line content ending with a trailing newline (both LF and CRLF), including:
    • Replacement on the last non-empty line.
    • Insertion at the final empty line (EOF) position.
  • Extract the CRLF/LF trimming logic into a helper to reduce branching in line_column_to_offset, along the lines of:
fn line_content_length(content: &str, line_start: usize, line_end: usize) -> usize {
    let adjusted_end = if line_end > 0 && content.as_bytes().get(line_end - 1) == Some(&b'\n') {
        if line_end > 1 && content.as_bytes().get(line_end - 2) == Some(&b'\r') {
            line_end - 2
        } else {
            line_end - 1
        }
    } else {
        line_end
    };
    adjusted_end.saturating_sub(line_start)
}

and then use line_content_length inside line_column_to_offset to compute line_len. This drops nested conditionals and keeps the offset function easier to audit.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between abbec3d and b8017b2.

📒 Files selected for processing (1)
  • crates/weaverd/src/safety_harness/verification.rs (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Run make check-fmt, make lint, and make test before committing, which execute cargo fmt --workspace -- --check, cargo clippy --workspace --all-targets --all-features -- -D warnings, and cargo test --workspace respectively.
Clippy warnings MUST be disallowed.
Fix any warnings emitted during tests in the code itself rather than silencing them.
Where a function is too long, extract meaningfully named helper functions adhering to separation of concerns and CQRS.
Where a function has too many parameters, group related parameters in meaningfully named structs.
Where a function is returning a large error, consider using Arc to reduce the amount of data returned.
Write unit and behavioural tests for new functionality; run both before and after making any change.
Every module must begin with a module level (//!) comment explaining the module's purpose and utility.
Document public APIs using Rustdoc comments (///) so documentation can be generated with cargo doc.
Prefer immutable data and avoid unnecessary mut bindings.
Handle errors with the Result type instead of panicking where feasible.
Avoid unsafe code unless absolutely necessary and document any usage clearly.
Place function attributes after doc comments in Rust.
Do not use return in single-line functions in Rust.
Use predicate functions for conditional criteria with more than two branches in Rust.
Lints must not be silenced except as a last resort; lint rule suppressions must be tightly scoped and include a clear reason.
Prefer expect over allow in Rust.
Use rstest fixtures for shared setup in Rust tests.
Replace duplicated tests with #[rstest(...)] parameterised cases in Rust.
Prefer mockall for mocks/stubs in Rust.
Prefer .expect() over .unwrap() in Rust.
Use concat!() to combine long string literals in Rust rather than escaping newlines with a backslash.
Prefer single-line function definitions in Rust where appropriate (e.g., `pub fn new(id: u64) -> Self { ...

Files:

  • crates/weaverd/src/safety_harness/verification.rs

⚙️ CodeRabbit configuration file

**/*.rs: * Seek to keep the cognitive complexity of functions no more than 9.

  • Adhere to single responsibility and CQRS
  • Place function attributes after doc comments.
  • Do not use return in single-line functions.
  • Move conditionals with >2 branches into a predicate function.
  • Avoid unsafe unless absolutely necessary.
  • Every module must begin with a //! doc comment that explains the module's purpose and utility.
  • Comments and docs must follow en-GB-oxendict (-ize / -yse / -our) spelling and grammar
  • Lints must not be silenced except as a last resort.
    • #[allow] is forbidden.
    • Only narrowly scoped #[expect(lint, reason = "...")] is allowed.
    • No lint groups, no blanket or file-wide suppression.
    • Include FIXME: with link if a fix is expected.
  • Where code is only used by specific features, it must be conditionally compiled or a conditional expectation for unused_code applied.
  • Use rstest fixtures for shared setup and to avoid repetition between tests.
  • Replace duplicated tests with #[rstest(...)] parameterised cases.
  • Prefer mockall for mocks/stubs.
  • Prefer .expect() over .unwrap() in tests.
  • .expect() and .unwrap() are forbidden outside of tests. Errors must be propagated.
  • Ensure that any API or behavioural changes are reflected in the documentation in docs/
  • Ensure that any completed roadmap steps are recorded in the appropriate roadmap in docs/
  • Files must not exceed 400 lines in length
    • Large modules must be decomposed
    • Long match statements or dispatch tables should be decomposed by domain and collocated with targets
    • Large blocks of inline data (e.g., test fixtures, constants or templates) must be moved to external files and inlined at compile-time or loaded at run-time.
  • Environment access (env::set_var and env::remove_var) are always unsafe in Rust 2024 and MUST be marked as such
    • For testing of functionality depending upon environment variables, dependency injection and...

Files:

  • crates/weaverd/src/safety_harness/verification.rs
🔍 Remote MCP Ref

Summary of additional review-relevant facts found on the PR page (concise):

  • PR metadata: Title "Add Double-Lock safety harness for weaverd with atomic commits (PR #34)", author leynos, source branch terragon/implement-double-lock-safety-x8eg4y -> main, created 2025-12-08, URL #34.,

  • Files changed (high-level): new crate module crates/weaverd/src/safety_harness with submodules edit.rs, error.rs, locks.rs, transaction.rs, verification.rs; tests added under crates/weaverd/src/tests and crates/weaverd/tests/features; docs updated (docs/users-guide.md, docs/weaver-design.md, docs/roadmap.md); Cargo dev-dep derive_more added for weaverd. (Matches PR description + file list on PR page.)

  • Tests: extensive unit tests and BDD behavioural tests added; feature file present for rstest_bdd scenarios covering commit, lock failures, backend unavailability, empty transactions, multi-file atomicity, new-file creation. Reviewer should run cargo test -p weaverd (CI matrix not shown on PR page).

  • Review action items visible on PR page: watch for API surface expansion (new public re-exports), correctness of two-phase commit/rollback across multiple files, apply_edits offset/CRLF correctness, error propagation preserving io error context, and test stability in CI. (These align with PR description and visible diffs.)

Sources consulted:

  • PR listing/search result and PR page: #34,

Comment thread crates/weaverd/src/safety_harness/verification.rs
Comment thread crates/weaverd/src/safety_harness/verification.rs Outdated
Comment thread crates/weaverd/src/safety_harness/verification.rs Outdated
…ansactions with verification

- Introduce two-phase commit for edit transactions: prepare writes to temp files; commit performs atomic renames.
- Track file existence to support rollback that restores original state, including removing newly created files.
- Create configurable syntactic and semantic verification locks for test scenarios.
- Moves text edit application logic to verification/apply.rs with robust handling of LF and CRLF line endings.
- Add extensive tests covering success, syntactic and semantic lock failures, backend errors, new file creation, and multi-file edits.
- Update docs to reflect verification and atomic transaction behavior and acceptance criteria.

This change enforces safety harness guarantees that multi-file edits are either fully committed or fully rolled back on failure, with verification locks gating commits.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Dec 10, 2025

@coderabbitai review

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Dec 10, 2025

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

crates/weaverd/src/safety_harness/verification/apply.rs

Comment on lines +121 to +130

    fn apply_edits_inserts_text() {
        let original = "hello world";
        let path = PathBuf::from("test.txt");
        let edit = FileEdit::with_edits(
            path,
            vec![TextEdit::insert_at(Position::new(0, 6), "beautiful ")],
        );
        let result = apply_edits(original, &edit).expect("edit should succeed");
        assert_eq!(result, "hello beautiful world");
    }

❌ New issue: Code Duplication
The module contains 7 functions with similar structure: tests.apply_edits_allows_end_of_file_position,tests.apply_edits_deletes_text,tests.apply_edits_handles_crlf_line_endings,tests.apply_edits_handles_mixed_multiline_with_lf and 3 more functions

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Dec 10, 2025

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

crates/weaverd/src/safety_harness/transaction/tests.rs

Comment on lines +195 to +211

fn syntactic_failure_prevents_commit() {
    test_lock_failure(
        |path| {
            let failures = vec![VerificationFailure::new(path, "syntax error")];
            (
                ConfigurableSyntacticLock::failing(failures),
                ConfigurableSemanticLock::passing(),
            )
        },
        |outcome| {
            assert!(matches!(
                outcome,
                TransactionOutcome::SyntacticLockFailed { .. }
            ));
        },
    );
}

❌ New issue: Code Duplication
The module contains 2 functions with similar structure: semantic_failure_prevents_commit,syntactic_failure_prevents_commit

@coderabbitai

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Dec 10, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

codescene-delta-analysis[bot]

This comment was marked as outdated.

…th helper function

Refactor tests applying text edits to use a new helper `assert_edits_produce` that consolidates common setup and assertions. This reduces boilerplate, improves readability, and maintains test coverage for various edit scenarios including insertions, deletions, replacements, multiple edits, and different line endings.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
@leynos leynos changed the title Add Double-Lock safety harness for weaverd with atomic commits Implement Double-Lock safety harness for weaverd with atomic commits Dec 10, 2025
…ction execution

- Change path argument types from &PathBuf to &Path for more idiomatic usage in verification.rs and error.rs.
- Adapt related iterator return types accordingly.
- In safety harness behaviour tests, execute transactions on demand before outcome assertions to ensure tests do not fail due to missing execution.

These changes improve code clarity and test robustness without altering behavior.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Dec 11, 2025

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

crates/weaverd/src/tests/safety_harness_behaviour.rs

Comment on lines +195 to +206

fn then_commits(world: &RefCell<SafetyHarnessWorld>) {
    // Execute if not already done
    if world.borrow().outcome().is_none() {
        world.borrow_mut().execute_transaction();
    }
    let world = world.borrow();
    let outcome = world.outcome().expect("outcome should exist");
    assert!(
        outcome.as_ref().is_ok_and(|o| o.committed()),
        "transaction should commit: {outcome:?}"
    );
}

❌ New issue: Code Duplication
The module contains 5 functions with similar structure: then_backend_error,then_commits,then_no_changes,then_semantic_fails and 1 more functions

@coderabbitai

This comment was marked as resolved.

…tests

- Removed the ReplacementText newtype wrapper in edit.rs and all its usages to reduce unnecessary abstraction.
- Updated mod.rs to stop re-exporting ReplacementText.
- Consolidated syntactic and semantic lock failure tests into a single parameterized test using rstest for better coverage and maintainability.
- Refactored apply.rs to extract line content length logic, improving newline handling accuracy (supporting CR, LF, CRLF).
- Converted multiple individual tests on apply_edits to parameterized rstest cases to reduce duplication.
- Enhanced safety_harness_behaviour.rs tests by caching original file contents to enable more reliable "file unchanged" assertions.
- Added a helper assert_outcome function to consolidate transaction outcome assertions and ensure consistent transaction execution before checks.

These changes improve code clarity, test robustness, and maintainability without altering external behavior.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
@leynos leynos merged commit 50cd6b9 into main Dec 11, 2025
4 checks passed
@leynos leynos deleted the terragon/implement-double-lock-safety-x8eg4y branch December 11, 2025 00:38
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