Skip to content

Graph-slice: stable JSONL complete with deterministic same-file slice (7.2.1)#106

Open
leynos wants to merge 2 commits intomainfrom
define-stable-jsonl-schemas-h0tgo1
Open

Graph-slice: stable JSONL complete with deterministic same-file slice (7.2.1)#106
leynos wants to merge 2 commits intomainfrom
define-stable-jsonl-schemas-h0tgo1

Conversation

@leynos
Copy link
Copy Markdown
Owner

@leynos leynos commented Apr 12, 2026

Summary

  • Implement deterministic same-file slice for observe graph-slice, with a stable JSONL contract and spillover metadata.
  • Wire graph-slice through backend providers to enable semantic enrichment and symbol discovery via the router, enabling backend-backed checkout while preserving the stable schema.
  • Resolve file:// URIs to local paths, load local sources for symbol discovery, and map tree-sitter extraction errors to stable refusals (e.g., unsupported language, no symbol at position, etc.).
  • Guarantee deterministic card ordering to ensure identical payloads across runs.
  • End-to-end testing surface expanded: add weaver-e2e graph_slice_fixtures with 20 Python + 20 Rust cases, extend test harness to drive graph-slice with stable JSONL, and snapshot outputs.
  • Introduce graph_slice_snapshots.rs for semantic snapshots, truncation, and refusal scenarios.
  • Add new test_support scaffolding (GraphSliceRequest, GraphSliceFixtureCase) to coordinate URI, position, and detail levels.
  • Snapshots and fixtures: 40 semantic-detail graph-slice snapshots (Python and Rust) plus truncation and refusal snapshots under crates/weaver-e2e/tests/snapshots.
  • Documentation and roadmap: update 7-2-1 to Status: COMPLETE, reflecting shipped stable JSONL schemas and deterministic same-file behavior; Roadmap: mark 7.2.1 COMPLETE; Users guide: adjust refusals messaging as appropriate.

Changes

  • Core runtime
    • Implement deterministic same-file slice in observe graph-slice, returning a stable set of cards (entry card plus additional same-file symbols) within budget and providing spillover metadata when truncated.
    • Add support for spillover frontier and truncated indicators to reflect when more local symbols exist beyond budget.
    • Map tree-sitter extraction errors to stable refusals (e.g., unsupported language, no symbol at position, etc.).
    • Resolve file:// URIs to local paths and load source for local symbol discovery.
    • Introduce stable ordering of cards to ensure deterministic responses across runs.
  • API / Router
    • graph-slice handler invocation updated to accept backend providers: graph-slice now uses backends in weaverd/router.rs to enable semantic enrichment and symbol discovery.
  • End-to-end tests and fixtures
    • Add weaver-e2e graph_slice_fixtures with Python and Rust fixture batteries (20 Python + 20 Rust cases).
    • Extend test harness to drive graph-slice via weaver with stable JSONL schema and to snapshot outputs.
    • Introduce graph_slice_snapshots.rs to exercise semantic snapshots and truncation/refusal scenarios.
    • Add new test_support scaffolding (GraphSliceRequest, GraphSliceFixtureCase) to coordinate URI, position, and detail levels.
  • Snapshots and fixtures
    • 40 semantic-detail graph-slice snapshots (Python and Rust) plus truncation and refusal snapshots added under crates/weaver-e2e/tests/snapshots.
  • Documentation and roadmap
    • Docs: Update 7-2-1 to Status: COMPLETE, reflecting the shipped stable JSONL schemas and deterministic same-file behavior.
    • Roadmap: Mark 7.2.1 as COMPLETE and reflect the new stable contract; mention that edge traversal remains deferred to 7.2.x milestones.
    • Users guide: Adjust example in refusal scenario to reflect unsupported language and clearer error messaging; align with new refusal shape.

Why

This change locks the stable JSONL contract for observe graph-slice, delivering a deterministic, same-file slice now, while preserving the plan to implement full graph traversal and multi-file edge extraction in later milestones (7.2.2 and beyond). It provides a robust testing surface via 40 fixture-based end-to-end snapshots and ensures downstream tooling can rely on a stable, predictable schema.

Testing Plan

  • Run end-to-end graph-slice tests:
    • cargo test -p weaver-e2e (or the workspace test suite as configured)
    • Ensure all 40 graph-slice snapshots pass (including truncation and refusal cases).
  • Validate internal unit tests for the graph_slice handler in weaverd/weaver core:
    • cargo test -p weaverd (observe graph-slice tests renamed to pass the new backend-parameterized signature)
  • Confirm deterministic output by re-running snapshots to ensure identical payloads across runs.
  • Sanity checks:
    • Unsupported language refusals map to a structured refusal with reason "unsupported_language" and a descriptive message.
    • Spillover frontiers are populated when truncation occurs and edges remain unpopulated in this milestone.

Migration / Compatibility

  • Public surface: graph-slice API signature in the daemon is augmented to accept backend providers (internal change). Callers using the stable schema interface remain unaffected. The change is isolated to the graph-slice path and its test harness.
  • No consumer-facing API changes outside the debugging and snapshot ecosystem; stable schema remains the same shape with added spillover and budget fields.

Notes for Reviewers

  • Pay attention to the new deterministic card ordering and how spillover.frontier is populated when budget is exceeded.
  • Review the updated router integration for graph-slice to ensure backends are wired correctly for enrichment and symbol extraction.
  • Validate that the new 40 graph-slice snapshots cover the core Python and Rust symbol cases as intended.

References

  • 7.2.1 Stable JSONL request/response schemas for observe graph-slice (now COMPLETE)
  • docs/jacquard-card-first-symbol-graph-design.md: updated with stable graph-slice expectations
  • docs/roadmap.md: 7.2.1 marked COMPLETE

📎 Task: https://www.devboxer.com/task/ccab0145-07d2-4f5a-862d-c1678eba521f

…ice response

- Replace not_yet_implemented refusal with real graph-slice handler
- Return entry symbol card plus same-file sibling cards up to max_cards budget
- Populate spillover metadata for truncated slices
- Defer multi-file edge extraction and graph traversal to roadmap items 7.2.2-7.2.5
- Add end-to-end snapshot tests with 40 fixtures covering python and rust
- Add new shared fixtures module for graph-slice tests in weaver-e2e
- Update design docs, user guide, and roadmap to reflect stable contract

This change completes roadmap item 7.2.1 by providing a stable JSON request
and response schema with real runtime coverage while deferring full graph
traversal to later roadmap phases. It improves user experience by providing
successful graph-slice responses bounded by budget constraints and exposes
spillover data for client handling of truncation.

Closes #7.2.1

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

coderabbitai Bot commented Apr 12, 2026

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: fea81a2b-fe2c-4c19-a97b-895e2c3a8d26

📥 Commits

Reviewing files that changed from the base of the PR and between 13215a5 and d24963d.

⛔ Files ignored due to path filters (43)
  • crates/weaver-e2e/tests/snapshots/graph_slice_python_abstract_base_class.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_python_async_function.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_python_class_init_methods.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_python_classmethod_staticmethod.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_python_complex_types.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_python_control_flow.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_python_dataclass.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_python_decorator_stack.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_python_default_params.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_python_generator_function.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_python_google_docstring.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_python_import_block.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_python_lambda_assignment.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_python_module_doc_and_imports.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_python_module_variable.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_python_nested_function.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_python_numpy_docstring.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_python_property_decorator.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_python_simple_function.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_python_varargs_kwargs.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_refusal_unsupported_language.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_rust_async_function.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_rust_attribute_macro.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_rust_closure_assignment.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_rust_const_static.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_rust_control_flow.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_rust_derive_macro.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_rust_doc_comments.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_rust_enum_definition.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_rust_generics_function.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_rust_impl_methods.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_rust_lifetime_function.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_rust_module_doc_and_uses.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_rust_result_function.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_rust_simple_function.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_rust_struct_definition.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_rust_trait_definition.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_rust_trait_impl.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_rust_tuple_struct.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_rust_type_alias.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_rust_use_block.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_truncated_python_classmethod_staticmethod.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_truncated_rust_trait_impl.snap is excluded by !**/*.snap
📒 Files selected for processing (15)
  • crates/weaver-cards/src/graph_slice/parse.rs
  • crates/weaver-cards/src/graph_slice/request_tests.rs
  • crates/weaver-e2e/src/graph_slice_fixtures/mod.rs
  • crates/weaver-e2e/src/graph_slice_fixtures/python.rs
  • crates/weaver-e2e/src/graph_slice_fixtures/rust.rs
  • crates/weaver-e2e/src/lib.rs
  • crates/weaver-e2e/tests/graph_slice_snapshots.rs
  • crates/weaver-e2e/tests/test_support/mod.rs
  • crates/weaverd/src/dispatch/observe/graph_slice.rs
  • crates/weaverd/src/dispatch/observe/graph_slice_tests.rs
  • crates/weaverd/src/dispatch/router.rs
  • docs/execplans/7-2-1-define-stable-jsonl-request-and-response-schemas-for-observe-graph-slice.md
  • docs/jacquard-card-first-symbol-graph-design.md
  • docs/roadmap.md
  • docs/users-guide.md
 ____________________________________________
< Like a moth to a flame, I'm drawn to bugs. >
 --------------------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch define-stable-jsonl-schemas-h0tgo1

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

codescene-delta-analysis[bot]

This comment was marked as outdated.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Apr 12, 2026

Reviewer's Guide

Implements the runtime for observe graph-slice to return a deterministic same-file slice under the stable JSONL schema, wires it through the router with backend support, adds end-to-end fixtures and snapshots (Python/Rust), and updates docs/roadmap and user guide to reflect the shipped behavior and refusal shapes.

Sequence diagram for observe graph-slice deterministic same-file slice

sequenceDiagram
    actor User
    participant WeaverCLI as Weaver_CLI
    participant DaemonRouter as Daemon_Router
    participant GraphSliceHandler as Graph_Slice_Handler
    participant FusionBackends as Fusion_Backends
    participant CardExtractor as Card_Extractor
    participant Enrichment as Enrichment_Backend
    participant FileSystem as File_System

    User->>WeaverCLI: run observe graph-slice (uri, line, column, budget, detail)
    WeaverCLI->>DaemonRouter: JSONL CommandRequest
    DaemonRouter->>GraphSliceHandler: handle(request, writer, backends)

    GraphSliceHandler->>GraphSliceHandler: GraphSliceRequest.parse(arguments)
    GraphSliceHandler-->>DaemonRouter: DispatchError (invalid_arguments) if parse fails

    GraphSliceHandler->>GraphSliceHandler: Url.parse(request.uri)
    GraphSliceHandler->>GraphSliceHandler: resolve_file_path(file_uri)
    GraphSliceHandler->>FileSystem: read_to_string(path)
    FileSystem-->>GraphSliceHandler: source

    GraphSliceHandler->>FusionBackends: provider()
    FusionBackends-->>GraphSliceHandler: SemanticBackendProvider
    GraphSliceHandler->>FusionBackends: card_extractor()
    FusionBackends-->>GraphSliceHandler: Card_Extractor_clone

    GraphSliceHandler->>CardExtractor: extract(path, source, entry_line, entry_column, entry_detail)
    alt unsupported language
        CardExtractor-->>GraphSliceHandler: CardExtractionError_UnsupportedLanguage
        GraphSliceHandler->>GraphSliceHandler: map_extraction_error(error)
        GraphSliceHandler-->>DaemonRouter: GraphSliceResponse.Refusal(UnsupportedLanguage)
    else no symbol at position
        CardExtractor-->>GraphSliceHandler: CardExtractionError_NoSymbolAtPosition
        GraphSliceHandler->>GraphSliceHandler: map_extraction_error(error)
        GraphSliceHandler-->>DaemonRouter: GraphSliceResponse.Refusal(NoSymbolAtPosition)
    else success
        CardExtractor-->>GraphSliceHandler: entry_card
        GraphSliceHandler->>FusionBackends: enrich_card_if_requested(entry_card, entry_detail)
        GraphSliceHandler->>GraphSliceHandler: discover_same_file_cards(SameFileDiscovery)

        loop each_candidate_position
            GraphSliceHandler->>CardExtractor: extract(path, source, line, column, node_detail)
            alt no_symbol_or_unsupported_language
                CardExtractor-->>GraphSliceHandler: CardExtractionError_NoSymbolAtPosition_or_UnsupportedLanguage
                GraphSliceHandler-->>GraphSliceHandler: skip_position
            else success
                CardExtractor-->>GraphSliceHandler: sibling_card
                GraphSliceHandler->>FusionBackends: enrich_card_if_requested(sibling_card, node_detail)
                GraphSliceHandler-->>GraphSliceHandler: collect_in_BTreeMap_by_symbol_id
            end
        end

        GraphSliceHandler->>GraphSliceHandler: stable_card_order sort
        GraphSliceHandler->>GraphSliceHandler: apply_card_budget(entry_card, sibling_cards, max_cards)
        GraphSliceHandler-->>DaemonRouter: GraphSliceResponse.Success(cards, edges = [], spillover)
    end

    DaemonRouter->>WeaverCLI: JSONL response (status from exit_status)
    WeaverCLI-->>User: prints deterministic same-file slice
Loading

Updated class diagram for observe graph-slice handler and related types

classDiagram
    class GraphSliceHandler {
        +handle(request: CommandRequest, writer: ResponseWriter, backends: FusionBackends) Result~DispatchResult~
        +build_response(request: GraphSliceRequest, path: Path, source: str, backends: FusionBackends) Result~GraphSliceResponse~
        +discover_same_file_cards(discovery: SameFileDiscovery) Result~Vec_SymbolCard_~
        +candidate_positions(source: str) Vec_Tuple_u32_u32_
        +first_non_whitespace_column(line: str) Option_u32_
        +stable_card_order(left: SymbolCard, right: SymbolCard) Ordering
        +apply_card_budget(entry_card: SymbolCard, sibling_cards: Vec_SymbolCard_, max_cards: u32) Tuple_Vec_SymbolCard__SliceSpillover_
        +enrich_card_if_requested(card: SymbolCard, detail: DetailLevel, source: str, backends: FusionBackends) void
        +resolve_file_path(uri: Url) Result~PathBuf~
        +map_extraction_error(error: CardExtractionError) Result~GraphSliceResponse~
    }

    class GraphSliceRequest {
        +parse(args: Vec_str_) Result~GraphSliceRequest~
        +uri() str
        +line() u32
        +column() u32
        +depth() u32
        +direction() SliceDirection
        +edge_types() Vec_SliceEdgeType_
        +min_confidence() f32
        +budget() SliceBudget
        +entry_detail() DetailLevel
        +node_detail() DetailLevel
    }

    class GraphSliceResponse {
        +Success
        +Refusal
        +not_yet_implemented() GraphSliceResponse
    }

    class SliceEntry {
        +symbol_id: str
    }

    class SliceConstraints {
        +depth: u32
        +direction: SliceDirection
        +edge_types: Vec_SliceEdgeType_
        +min_confidence: f32
        +budget: SliceBudget
        +entry_detail: DetailLevel
        +node_detail: DetailLevel
    }

    class SliceBudget {
        +max_cards: u32
        +max_edges: u32
        +max_estimated_tokens: u32
    }

    class SliceSpillover {
        +truncated: bool
        +frontier: Vec_SpilloverCandidate_
        +empty() SliceSpillover
    }

    class SpilloverCandidate {
        +symbol_id: str
        +depth: u32
    }

    class SliceRefusal {
        +reason: SliceRefusalReason
        +message: str
    }

    class SliceRefusalReason {
        <<enumeration>>
        UnsupportedLanguage
        NoSymbolAtPosition
        PositionOutOfRange
        NotYetImplemented
    }

    class SymbolCard {
        +symbol: SymbolDescriptor
        +provenance: SymbolProvenance
    }

    class SymbolDescriptor {
        +symbol_id: str
        +symbol_ref: SymbolRef
    }

    class SymbolRef {
        +container: Option_str_
        +name: str
        +kind: SymbolKind
        +range: SourceRange
    }

    class SourceRange {
        +start: SourcePosition
        +end: SourcePosition
    }

    class SourcePosition {
        +line: u32
        +column: u32
    }

    class SymbolProvenance {
        +sources: Vec_str_
    }

    class DetailLevel {
        <<enumeration>>
        Minimal
        Structure
        Semantic
    }

    class FusionBackends {
        +provider() SemanticBackendProvider
    }

    class SemanticBackendProvider {
        +card_extractor() CardExtractor
        +lsp_client() LspClient
    }

    class CardExtractor {
        +extract(input: CardExtractionInput) Result~SymbolCard~
    }

    class CardExtractionInput {
        +path: Path
        +source: str
        +line: u32
        +column: u32
        +detail: DetailLevel
    }

    class CardExtractionError {
        <<enumeration>>
        UnsupportedLanguage
        InvalidPath
        NoSymbolAtPosition
        PositionOutOfRange
        Parse
    }

    class Enrichment {
        +try_lsp_enrichment(card: SymbolCard, source: str, backends: FusionBackends) EnrichmentOutcome
    }

    class EnrichmentOutcome {
        <<enumeration>>
        Enriched
        Skipped
        Failed
    }

    class SameFileDiscovery {
        +request: GraphSliceRequest
        +path: Path
        +source: str
        +entry_symbol_id: str
        +backends: FusionBackends
    }

    GraphSliceHandler --> GraphSliceRequest
    GraphSliceHandler --> GraphSliceResponse
    GraphSliceHandler --> SameFileDiscovery
    GraphSliceHandler --> SliceSpillover
    GraphSliceHandler --> SpilloverCandidate
    GraphSliceHandler --> FusionBackends
    GraphSliceHandler --> CardExtractor
    GraphSliceHandler --> Enrichment

    GraphSliceResponse --> SliceEntry
    GraphSliceResponse --> SliceConstraints
    GraphSliceResponse --> SymbolCard
    GraphSliceResponse --> SliceSpillover
    GraphSliceResponse --> SliceRefusal

    SliceRefusal --> SliceRefusalReason

    SliceConstraints --> SliceBudget
    SliceConstraints --> DetailLevel

    SymbolCard --> SymbolDescriptor
    SymbolCard --> SymbolProvenance

    SymbolDescriptor --> SymbolRef
    SymbolRef --> SourceRange
    SourceRange --> SourcePosition

    FusionBackends --> SemanticBackendProvider
    SemanticBackendProvider --> CardExtractor
    SemanticBackendProvider --> Enrichment

    CardExtractor --> CardExtractionInput
    CardExtractor --> CardExtractionError
    Enrichment --> EnrichmentOutcome
Loading

File-Level Changes

Change Details Files
Implement deterministic same-file graph-slice runtime using Tree-sitter extraction, backend enrichment, and a stable card ordering with spillover budget semantics.
  • Change observe::graph_slice::handle to parse GraphSliceRequest, resolve file:// URIs, read source, and delegate to a new build_response function using FusionBackends
  • Add build_response to extract the entry SymbolCard, discover additional same-file cards, apply the max_cards budget, and construct GraphSliceResponse::Success with empty edges and spillover metadata
  • Implement SameFileDiscovery and discover_same_file_cards to scan candidate positions in the same file, handle CardExtractionError variants, enrich cards when requested, and deduplicate by symbol_id
  • Add candidate_positions and first_non_whitespace_column helpers to produce deterministic cursor positions per line for same-file discovery
  • Define stable_card_order to impose deterministic ordering by container, name, kind, and source range; use apply_card_budget to include entry plus siblings and populate spillover.frontier when truncated
  • Implement enrich_card_if_requested to invoke LSP-based enrichment via backends when detail >= Semantic and adjust provenance sources accordingly
  • Add resolve_file_path to validate file:// URIs and map them to local paths with argument-level errors for unsupported schemes
  • Map Tree-sitter CardExtractionError variants to either structured GraphSliceResponse refusals (unsupported_language, no_symbol_at_position, position_out_of_range) or internal DispatchError cases (invalid path, parse failures)
crates/weaverd/src/dispatch/observe/graph_slice.rs
Adapt the daemon router and tests to the new backend-parameterized graph-slice handler and validate success, truncation, and refusal flows.
  • Update observe DomainRouter to pass FusionBackends into graph_slice::handle so graph-slice uses shared backend infrastructure
  • Refactor graph_slice unit tests to construct FusionBackends via a rstest fixture, materialize temp files, and drive the handler with real URIs and detail levels
  • Introduce helpers in tests for encoding DetailLevel values, dispatching requests, and parsing JSONL envelopes into payload JSON
  • Add new tests covering a successful same-file slice with echoed constraints, budget-based truncation with non-empty spillover.frontier, structured unsupported_language refusals, and invalid-argument error handling
crates/weaverd/src/dispatch/router.rs
crates/weaverd/src/dispatch/observe/graph_slice_tests.rs
Extend the weaver-e2e harness with graph-slice support, fixtures, and snapshots to lock the stable JSONL behavior.
  • Add GraphSliceRequest struct and run_graph_slice helper to test_support, including CLI argument construction and a more robust weaver binary resolver that falls back to target/debug/weaver when cargo_bin is absent
  • Introduce graph_slice_fixtures module (with python.rs and rust.rs) that re-exports existing CardFixtureCase batteries as GraphSliceFixtureCase arrays for Python and Rust
  • Add graph_slice_snapshots.rs integration test harness that spins up TestDaemon instances, materializes fixture workspaces or unsupported-language files, invokes run_graph_slice, and asserts named insta snapshots for 40 semantic-detail cases, truncation, and refusal scenarios
  • Create numerous snapshot files under crates/weaver-e2e/tests/snapshots for Python/Rust semantic slices, truncation cases, and an unsupported_language refusal, encoding the stable JSON transcripts
crates/weaver-e2e/tests/test_support/mod.rs
crates/weaver-e2e/src/graph_slice_fixtures/mod.rs
crates/weaver-e2e/src/graph_slice_fixtures/python.rs
crates/weaver-e2e/src/graph_slice_fixtures/rust.rs
crates/weaver-e2e/tests/graph_slice_snapshots.rs
crates/weaver-e2e/tests/snapshots/graph_slice_*.snap
Update execplan, roadmap, and design/user docs to reflect the complete stable JSONL schema, deterministic same-file behavior, and deferred edges.
  • Mark execplan 7-2-1 as COMPLETE, fill in additional stages (E/F) and outcomes describing the deterministic same-file implementation, e2e coverage, and successful gates, and record decisions about same-file-only cards and spillover semantics
  • Adjust users-guide graph-slice refusal example to use unsupported_language reason and message, and update the success example to show empty edges with deterministic same-file cards and spillover semantics tied to budget.max_cards, plus notes about deferred edge extraction
  • Update jacquard-card-first-symbol-graph-design to show the full constraints object (direction, edge_types, min_confidence, nested budget, entry_detail/node_detail) and describe the 7.2.1 behavior of returning same-file slices with empty edges until later milestones
  • Mark roadmap item 7.2.1 as complete and its acceptance criteria satisfied (schema fixtures, determinism, spillover, edge resolution_scope semantics in schema)
  • Update weaver-e2e lib docs to mention the new graph_slice_fixtures module
docs/execplans/7-2-1-define-stable-jsonl-request-and-response-schemas-for-observe-graph-slice.md
docs/users-guide.md
docs/jacquard-card-first-symbol-graph-design.md
docs/roadmap.md
crates/weaver-e2e/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

@leynos leynos marked this pull request as ready for review April 12, 2026 23:34
Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 5 issues, and left some high level feedback:

  • In handle/build_response, fs::read_to_string errors currently bubble up as generic DispatchErrors; consider mapping IO failures (missing file, permission issues) into a clearer invalid-arguments or user-facing refusal so clients can distinguish local file problems from internal daemon faults.
  • The discover_same_file_cards implementation calls the extractor once per non-blank line via candidate_positions; for large files this could be quite expensive, so it may be worth adding a heuristic cap or a more symbol-aware scan to avoid O(lines) extraction calls.
  • In detail_value, using unreachable! for additional DetailLevel variants will cause panics if the enum grows; replacing this with a non-panicking _ arm or a dedicated conversion method on DetailLevel would make the helper more robust to future changes.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `handle`/`build_response`, `fs::read_to_string` errors currently bubble up as generic `DispatchError`s; consider mapping IO failures (missing file, permission issues) into a clearer invalid-arguments or user-facing refusal so clients can distinguish local file problems from internal daemon faults.
- The `discover_same_file_cards` implementation calls the extractor once per non-blank line via `candidate_positions`; for large files this could be quite expensive, so it may be worth adding a heuristic cap or a more symbol-aware scan to avoid O(lines) extraction calls.
- In `detail_value`, using `unreachable!` for additional `DetailLevel` variants will cause panics if the enum grows; replacing this with a non-panicking `_` arm or a dedicated conversion method on `DetailLevel` would make the helper more robust to future changes.

## Individual Comments

### Comment 1
<location path="crates/weaverd/src/dispatch/observe/graph_slice.rs" line_range="237-246" />
<code_context>
+        ))
+}
+
+fn apply_card_budget(
+    entry_card: SymbolCard,
+    sibling_cards: Vec<SymbolCard>,
+    max_cards: u32,
+) -> (Vec<SymbolCard>, SliceSpillover) {
+    let remaining_capacity = max_cards.saturating_sub(1) as usize;
+    let included_siblings = sibling_cards
+        .iter()
+        .take(remaining_capacity)
+        .cloned()
+        .collect::<Vec<_>>();
+    let frontier = sibling_cards
+        .iter()
+        .skip(remaining_capacity)
+        .map(|card| SpilloverCandidate {
+            symbol_id: card.symbol.symbol_id.clone(),
+            depth: 1,
+        })
+        .collect::<Vec<_>>();
+
+    let mut cards = Vec::with_capacity(1 + included_siblings.len());
+    cards.push(entry_card);
+    cards.extend(included_siblings);
+
+    let spillover = if frontier.is_empty() {
+        SliceSpillover::empty()
+    } else {
+        SliceSpillover {
+            truncated: true,
+            frontier,
+        }
+    };
+
+    (cards, spillover)
+}
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Card budget logic ignores a `max_cards` value of 0 and still returns the entry card, which may violate the intended budget constraints.

In `apply_card_budget`, `remaining_capacity` uses `max_cards.saturating_sub(1)`, but the entry card is always added to `cards`. When `max_cards == 0`, the function still returns a single-card vector (the entry) and marks the slice as truncated. If the budget is intended as a strict upper bound, this violates the contract. Please handle `max_cards == 0` explicitly (e.g., return no cards and put the entry plus siblings in the frontier, or normalize `max_cards` to ≥ 1 at validation) so the behavior matches the budget semantics.
</issue_to_address>

### Comment 2
<location path="crates/weaverd/src/dispatch/observe/graph_slice.rs" line_range="139-149" />
<code_context>
+    discovery: SameFileDiscovery<'_>,
+) -> Result<Vec<SymbolCard>, DispatchError> {
+    let extractor = discovery.backends.provider().card_extractor().clone();
+    let mut cards = BTreeMap::new();
+    let mut visited_positions = BTreeSet::new();
+
+    for (line, column) in candidate_positions(discovery.source) {
</code_context>
<issue_to_address>
**suggestion:** The `visited_positions` set in `discover_same_file_cards` appears redundant given how `candidate_positions` is computed.

Here `visited_positions` deduplicates `(line, column)` pairs, but `candidate_positions` yields each line at most once and `first_non_whitespace_column` produces only one column per line. That makes the set unnecessary overhead unless you expect future changes to introduce duplicates. I’d either remove `visited_positions` or add a brief comment explaining under what conditions duplicates could appear to justify keeping it.

```suggestion
    let extractor = discovery.backends.provider().card_extractor().clone();
    let mut cards = BTreeMap::new();

    for (line, column) in candidate_positions(discovery.source) {
        if line == discovery.request.line() && column == discovery.request.column() {
            continue;
        }
```
</issue_to_address>

### Comment 3
<location path="crates/weaverd/src/dispatch/observe/graph_slice_tests.rs" line_range="177-186" />
<code_context>
+}
+
+#[rstest]
+fn unsupported_language_returns_structured_refusal(
+    backends: (FusionBackends<SemanticBackendProvider>, TempDir),
+) {
+    let (mut backends, temp_dir) = backends;
+    let path = write_source(&temp_dir, "notes.txt", "plain text\n");
+    let uri = Url::from_file_path(&path).expect("file uri").to_string();
+    let request = make_request(&["--uri", &uri, "--position", "1:1"]);
+
+    let (status, payload) = dispatch_payload(&request, &mut backends);
+
+    assert_eq!(status, 1);
+    assert_eq!(payload["status"], "refusal");
+    assert_eq!(payload["refusal"]["reason"], "unsupported_language");
 }

</code_context>
<issue_to_address>
**suggestion (testing):** Add tests for `NoSymbolAtPosition` and `PositionOutOfRange` refusals to cover all mapped extraction errors.

The handler maps multiple `CardExtractionError` variants to `NoSymbolAtPosition` and `PositionOutOfRange` via `map_extraction_error`, but tests currently only cover `UnsupportedLanguage`. Please add tests that:

* Trigger `NoSymbolAtPosition` (valid position with no symbol) and assert `status == 1`, `refusal.reason == "no_symbol_at_position"`, and the expected message.
* Trigger `PositionOutOfRange` (position beyond file bounds) and assert `refusal.reason == "position_out_of_range"`.

You can mirror the pattern in `unsupported_language_returns_structured_refusal`, using `write_source` and carefully chosen cursor positions, to lock down the error-to-refusal mapping and message shape.

Suggested implementation:

```rust
#[rstest]
fn unsupported_language_returns_structured_refusal(
    backends: (FusionBackends<SemanticBackendProvider>, TempDir),
) {
    let (mut backends, temp_dir) = backends;
    let path = write_source(&temp_dir, "notes.txt", "plain text\n");
    let uri = Url::from_file_path(&path).expect("file uri").to_string();
    let request = make_request(&["--uri", &uri, "--position", "1:1"]);

    let (status, payload) = dispatch_payload(&request, &mut backends);

    assert_eq!(status, 1);
    assert_eq!(payload["status"], "refusal");
    assert_eq!(payload["refusal"]["reason"], "unsupported_language");
}

#[rstest]
fn no_symbol_at_position_returns_structured_refusal(
    backends: (FusionBackends<SemanticBackendProvider>, TempDir),
) {
    let (mut backends, temp_dir) = backends;

    // Use a supported language extension so we exercise symbol extraction
    let path = write_source(&temp_dir, "main.rs", "fn main() {}\n");
    let uri = Url::from_file_path(&path).expect("file uri").to_string();

    // Choose a position that is within the file but on whitespace (no symbol)
    // Line/column are 1-based in the CLI interface.
    let request = make_request(&["--uri", &uri, "--position", "1:4"]);

    let (status, payload) = dispatch_payload(&request, &mut backends);

    assert_eq!(status, 1);
    assert_eq!(payload["status"], "refusal");
    assert_eq!(payload["refusal"]["reason"], "no_symbol_at_position");

    // The handler should surface a helpful message; assert the shape here.
    let message = payload["refusal"]["message"]
        .as_str()
        .expect("refusal message");
    assert!(
        message.to_lowercase().contains("no symbol") || message.to_lowercase().contains("no node"),
        "unexpected refusal message for no_symbol_at_position: {message}"
    );
}

#[rstest]
fn position_out_of_range_returns_structured_refusal(
    backends: (FusionBackends<SemanticBackendProvider>, TempDir),
) {
    let (mut backends, temp_dir) = backends;

    let path = write_source(&temp_dir, "main.rs", "fn main() {}\n");
    let uri = Url::from_file_path(&path).expect("file uri").to_string();

    // Position beyond file bounds: line 10, column 1 for a single‑line file
    let request = make_request(&["--uri", &uri, "--position", "10:1"]);

    let (status, payload) = dispatch_payload(&request, &mut backends);

    assert_eq!(status, 1);
    assert_eq!(payload["status"], "refusal");
    assert_eq!(payload["refusal"]["reason"], "position_out_of_range");

    let message = payload["refusal"]["message"]
        .as_str()
        .expect("refusal message");
    assert!(
        message.to_lowercase().contains("out of range")
            || message.to_lowercase().contains("outside file bounds"),
        "unexpected refusal message for position_out_of_range: {message}"
    );
}

```

These tests assume:
1. `main.rs` is treated as a supported language by the graph slice handler. If your handler uses a different extension (e.g. `.py`, `.ts`), update the filename accordingly.
2. Positions are 1-based `line:column` as in the existing tests. If they are 0-based, adjust the `--position` arguments.
3. The exact refusal messages may differ slightly. If `map_extraction_error` uses a specific phrasing, tighten the `assert!` conditions on `message` to match the exact expected string instead of `contains(...)` checks.
</issue_to_address>

### Comment 4
<location path="crates/weaverd/src/dispatch/observe/graph_slice_tests.rs" line_range="86-95" />
<code_context>
+    serde_json::to_string_pretty(transcript).expect("serialize transcript")
+}
+
+#[rstest]
+#[case::python_01(PYTHON_CASES[0])]
+#[case::python_02(PYTHON_CASES[1])]
</code_context>
<issue_to_address>
**suggestion (testing):** Extend invalid-argument tests to cover non-`file://` URIs and malformed URIs.

The new `handle` logic now validates URIs with `Url::parse` and `resolve_file_path`, returning `DispatchError::invalid_arguments` for unsupported schemes and malformed URIs, but the current `invalid_arguments_return_dispatch_error` tests only cover missing `--uri` and bad `--position`.

Please add rstest cases like:

* `&[
</issue_to_address>

### Comment 5
<location path="crates/weaverd/src/dispatch/observe/graph_slice.rs" line_range="128" />
<code_context>
+    })
+}
+
+struct SameFileDiscovery<'a> {
+    request: &'a GraphSliceRequest,
+    path: &'a Path,
</code_context>
<issue_to_address>
**issue (complexity):** Consider simplifying `discover_same_file_cards` by removing the `SameFileDiscovery` struct, extracting card-extraction/error-handling into a helper, and separating enrichment from provenance normalization to make the control flow flatter and easier to read.

- `SameFileDiscovery` currently adds indirection without much benefit and is immediately destructured in `discover_same_file_cards`. You can simplify the call site and function signature by passing explicit parameters, which makes both sides easier to read and removes the need to mentally jump to the struct definition:

  ```rust
  fn discover_same_file_cards(
      request: &GraphSliceRequest,
      path: &Path,
      source: &str,
      entry_symbol_id: &str,
      backends: &mut FusionBackends<SemanticBackendProvider>,
  ) -> Result<Vec<SymbolCard>, DispatchError> {
      let extractor = backends.provider().card_extractor().clone();
      // ... same body, replacing discovery.* with the params ...
  }

  // call site
  let sibling_cards = discover_same_file_cards(
      request,
      path,
      source,
      &entry_symbol_id,
      backends,
  )?;
  ```

- The main loop in `discover_same_file_cards` is doing a lot: candidate generation, extraction, error handling, entry-skip, enrichment, and deduplication. Extracting the extraction/error-handling piece into a helper that returns `Option<SymbolCard>` flattens the loop and gives you one place to manage the `CardExtractionError` logic for siblings:

  ```rust
  fn extract_card_at_position(
      extractor: &impl CardExtractorTrait, // adjust to actual trait
      path: &Path,
      source: &str,
      line: u32,
      column: u32,
      detail: DetailLevel,
  ) -> Result<Option<SymbolCard>, DispatchError> {
      match extractor.extract(CardExtractionInput {
          path,
          source,
          line,
          column,
          detail,
      }) {
          Ok(card) => Ok(Some(card)),
          Err(CardExtractionError::NoSymbolAtPosition { .. }) => Ok(None),
          Err(CardExtractionError::UnsupportedLanguage { .. }) => Ok(None),
          Err(CardExtractionError::PositionOutOfRange { .. }) => {
              let message = format!(
                  "computed position {line}:{column} should be valid during same-file slice discovery: {error}"
              );
              Err(DispatchError::internal(message))
          }
          Err(CardExtractionError::InvalidPath { path }) => Err(DispatchError::internal(
              format!(
                  "Tree-sitter extractor requires an absolute path: {}",
                  path.display()
              ),
          )),
          Err(CardExtractionError::Parse { language, message }) => Err(
              DispatchError::internal(format!(
                  "Tree-sitter parse failed for {language}: {message}"
              )),
          ),
      }
  }
  ```

  Then the loop becomes:

  ```rust
  for (line, column) in candidate_positions(source) {
      if (line, column) == (request.line(), request.column()) {
          continue;
      }
      if !visited_positions.insert((line, column)) {
          continue;
      }

      let Some(mut card) = extract_card_at_position(
          &extractor,
          path,
          source,
          line,
          column,
          request.node_detail(),
      )? else {
          continue;
      };

      if card.symbol.symbol_id == entry_symbol_id {
          continue;
      }

      enrich_card_if_requested(&mut card, request.node_detail(), source, backends);
      cards.entry(card.symbol.symbol_id.clone()).or_insert(card);
  }
  ```

  This keeps all current behavior, but centralizes error handling and removes a large `match` from the loop.

- `enrich_card_if_requested` is mixing the decision of “should we enrich?” with provenance normalization. Pulling the provenance mutation into a small helper makes the control flow easier to scan:

  ```rust
  fn normalize_lsp_provenance(provenance: &mut ProvenanceType) {
      provenance
          .sources
          .retain(|source_name| source_name != "tree_sitter_degraded_semantic");

      if !provenance
          .sources
          .iter()
          .any(|source_name| source_name == "lsp_hover")
      {
          provenance.sources.push(String::from("lsp_hover"));
      }
  }

  fn enrich_card_if_requested(
      card: &mut SymbolCard,
      detail: DetailLevel,
      source: &str,
      backends: &mut FusionBackends<SemanticBackendProvider>,
  ) {
      if detail < DetailLevel::Semantic {
          return;
      }

      if enrich::try_lsp_enrichment(card, source, backends) == EnrichmentOutcome::Enriched {
          normalize_lsp_provenance(&mut card.provenance);
      }
  }
  ```

  This keeps semantics intact while making enrichment behavior more declarative and easier to reason about.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread crates/weaverd/src/dispatch/observe/graph_slice.rs
Comment thread crates/weaverd/src/dispatch/observe/graph_slice.rs Outdated
Comment on lines +177 to +186
fn unsupported_language_returns_structured_refusal(
backends: (FusionBackends<SemanticBackendProvider>, TempDir),
) {
let (mut backends, temp_dir) = backends;
let path = write_source(&temp_dir, "notes.txt", "plain text\n");
let uri = Url::from_file_path(&path).expect("file uri").to_string();
let request = make_request(&["--uri", &uri, "--position", "1:1"]);

let (status, payload) = dispatch_payload(&request, &mut backends);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): Add tests for NoSymbolAtPosition and PositionOutOfRange refusals to cover all mapped extraction errors.

The handler maps multiple CardExtractionError variants to NoSymbolAtPosition and PositionOutOfRange via map_extraction_error, but tests currently only cover UnsupportedLanguage. Please add tests that:

  • Trigger NoSymbolAtPosition (valid position with no symbol) and assert status == 1, refusal.reason == "no_symbol_at_position", and the expected message.
  • Trigger PositionOutOfRange (position beyond file bounds) and assert refusal.reason == "position_out_of_range".

You can mirror the pattern in unsupported_language_returns_structured_refusal, using write_source and carefully chosen cursor positions, to lock down the error-to-refusal mapping and message shape.

Suggested implementation:

#[rstest]
fn unsupported_language_returns_structured_refusal(
    backends: (FusionBackends<SemanticBackendProvider>, TempDir),
) {
    let (mut backends, temp_dir) = backends;
    let path = write_source(&temp_dir, "notes.txt", "plain text\n");
    let uri = Url::from_file_path(&path).expect("file uri").to_string();
    let request = make_request(&["--uri", &uri, "--position", "1:1"]);

    let (status, payload) = dispatch_payload(&request, &mut backends);

    assert_eq!(status, 1);
    assert_eq!(payload["status"], "refusal");
    assert_eq!(payload["refusal"]["reason"], "unsupported_language");
}

#[rstest]
fn no_symbol_at_position_returns_structured_refusal(
    backends: (FusionBackends<SemanticBackendProvider>, TempDir),
) {
    let (mut backends, temp_dir) = backends;

    // Use a supported language extension so we exercise symbol extraction
    let path = write_source(&temp_dir, "main.rs", "fn main() {}\n");
    let uri = Url::from_file_path(&path).expect("file uri").to_string();

    // Choose a position that is within the file but on whitespace (no symbol)
    // Line/column are 1-based in the CLI interface.
    let request = make_request(&["--uri", &uri, "--position", "1:4"]);

    let (status, payload) = dispatch_payload(&request, &mut backends);

    assert_eq!(status, 1);
    assert_eq!(payload["status"], "refusal");
    assert_eq!(payload["refusal"]["reason"], "no_symbol_at_position");

    // The handler should surface a helpful message; assert the shape here.
    let message = payload["refusal"]["message"]
        .as_str()
        .expect("refusal message");
    assert!(
        message.to_lowercase().contains("no symbol") || message.to_lowercase().contains("no node"),
        "unexpected refusal message for no_symbol_at_position: {message}"
    );
}

#[rstest]
fn position_out_of_range_returns_structured_refusal(
    backends: (FusionBackends<SemanticBackendProvider>, TempDir),
) {
    let (mut backends, temp_dir) = backends;

    let path = write_source(&temp_dir, "main.rs", "fn main() {}\n");
    let uri = Url::from_file_path(&path).expect("file uri").to_string();

    // Position beyond file bounds: line 10, column 1 for a single‑line file
    let request = make_request(&["--uri", &uri, "--position", "10:1"]);

    let (status, payload) = dispatch_payload(&request, &mut backends);

    assert_eq!(status, 1);
    assert_eq!(payload["status"], "refusal");
    assert_eq!(payload["refusal"]["reason"], "position_out_of_range");

    let message = payload["refusal"]["message"]
        .as_str()
        .expect("refusal message");
    assert!(
        message.to_lowercase().contains("out of range")
            || message.to_lowercase().contains("outside file bounds"),
        "unexpected refusal message for position_out_of_range: {message}"
    );
}

These tests assume:

  1. main.rs is treated as a supported language by the graph slice handler. If your handler uses a different extension (e.g. .py, .ts), update the filename accordingly.
  2. Positions are 1-based line:column as in the existing tests. If they are 0-based, adjust the --position arguments.
  3. The exact refusal messages may differ slightly. If map_extraction_error uses a specific phrasing, tighten the assert! conditions on message to match the exact expected string instead of contains(...) checks.

Comment on lines +86 to +95
#[rstest]
fn valid_request_returns_success_and_echoed_constraints(
backends: (FusionBackends<SemanticBackendProvider>, TempDir),
) {
let (mut backends, temp_dir) = backends;
let path = write_source(
&temp_dir,
"slice.rs",
concat!(
"struct Counter(u32);\n\n",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): Extend invalid-argument tests to cover non-file:// URIs and malformed URIs.

The new handle logic now validates URIs with Url::parse and resolve_file_path, returning DispatchError::invalid_arguments for unsupported schemes and malformed URIs, but the current invalid_arguments_return_dispatch_error tests only cover missing --uri and bad --position.

Please add rstest cases like:

  • `&[

Comment thread crates/weaverd/src/dispatch/observe/graph_slice.rs Outdated
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2e431fb22e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +257 to +259
let mut cards = Vec::with_capacity(1 + included_siblings.len());
cards.push(entry_card);
cards.extend(included_siblings);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Enforce max_cards when building the response card list

When callers pass --max-cards 0 (currently allowed by parsing), apply_card_budget still unconditionally appends entry_card, so the response can return 1 card while echoing constraints.budget.max_cards = 0. This breaks the documented budget contract that cards must fit within the applied budget and can cause downstream consumers that trust the echoed budget to mis-handle responses.

Useful? React with 👍 / 👎.

Comment on lines +210 to +211
line.char_indices()
.find_map(|(index, ch)| (!ch.is_whitespace()).then_some((index as u32) + 1))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use character offsets for candidate position columns

first_non_whitespace_column derives the column from char_indices() byte offsets, but extraction APIs treat columns as character positions. On lines with multibyte leading whitespace/text, this computes a larger-than-actual column, which can select the wrong symbol or raise PositionOutOfRange during same-file discovery and abort an otherwise valid graph-slice request.

Useful? React with 👍 / 👎.

…discovery

- Enforce `max_cards >= 1` at parse time and add defensive zero-budget guard at runtime.
- Cap same-file discovery to 256 positions to avoid unbounded extraction cost.
- Correct column computation in candidate positions to use character offsets instead of UTF-8 byte offsets.
- Map local source file read failures to invalid argument dispatch errors for clearer client feedback.
- Add comprehensive tests for refusals, invalid arguments, and edge cases related to position parsing.
- Normalize LSP enrichment provenance to exclude degraded semantic source and include "lsp_hover" when enriched.

These changes harden the 7.2.1 observe graph-slice implementation based on review findings and improve robustness and user experience.

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
@leynos leynos changed the title Complete stable JSONL for observe graph-slice with deterministic slice Graph-slice: stable JSONL complete with deterministic same-file slice Apr 20, 2026
Copy link
Copy Markdown

@codescene-delta-analysis codescene-delta-analysis Bot left a comment

Choose a reason for hiding this comment

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

Gates Failed
Enforce advisory code health rules (1 file with Code Duplication, Large Assertion Blocks)

Gates Passed
5 Quality Gates Passed

See analysis details in CodeScene

Reason for failure
Enforce advisory code health rules Violations Code Health Impact
graph_slice_tests.rs 2 advisory rules 10.00 → 8.82 Suppress

Quality Gate Profile: Pay Down Tech Debt
Install CodeScene MCP: safeguard and uplift AI-generated code. Catch issues early with our IDE extension and CLI tool.

Comment on lines +214 to +226
fn position_out_of_range_returns_structured_refusal(
backends: (FusionBackends<SemanticBackendProvider>, TempDir),
) {
let (mut backends, temp_dir) = backends;
let path = write_source(&temp_dir, "main.rs", "fn main() {}\n");
let uri = Url::from_file_path(&path).expect("file uri").to_string();
let request = make_request(&["--uri", &uri, "--position", "10:1"]);

let (status, payload) = dispatch_payload(&request, &mut backends);

assert_eq!(status, 1);
assert_eq!(payload["status"], "refusal");
assert_eq!(payload["refusal"]["reason"], "position_out_of_range");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

❌ New issue: Code Duplication
The module contains 3 functions with similar structure: no_symbol_at_position_returns_structured_refusal,position_out_of_range_returns_structured_refusal,unsupported_language_returns_structured_refusal

Suppress

Comment on lines +214 to +226
fn position_out_of_range_returns_structured_refusal(
backends: (FusionBackends<SemanticBackendProvider>, TempDir),
) {
let (mut backends, temp_dir) = backends;
let path = write_source(&temp_dir, "main.rs", "fn main() {}\n");
let uri = Url::from_file_path(&path).expect("file uri").to_string();
let request = make_request(&["--uri", &uri, "--position", "10:1"]);

let (status, payload) = dispatch_payload(&request, &mut backends);

assert_eq!(status, 1);
assert_eq!(payload["status"], "refusal");
assert_eq!(payload["refusal"]["reason"], "position_out_of_range");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

❌ New issue: Large Assertion Blocks
The test suite contains 4 assertion blocks with at least 4 assertions, threshold = 4

Suppress

@coderabbitai coderabbitai Bot added the Roadmap label Apr 20, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 20, 2026

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{"name":"HttpError","status":500,"request":{"method":"PATCH","url":"https://api.github.com/repos/leynos/weaver/issues/comments/4232830534","headers":{"accept":"application/vnd.github.v3+json","user-agent":"octokit.js/0.0.0-development octokit-core.js/7.0.6 Node.js/24","authorization":"token [REDACTED]","content-type":"application/json; charset=utf-8"},"body":{"body":"<!-- This is an auto-generated comment: summarize by coderabbit.ai -->\n<!-- walkthrough_start -->\n\n<!-- This is an auto-generated comment: release notes by coderabbit.ai -->\n\n## Overview\n\nThis PR completes the implementation of deterministic, same-file graph-slice execution as defined in the execplan [7.2.1 Define stable JSONL request and response schemas for observe graph-slice](docs/execplans/7-2-1-define-stable-jsonl-request-and-response-schemas-for-observe-graph-slice.md), marking the execplan status as COMPLETE. The implementation delivers a stable JSONL contract for the `observe graph-slice` operation whilst deferring multi-file traversal and edge extraction to later milestones.\n\n## Core Implementation Changes\n\n### Graph-slice Handler (`crates/weaverd/src/dispatch/observe/graph_slice.rs`)\n\nReplaced the prior placeholder `not_yet_implemented` response with a deterministic `GraphSliceResponse::Success` for same-file slicing. The handler now:\n\n- Parses and validates URIs as `file://` paths, with fallback to `DispatchError::internal` for invalid paths\n- Reads local source files, mapping read failures to `DispatchError::InvalidArguments`\n- Extracts an entry card at the requested line and column position using character offsets (not UTF-8 byte offsets)\n- Enriches the entry card with semantic detail when requested, using injected `FusionBackends<SemanticBackendProvider>` for discovery\n- Discovers additional sibling cards from the same file up to a fixed 256-position bound\n- Deduplicates discovered cards by `symbol_id` and applies stable ordering based on symbol metadata and source ranges\n- Implements budgeting using `max_cards`, including excess symbols in `SliceSpillover` frontier metadata with truncation indicators\n- Returns structured `GraphSliceResponse::Refusal` for unsupported languages, no symbol at position, or out-of-range positions\n- Returns an empty `edges` array (reserved for later milestones)\n\n### Router Changes (`crates/weaverd/src/dispatch/router.rs`)\n\nUpdated `DomainRouter::route_observe` to pass `FusionBackends<SemanticBackendProvider>` to the graph-slice handler, enabling enrichment and discovery functionality.\n\n## Request Validation\n\n### `--max-cards` Enforcement\n\nAdded validation to reject `--max-cards` values of `0`, enforcing the requirement that `max_cards >= 1`. Invalid values now return `GraphSliceError::InvalidValue` with a clear error message. A defensive runtime guard further ensures consistency.\n\n### Enhanced Error Handling\n\n- Maps tree-sitter extraction errors to structured `GraphSliceResponse::Refusal` responses\n- Maps local file read failures to `DispatchError::InvalidArguments` with descriptive messages\n- Corrects column derivation for same-file discovery to use character offsets instead of UTF-8 byte offsets, with Unicode-whitespace regression testing\n\n## Test Infrastructure and Coverage\n\n### Fixtures Module (`crates/weaver-e2e/src/graph_slice_fixtures/`)\n\nIntroduced a dedicated fixtures module re-exporting 20 Python and 20 Rust fixture cases from the existing card-fixtures battery, establishing a centralised entry point for graph-slice snapshot coverage.\n\n### End-to-End Snapshot Tests (`crates/weaver-e2e/tests/graph_slice_snapshots.rs`)\n\nAdded comprehensive snapshot test harness covering:\n\n- `graph_slice_semantic_snapshots_cover_python_and_rust_fixture_battery`: Validates deterministic outputs across 40 fixture cases (20 Python + 20 Rust)\n- `graph_slice_truncation_snapshots`: Validates spillover frontier population and truncation indicators when `max_cards` is set to `1`\n- `graph_slice_refusal_snapshots`: Validates refusal semantics for unsupported languages (plain-text file)\n\n### Unit Test Enhancements (`crates/weaverd/src/dispatch/observe/graph_slice_tests.rs`)\n\nExpanded the test suite to:\n\n- Construct and inject real `FusionBackends<SemanticBackendProvider>` into dispatch calls\n- Validate successful same-file graph-slice responses including echoed constraints and card/symbol expectations\n- Validate truncation behaviour with `--max-cards`\n- Test refusals for unsupported languages, no symbol at position, and out-of-bounds positions with specific `refusal.reason` and `refusal.message` values\n- Test invalid arguments with parameterised expected error substrings\n- Test missing source file handling\n- Validate character-offset column computation with Unicode-whitespace regression testing\n\n### Test Support Utilities (`crates/weaver-e2e/tests/test_support/mod.rs`)\n\n- Added `GraphSliceRequest<'a>` struct capturing graph-slice parameters\n- Introduced `run_graph_slice` helper to construct and execute `weaver observe graph-slice` CLI commands\n- Updated binary resolution to fall back to `target/debug/weaver` when cargo-bin resolution fails\n\n## Documentation Updates\n\n### Execplan (`docs/execplans/7-2-1-define-stable-jsonl-request-and-response-schemas-for-observe-graph-slice.md`)\n\nMarked as COMPLETE with checkpoint updates reflecting contract hardening: `--max-cards` lower-bound enforcement (`>= 1`), local file read error mapping, character-offset column derivation, and same-file discovery scoping to 256 positions.\n\n### Design Document (`docs/jacquard-card-first-symbol-graph-design.md`)\n\nUpdated the `observe graph-slice` response schema to include richer constraint structure with traversal parameters, nested `budget` object, and explicit detail selectors. Added documentation note on deterministic same-file slicing behaviour respecting `budget.max_cards` and `spillover` population.\n\n### Roadmap (`docs/roadmap.md`)\n\nUpdated 7.2.1 checklist markers from unchecked to completed, reflecting stable schema delivery.\n\n### User Guide (`docs/users-guide.md`)\n\n- Updated failure semantics from `not_yet_implemented` to `unsupported_language` for unsupported file types\n- Removed sample `edges` array from success examples and replaced with `edges: []`\n- Clarified runtime behaviour: Weaver currently builds deterministic same-file slices within `budget.max_cards`, with runtime `edges` empty whilst the edge schema remains reserved for future milestones\n- Clarified `spillover.frontier` semantics as discovered but excluded same-file symbols\n- Removed \"not yet fully implemented\" claims in favour of statements on stable schema locking\n\n## Migration Notes\n\nThe daemon's `graph-slice` handler signature now accepts `FusionBackends<SemanticBackendProvider>` internally for enrichment and discovery. The stable JSONL schema for consumers remains unchanged. All changes are backwards-compatible at the API boundary.\n\n<!-- end of auto-generated comment: release notes by coderabbit.ai -->\n## Walkthrough\n\nThis pull request replaces the placeholder \"not_yet_implemented\" graph-slice handler with a functional implementation that extracts entry cards and same-file sibling cards from source files, respects budget constraints via `max_cards`, and populates spillover metadata for excluded symbols. Comprehensive end-to-end snapshot tests and updated documentation complete the roadmap item 7.2.1.\n\n## Changes\n\n|Cohort / File(s)|Summary|\n|---|---|\n|**Parse validation** <br> `crates/weaver-cards/src/graph_slice/parse.rs`, `crates/weaver-cards/src/graph_slice/request_tests.rs`|Added validation to reject `--max-cards 0`, enforcing minimum bound of 1 with error message `\"max_cards must be >= 1\"`.|\n|**Test fixtures and infrastructure** <br> `crates/weaver-e2e/src/graph_slice_fixtures/...`, `crates/weaver-e2e/src/lib.rs`|Introduced new fixture modules and type aliases (`GraphSliceFixtureCase`, `PYTHON_CASES`, `RUST_CASES`) to organise snapshot test data for Python and Rust sources.|\n|**Snapshot test suite** <br> `crates/weaver-e2e/tests/graph_slice_snapshots.rs`, `crates/weaver-e2e/tests/test_support/mod.rs`|Added comprehensive end-to-end snapshot tests (`semantic_snapshots`, `truncation_snapshots`, `refusal_snapshots`) with helpers and request/response handling infrastructure.|\n|**Graph-slice handler implementation** <br> `crates/weaverd/src/dispatch/observe/graph_slice.rs`|Replaced placeholder with deterministic same-file slicing: reads source files, extracts entry and sibling cards, applies budget constraints, deduplicates and sorts by symbol metadata, and returns structured success or refusal responses.|\n|**Handler test coverage** <br> `crates/weaverd/src/dispatch/observe/graph_slice_tests.rs`|Rewrote test suite with real `FusionBackends` injection, new helpers, and comprehensive assertions covering success, truncation, refusals (unsupported language, no symbol, out of bounds), and invalid-argument cases.|\n|**Router integration** <br> `crates/weaverd/src/dispatch/router.rs`|Updated `\"graph-slice\"` routing to pass `FusionBackends<SemanticBackendProvider>` to handler invocation.|\n|**Documentation updates** <br> `docs/execplans/7-2-1-...`, `docs/jacquard-card-first-symbol-graph-design.md`, `docs/roadmap.md`, `docs/users-guide.md`|Marked roadmap item 7.2.1 complete, updated response schema examples with nested `budget` and `constraints` structures, replaced \"not_yet_implemented\" with deterministic same-file behaviour documentation, and added contract/validation details.|\n\n## Sequence Diagram(s)\n\n```mermaid\nsequenceDiagram\n    participant CLI as CLI Client\n    participant Daemon as Weaver Daemon\n    participant FS as File System\n    participant Extractor as TreeSitter<br/>Extractor\n    participant Response as Response<br/>Writer\n\n    CLI->>Daemon: observe graph-slice<br/>(uri, line, column,<br/>max_cards)\n    Daemon->>Daemon: Parse & validate<br/>request parameters\n    Daemon->>FS: Read source file<br/>from file:// URI\n    FS-->>Daemon: File contents\n    Daemon->>Extractor: Extract card at<br/>line/column position\n    Extractor-->>Daemon: Entry card /<br/>Refusal\n    alt Refusal (unsupported language,<br/>no symbol, out of bounds)\n        Daemon->>Response: GraphSliceResponse::<br/>Refusal\n    else Success\n        Daemon->>Extractor: Discover sibling cards<br/>at same-file positions\n        Extractor-->>Daemon: Sibling cards<br/>(up to limit)\n        Daemon->>Daemon: Deduplicate by symbol_id,<br/>sort by metadata,<br/>apply budget (max_cards)\n        Daemon->>Response: GraphSliceResponse::<br/>Success with cards<br/>& spillover\n    end\n    Response-->>CLI: JSONL response<br/>(transcript)\n```\n\n## Possibly related PRs\n\n- leynos/weaver#96: Introduces the validation that rejects `--max-cards 0` in the same parsing module, directly related to this feature's constraints enforcement.\n\n## Suggested labels\n\n`Roadmap`, `feature`\n\n## Poem\n\n> 🎯 Graph-slice awakens from its slumber,  \n> Real cards extracted, no longer lumber,  \n> Siblings sorted, budgets respected,  \n> Spillover marked where symbols were rejected,  \n> Same-file snapshots now capture the truth.\n\n<!-- walkthrough_end -->\n\n\n<!-- pre_merge_checks_walkthrough_start -->\n\n<details>\n<summary>🚥 Pre-merge checks | ✅ 4 | ❌ 3</summary>\n\n### ❌ Failed checks (3 warnings)\n\n|         Check name        | Status     | Explanation                                                                                                                                                                                | Resolution                                                                                                                                                                               |\n| :-----------------------: | :--------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n|          Testing          | ⚠️ Warning | PR lacks unit tests for critical internal logic: stable_card_order, deduplication, and apply_card_budget functions that affect deterministic behaviour.                                    | Add unit tests for stable_card_order, deduplication-by-symbol_id, and apply_card_budget. Add Ord derive to SymbolKind and refactor stable_card_order to eliminate format!() allocations. |\n| User-Facing Documentation | ⚠️ Warning | Documentation lacks explicit validation constraint that `--max-cards` must be >= 1, leaving user-facing behaviour undocumented.                                                            | Add explicit documentation of the `--max-cards` >= 1 validation constraint to the users-guide parameter description.                                                                     |\n|  Developer Documentation  | ⚠️ Warning | PR introduces architectural changes to graph_slice handler patterns, test infrastructure APIs, and fixture catalogue systems but lacks corresponding documentation in developers-guide.md. | Extend docs/developers-guide.md to document handler pattern architecture, test infrastructure patterns, and fixture catalogue system organisation with examples.                         |\n\n<details>\n<summary>✅ Passed checks (4 passed)</summary>\n\n|         Check name         | Status   | Explanation                                                                                                                                                                                                                                              |\n| :------------------------: | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n|         Title check        | ✅ Passed | The title accurately summarises the main change: completing a stable JSONL contract for observe graph-slice with deterministic same-file slicing, which aligns with the substantial implementation across runtime, router, e2e tests, and documentation. |\n|      Description check     | ✅ Passed | The description comprehensively relates to the changeset, detailing the deterministic same-file slice implementation, backend wiring, fixture additions, e2e testing expansion, and documentation updates that match the diff.                           |\n|     Docstring Coverage     | ✅ Passed | Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.                                                                                                                                                                     |\n| Module-Level Documentation | ✅ Passed | All seven modules introduced or substantially modified in this pull request carry comprehensive module-level docstrings using the //! convention with clear explanations of purpose and utility.                                                         |\n\n</details>\n\n<sub>✏️ Tip: You can configure your own custom pre-merge checks in the settings.</sub>\n\n</details>\n\n<!-- pre_merge_checks_walkthrough_end -->\n\n<!-- finishing_touch_checkbox_start -->\n\n<details>\n<summary>✨ Finishing Touches</summary>\n\n<details>\n<summary>📝 Generate docstrings</summary>\n\n- [ ] <!-- {\"checkboxId\": \"7962f53c-55bc-4827-bfbf-6a18da830691\"} --> Create stacked PR\n- [ ] <!-- {\"checkboxId\": \"3e1879ae-f29b-4d0d-8e06-d12b7ba33d98\"} --> Commit on current branch\n\n</details>\n<details>\n<summary>🧪 Generate unit tests (beta)</summary>\n\n- [ ] <!-- {\"checkboxId\": \"f47ac10b-58cc-4372-a567-0e02b2c3d479\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Create PR with unit tests\n- [ ] <!-- {\"checkboxId\": \"6ba7b810-9dad-11d1-80b4-00c04fd430c8\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Commit unit tests in branch `define-stable-jsonl-schemas-h0tgo1`\n\n</details>\n\n</details>\n\n<!-- finishing_touch_checkbox_end -->\n\n<!-- tips_start -->\n\n---\n\n\n\n<sub>Comment `@coderabbitai help` to get the list of available commands and usage tips.</sub>\n\n<!-- tips_end -->\n\n<!-- internal state start -->\n\n\n<!-- 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 -->\n\n<!-- internal state end -->"},"request":{"retryCount":3,"signal":{},"retries":3,"retryAfter":16}}}

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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/weaverd/src/dispatch/observe/graph_slice.rs`:
- Around line 220-241: The comparator stable_card_order is allocating a String
each comparison via format!("{:?}", left_ref.kind) / right_ref.kind); change the
tuple key to compare the SymbolKind values directly instead of formatting them,
and update the tuple elements to use left_ref.kind and right_ref.kind (or
otherwise compare SymbolKind) so no heap allocation occurs in stable_card_order;
additionally add Ord (and PartialOrd if needed) to the derives for the
SymbolKind enum in node.rs (the SymbolKind definition currently derives Debug,
Clone, Copy, PartialEq, Eq) so SymbolKind can be compared directly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: fea81a2b-fe2c-4c19-a97b-895e2c3a8d26

📥 Commits

Reviewing files that changed from the base of the PR and between 13215a5 and d24963d.

⛔ Files ignored due to path filters (43)
  • crates/weaver-e2e/tests/snapshots/graph_slice_python_abstract_base_class.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_python_async_function.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_python_class_init_methods.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_python_classmethod_staticmethod.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_python_complex_types.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_python_control_flow.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_python_dataclass.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_python_decorator_stack.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_python_default_params.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_python_generator_function.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_python_google_docstring.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_python_import_block.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_python_lambda_assignment.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_python_module_doc_and_imports.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_python_module_variable.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_python_nested_function.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_python_numpy_docstring.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_python_property_decorator.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_python_simple_function.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_python_varargs_kwargs.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_refusal_unsupported_language.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_rust_async_function.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_rust_attribute_macro.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_rust_closure_assignment.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_rust_const_static.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_rust_control_flow.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_rust_derive_macro.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_rust_doc_comments.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_rust_enum_definition.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_rust_generics_function.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_rust_impl_methods.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_rust_lifetime_function.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_rust_module_doc_and_uses.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_rust_result_function.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_rust_simple_function.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_rust_struct_definition.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_rust_trait_definition.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_rust_trait_impl.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_rust_tuple_struct.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_rust_type_alias.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_rust_use_block.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_truncated_python_classmethod_staticmethod.snap is excluded by !**/*.snap
  • crates/weaver-e2e/tests/snapshots/graph_slice_truncated_rust_trait_impl.snap is excluded by !**/*.snap
📒 Files selected for processing (15)
  • crates/weaver-cards/src/graph_slice/parse.rs
  • crates/weaver-cards/src/graph_slice/request_tests.rs
  • crates/weaver-e2e/src/graph_slice_fixtures/mod.rs
  • crates/weaver-e2e/src/graph_slice_fixtures/python.rs
  • crates/weaver-e2e/src/graph_slice_fixtures/rust.rs
  • crates/weaver-e2e/src/lib.rs
  • crates/weaver-e2e/tests/graph_slice_snapshots.rs
  • crates/weaver-e2e/tests/test_support/mod.rs
  • crates/weaverd/src/dispatch/observe/graph_slice.rs
  • crates/weaverd/src/dispatch/observe/graph_slice_tests.rs
  • crates/weaverd/src/dispatch/router.rs
  • docs/execplans/7-2-1-define-stable-jsonl-request-and-response-schemas-for-observe-graph-slice.md
  • docs/jacquard-card-first-symbol-graph-design.md
  • docs/roadmap.md
  • docs/users-guide.md

Comment on lines +220 to +241
fn stable_card_order(left: &SymbolCard, right: &SymbolCard) -> std::cmp::Ordering {
let left_ref = &left.symbol.symbol_ref;
let right_ref = &right.symbol.symbol_ref;
(
left_ref.container.as_deref().unwrap_or_default(),
left_ref.name.as_str(),
format!("{:?}", left_ref.kind),
left_ref.range.start.line,
left_ref.range.start.column,
left_ref.range.end.line,
left_ref.range.end.column,
)
.cmp(&(
right_ref.container.as_deref().unwrap_or_default(),
right_ref.name.as_str(),
format!("{:?}", right_ref.kind),
right_ref.range.start.line,
right_ref.range.start.column,
right_ref.range.end.line,
right_ref.range.end.column,
))
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if SymbolKind implements Ord
ast-grep --pattern $'#[derive($$$Ord$$$)]
$$$
enum SymbolKind {
  $$$
}'

Repository: leynos/weaver

Length of output: 39


🏁 Script executed:

#!/bin/bash
# Find SymbolKind definition
rg "enum SymbolKind" -A 10 --type rs

Repository: leynos/weaver

Length of output: 83


🏁 Script executed:

#!/bin/bash
# Find SymbolKind definition
rg "enum SymbolKind" -A 15

Repository: leynos/weaver

Length of output: 950


🏁 Script executed:

#!/bin/bash
# Get the full SymbolKind definition including derives
rg "#\[derive" -B 5 -A 15 crates/weaver-graph/src/node.rs | grep -A 15 "SymbolKind"

Repository: leynos/weaver

Length of output: 1138


🏁 Script executed:

#!/bin/bash
# Check for manual Ord implementation for SymbolKind
rg "impl.*Ord.*SymbolKind|impl.*SymbolKind.*Ord" -A 5

Repository: leynos/weaver

Length of output: 39


🏁 Script executed:

#!/bin/bash
# Verify SymbolKind implements Copy (confirming Ord can be safely derived)
rg "Copy" crates/weaver-graph/src/node.rs | grep -i symbolkind

Repository: leynos/weaver

Length of output: 39


🏁 Script executed:

#!/bin/bash
# Check the actual code location mentioned in the review
cat -n crates/weaverd/src/dispatch/observe/graph_slice.rs | sed -n '215,245p'

Repository: leynos/weaver

Length of output: 1257


🏁 Script executed:

#!/bin/bash
# Get the exact line with the derive for SymbolKind to confirm Copy
sed -n '1,50p' crates/weaver-graph/src/node.rs | rg "#\[derive" -A 2 | head -20

Repository: leynos/weaver

Length of output: 237


🏁 Script executed:

#!/bin/bash
# Get exact line number and derive for SymbolKind
rg -n "enum SymbolKind" -B 1 crates/weaver-graph/src/node.rs

Repository: leynos/weaver

Length of output: 129


Remove unnecessary heap allocations in sort comparator.

format!("{:?}", kind) allocates a new String on each comparison call. Replace with direct SymbolKind comparison:

♻️ Suggested refactor
 fn stable_card_order(left: &SymbolCard, right: &SymbolCard) -> std::cmp::Ordering {
     let left_ref = &left.symbol.symbol_ref;
     let right_ref = &right.symbol.symbol_ref;
     (
         left_ref.container.as_deref().unwrap_or_default(),
         left_ref.name.as_str(),
-        format!("{:?}", left_ref.kind),
+        left_ref.kind,
         left_ref.range.start.line,
         left_ref.range.start.column,
         left_ref.range.end.line,
         left_ref.range.end.column,
     )
         .cmp(&(
             right_ref.container.as_deref().unwrap_or_default(),
             right_ref.name.as_str(),
-            format!("{:?}", right_ref.kind),
+            right_ref.kind,
             right_ref.range.start.line,
             right_ref.range.start.column,
             right_ref.range.end.line,
             right_ref.range.end.column,
         ))
 }

Add Ord to the derives for SymbolKind in crates/weaver-graph/src/node.rs (currently only derives Debug, Clone, Copy, PartialEq, Eq).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/weaverd/src/dispatch/observe/graph_slice.rs` around lines 220 - 241,
The comparator stable_card_order is allocating a String each comparison via
format!("{:?}", left_ref.kind) / right_ref.kind); change the tuple key to
compare the SymbolKind values directly instead of formatting them, and update
the tuple elements to use left_ref.kind and right_ref.kind (or otherwise compare
SymbolKind) so no heap allocation occurs in stable_card_order; additionally add
Ord (and PartialOrd if needed) to the derives for the SymbolKind enum in node.rs
(the SymbolKind definition currently derives Debug, Clone, Copy, PartialEq, Eq)
so SymbolKind can be compared directly.

@leynos leynos changed the title Graph-slice: stable JSONL complete with deterministic same-file slice Graph-slice: stable JSONL complete with deterministic same-file slice (7.2.1) Apr 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant