Skip to content

Implement normalization into canonical Formula with semantic checks (4.1.5) - #104

Merged
leynos merged 56 commits into
mainfrom
implement-normalization-canonical-formula-6op9mx
May 4, 2026
Merged

Implement normalization into canonical Formula with semantic checks (4.1.5)#104
leynos merged 56 commits into
mainfrom
implement-normalization-canonical-formula-6op9mx

Conversation

@leynos

@leynos leynos commented Apr 11, 2026

Copy link
Copy Markdown
Owner

Summary

  • Introduces a canonical Formula model in sempai-core and a normalization pipeline to unify legacy pattern* syntax and v2 match syntax into a single representation in sempai_core.
  • Adds semantic validation for invalid constructs (e.g. InvalidNotInOr, MissingPositiveTermInAnd).
  • Replaces the NOT_IMPLEMENTED placeholder in Engine::compile_yaml with real normalization, validation, and QueryPlan construction.
  • Aligns tooling, tests, and docs toward a live normalization path across core, YAML parsing, and engine layers.

Changes

Core modelling

  • Add canonical Formula types in sempai-core:
    • Formula enum with variants: Atom, Not, Inside, Anywhere, And, Or.
    • Atom enum: Pattern, Regex, TreeSitterQuery.
    • Lightweight leaf wrappers: PatternAtom, RegexAtom, TreeSitterQueryAtom.
    • Decorated<T> wrapper carrying where_clauses, as_name, fix, and span.
    • WhereClause as an opaque container (initially storing a raw JSON value).
  • Expose and re-export new types from public API (e.g. pub mod formula; and re-export from sempai-core lib).

Normalization layer

  • Introduce normalization entry points to convert both legacy formulas and v2 match formulas into Decorated<Formula>:
    • Legacy normalization maps pattern, pattern-regex, patterns, pattern-either, pattern-inside, etc. to canonical Formula structures.
    • v2 normalization maps match forms (all, any, not, inside, anywhere, etc.) to canonical shapes.
    • Attach WhereClauses to the enclosing Decorated<Formula> for constraints carried over from legacy forms.
    • Propagate Decorated metadata (where, as, fix) during normalization.

Semantic validation

  • Add semantic checks for canonical formulas:
    • InvalidNotInOr: reject negated branches inside disjunctions.
    • MissingPositiveTermInAnd: require at least one positive term in an And.
  • Implement recursive validation across nested formula trees and emit deterministic diagnostics (codes like E_SEMPAI_INVALID_NOT_IN_OR, E_SEMPAI_MISSING_POSITIVE_TERM_IN_AND).

Engine integration

  • Wire normalization into Engine::compile_yaml:
    1. Parse rules as before.
    2. For each rule, normalize its principal into Decorated<Formula>.
    3. Run semantic validation on the normalized formula.
    4. Build and return a QueryPlan containing the normalized formula and rule metadata.
  • Update QueryPlan to store the Decorated<Formula> instead of the prior placeholder.

Testing

  • Add unit tests in:
    • sempai-core for construction and equality of Formula variants.
    • sempai for normalization and semantic validation scenarios.
  • Extend/align BDD tests to cover happy paths, negative paths (invalid-not-in-or, missing-positive-term-in-and), and parity between legacy and v2 fixtures.

Documentation

  • Documentation updates to reflect normalization as the live path:
    • docs/sempai-query-language-design.md (note canonical Formula model and WhereClause handling).
    • docs/users-guide.md (update to reflect compile_yaml returning compiled plans for valid rules).
    • docs/roadmap.md (mark 4.1.5 as done).

Validation and acceptance criteria

  • Engine::compile_yaml returns QueryPlans with normalized Decorated<Formula> for valid rules (no more NOT_IMPLEMENTED).
  • Legacy and v2 fixtures normalize to structurally equivalent Formula values.
  • Semantic diagnostics are emitted for invalid forms (e.g. invalid not-in-or, missing positive term).
  • Tests pass for new normalization and semantic checks; existing tests adapt to new behavior where necessary.
  • Documentation and roadmap reflect the completed milestone.

How to test locally

  • Build docs and run checks:
    • cargo doc -p sempai_core
    • make lint
    • make check-fmt
    • make markdownlint
  • Run tests:
    • cargo test -p sempai_core
    • cargo test -p sempai
    • cargo test -p sempai_yaml
  • Exercise engine path:
    • cargo test -p sempai

Notes on compatibility and scope

  • Public API changes are limited to internal canonical-form representations; the existing sempai_yaml::parse_rule_file signature remains unchanged.
  • The new modules are designed to be incremental (Stage B–D) with Stage E–H focused on wiring, testing, and documentation.
  • New functionality should be covered by targeted tests and BDD scenarios before release.

📎 Task: https://www.devboxer.com/task/795de59f-09d8-463e-9ef9-350b92fa57be

@coderabbitai

coderabbitai Bot commented Apr 11, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6bbf0c24-e0b9-483a-ab83-04e64d73edf5

📥 Commits

Reviewing files that changed from the base of the PR and between a3faf60 and f0367fd.

📒 Files selected for processing (2)
  • crates/sempai/src/tests/normalization_constraint_tests.rs
  • crates/sempai/src/tests/normalization_metadata_tests.rs

Core Formula modelling

Introduces a canonical formula AST in crates/sempai-core/src/formula.rs (exported via pub mod formula). Formula unifies legacy pattern* and v2 match syntaxes with variants: Atom (Pattern / Regex / TreeSitterQuery), unary operators (Not, Inside, Anywhere) and boolean composition (And, Or). Any node can be wrapped in Decorated carrying metadata (where_clauses, as_name, fix, span). Constraints are modelled as a Constraint enum (MetavariableRegex, MetavariablePattern, Other(String)). The new execplan docs/execplans/4-1-5-normalization-into-canonical-formula-model.md documents the design and acceptance criteria and is referenced by code and docs.

Normalization pipeline

Adds normalize_search_principal in crates/sempai/src/normalize.rs to lower parsed SearchQueryPrincipal variants (legacy pattern* forms, v2 match forms, and the r2c-internal-project-depends-on principal) into Decorated. Behaviour:

  • Recursively lowers legacy/v2 shapes (atoms, patterns-either/all/any, unary wrappers, anywhere) to the canonical Formula AST.
  • Preserves root metadata (where → parsed WhereClauses, as, fix, span) and clears metadata on child Decorated nodes.
  • Lowers r2c-internal-project-depends-on to a degenerate TreeSitterQuery atom "(NONEXISTENT_NODE) @_dependency_check".
  • Constraint parsing is implemented in crates/sempai/src/normalize_constraints.rs (parse_constraint): known shapes become Constraint variants; malformed constraint shapes return schema-invalid DiagnosticReport. As a result normalization is fallible and returns Result<Decorated, DiagnosticReport> for schema errors.

Semantic validation

Adds validate_formula in crates/sempai/src/semantic_check.rs with:

  • Two deterministic semantic checks: E_SEMPAI_INVALID_NOT_IN_OR (negation within a disjunction) and E_SEMPAI_MISSING_POSITIVE_TERM_IN_AND (conjunctions must include at least one positive/atomic term).
  • MAX_FORMULA_DEPTH = 1000 to guard recursion.
  • Deterministic primary_span selection with preferred ordering (node span → first child span → fallback) for reported diagnostics.
  • Validation implemented by a depth-tracked recursive analysis; reviewer feedback prompted extraction of per-variant helpers and improved span-selection.

Engine integration

Implements Engine::compile_yaml in crates/sempai/src/engine.rs:

  • Parses YAML, validates supported modes, normalizes search principals, runs semantic validation, and returns Ok(Vec) for valid search rules (replacing the prior NOT_IMPLEMENTED path).
  • QueryPlan now stores formula: Arc<Decorated> (shared across languages for a rule) and exposes a public formula() accessor.
  • YAML parse/validation and normalization/semantic failures surface deterministic DiagnosticCodes (including schema-invalid diagnostics from normalization).

Tracing helpers (crates/sempai/src/normalize_trace.rs) add discriminants and pattern/branch introspection to improve observability; compile_yaml records rule counts and principal discriminants.

Tests

Large test additions and refactors to exercise normalization, validation and engine wiring:

  • Unit tests: normalization_tests.rs, normalization_metadata_tests.rs, normalization_constraint_tests.rs — validate legacy/v2 lowering, metadata propagation/clearing, constraint parsing, span propagation and placeholder lowering.
  • Semantic tests: semantic_validation_tests.rs — failing and passing shapes, and diagnostic primary_span selection.
  • Integration tests: engine_integration_tests.rs and updated engine_tests.rs/behaviour.rs — assert Engine::compile_yaml returns QueryPlan(s) with preserved Decorated metadata and Arc reuse across languages.
  • BDD: crates/sempai/tests/features/sempai_engine.feature updated to assert successful compilation and inspect returned QueryPlan formulas.
  • Snapshot tests: diagnostic_snapshot_tests.rs (insta) redact spans for stable snapshots.
  • Property tests: property_tests.rs (proptest) generate recursive Decorated trees to exercise semantic invariants.
  • Tests refactored to reduce boilerplate (small helpers and rstest parameterisation) and strengthened to assert inner formula texts, where_clauses, as_name/fix and precise span propagation. Malformed where-clause scenarios are explicitly tested to assert schema-invalid diagnostics.

Documentation & migration

Updates and new docs:

  • New execplan: docs/execplans/4-1-5-normalization-into-canonical-formula-model.md (design, acceptance criteria, sketches).
  • Updates: docs/sempai-query-language-design.md, docs/users-guide.md, docs/developers-guide.md — describe the normalization pipeline, semantic checks, span-selection rules and Engine::compile_yaml behaviour.
  • Migration guide: docs/sempai-v0.1-to-v0.2-migration-guide.md (notes on compile_yaml returning QueryPlan(s) and new deterministic diagnostics).
  • docs/roadmap.md marks milestone 4.1.5 complete.

Dependencies

Adds serde_json and tracing to crates/sempai/Cargo.toml; adds insta and proptest to dev-dependencies for snapshot and property testing.

Reviewer feedback applied / tracked follow-ups

  • Tests: extracted helpers and parameterised test cases; strengthened assertions to unpack nested formulas and metadata.
  • Normalization: made fallible for malformed where-clauses; added parse_constraint to convert raw JSON into Constraint variants (follow-ups track decoupling raw serde_json::Value from core types where relevant).
  • Semantic validation: improved span selection (prefer node → child → fallback) and reduced analyze_formula complexity via per-variant helpers.
  • Observability & safety: added tracing, enforced MAX_FORMULA_DEPTH, and tracked follow-ups for performance (reduce redundant traversals, minimise span cloning), developer docs, diagnostic snapshots and further proptest expansions.

Acceptance / validation

Per tests and docs: normalization parity between legacy and v2 fixtures, schema-invalid diagnostics for malformed where-clauses, semantic diagnostics for the two enforced invariants, end-to-end Engine::compile_yaml producing QueryPlan(s) with Decorated, extensive unit/integration/property/snapshot tests, and documentation/migration guidance are present. The new execplan (docs/execplans/4-1-5-normalization-into-canonical-formula-model.md) is included and referenced by code and docs changes.

Walkthrough

Summarise the addition of a canonical Formula AST with Decorated metadata; implement normalization from legacy/v2 principals and structural parsing of where-clauses; add semantic validation and depth/shape checks; wire normalization into Engine::compile_yaml to emit per-language QueryPlans sharing Arc<Decorated>; add tests and docs.

Changes

Canonical Formula Normalisation and Semantic Validation

Layer / File(s) Summary
Data Shape: Canonical Formula Model
crates/sempai-core/src/formula.rs, crates/sempai-core/src/lib.rs
Add Formula enum (Atom, Not, Inside, Anywhere, And, Or), Atom variants (Pattern, Regex, TreeSitterQuery), PatternAtom/RegexAtom/TreeSitterQueryAtom, generic Decorated<T> with where_clauses/as_name/fix/span, WhereClause and Constraint (MetavariableRegex, MetavariablePattern, Other); export formula module.
Normalisation Pipeline
crates/sempai/src/normalize.rs, crates/sempai/src/normalize_constraints.rs, crates/sempai/src/normalize_trace.rs, crates/sempai/src/lib.rs
Add normalize_search_principal to lower Legacy and v2 Match principals into Decorated<Formula>; implement parse_constraint to map where JSON to Constraint variants or Other; preserve as_name/fix/span; lower r2c-internal-project-depends-on to a degenerate TreeSitterQuery atom; add tracing/size introspection helpers; register new submodules.
Semantic Validation
crates/sempai/src/semantic_check.rs
Add validate_formula and MAX_FORMULA_DEPTH = 1000; enforce Not not appear in Or (ESempaiInvalidNotInOr), require at least one positive term in And (ESempaiMissingPositiveTermInAnd); compute deterministic primary_span fallbacks and log first diagnostic code on failure.
Engine Wiring and QueryPlan
crates/sempai/src/engine.rs, crates/sempai/src/lib.rs
Change QueryPlan to hold formula: Arc<Decorated<Formula>> (remove Clone derive); add QueryPlan::formula() accessor; implement Engine::compile_yaml to parse rules, filter to Search principals, normalise & validate formulas, and compile per-language QueryPlans reusing a shared Arc per rule; map language parse failures to ESempaiSchemaInvalid.
Unit Tests: Normalisation & Constraints
crates/sempai/src/tests/normalization_tests.rs, crates/sempai/src/tests/normalization_constraint_tests.rs, crates/sempai/src/tests/normalization_metadata_tests.rs
Add tests validating legacy and v2 lowering, unary/branch mappings, constraint parsing into where_clauses and mapping to Constraint variants, preservation of as_name/fix, and span propagation; include malformed-constraint negative tests.
Unit & Property Tests: Semantic Validation
crates/sempai/src/tests/semantic_validation_tests.rs, crates/sempai/src/tests/property_tests.rs
Add unit tests for Not-in-Or and missing-positive-term-in-And with span precedence checks; add proptest generators exercising invariants and asserting expected diagnostic codes or success.
Integration & Behaviour Tests / Snapshots
crates/sempai/src/tests/engine_tests.rs, crates/sempai/src/tests/behaviour.rs, crates/sempai/src/tests/engine_integration_tests.rs, crates/sempai/src/tests/diagnostic_snapshot_tests.rs, crates/sempai/tests/features/sempai_engine.feature, crates/sempai/src/tests/mod.rs
Update BDD and unit expectations so compile_yaml returns Vec<QueryPlan> for valid search rules; assert plan counts, rule_id, language, and normalized Formula content; add JSON redaction and insta snapshots for diagnostics; assert shared Arc reuse across language plans.
Configuration and Dependencies
crates/sempai/Cargo.toml
Add serde_json (workspace) and tracing = "0.1" to dependencies; add insta = "1" and proptest = "1.5" to dev-dependencies.
Documentation & Guides
docs/execplans/4-1-5-normalization-into-canonical-formula-model.md, docs/developers-guide.md, docs/users-guide.md, docs/sempai-query-language-design.md, docs/sempai-v0.1-to-v0.2-migration-guide.md, docs/roadmap.md
Add execplan and developer overview; update user guide and design doc to describe canonical formula model, normalization pipeline, and semantic diagnostics (E_SEMPAI_INVALID_NOT_IN_OR, E_SEMPAI_MISSING_POSITIVE_TERM_IN_AND, E_SEMPAI_SCHEMA_INVALID, E_SEMPAI_UNSUPPORTED_MODE); mark roadmap milestone 4.1.5 complete.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant Engine as Engine
    participant Parser as Parser
    participant Normaliser as Normaliser
    participant Validator as Validator
    participant Compiler as Compiler
    participant QueryPlan as QueryPlan

    Client->>Engine: compile_yaml(yaml_str)
    Engine->>Parser: Parse YAML rules
    Parser-->>Engine: Parsed rules (SearchQueryPrincipal list)
    Engine->>Engine: Filter to Search principals
    Engine->>Normaliser: normalize_search_principal(principal)
    Normaliser-->>Engine: Decorated<Formula>
    Engine->>Validator: validate_formula(formula)
    alt Validation passes
        Validator-->>Engine: Ok
        Engine->>Compiler: compile_rule_plans(rule, Arc<formula>)
        loop For each language
            Compiler->>QueryPlan: QueryPlan::new(rule_id, language, Arc<formula>)
            QueryPlan-->>Compiler: QueryPlan
        end
        Compiler-->>Engine: Vec<QueryPlan>
        Engine-->>Client: Ok(Vec<QueryPlan>)
    else Validation fails
        Validator-->>Engine: Err(DiagnosticReport)
        Engine-->>Client: Err(DiagnosticReport)
    end
Loading

Possibly related issues

Possibly related PRs

Poem

From legacy patterns and v2 matches refined,
A canonical Formula took shape in kind.
Normalise, validate, compile per-language with care,
Return QueryPlans that carry the rule's share.
Diagnostics now point where the problems are declared.

✨ 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 implement-normalization-canonical-formula-6op9mx

codescene-delta-analysis[bot]

This comment was marked as outdated.

@sourcery-ai

sourcery-ai Bot commented Apr 11, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements a canonical Formula model in sempai-core and wires a full normalization + semantic validation pipeline from YAML search rules through to Engine::compile_yaml, replacing the NOT_IMPLEMENTED placeholder with real QueryPlan construction and updating tests and docs accordingly.

Sequence diagram for Engine::compile_yaml normalization and planning

sequenceDiagram
    actor User
    participant Engine
    participant YamlParser as sempai_yaml_parse_rule_file
    participant ModeValidator as validate_supported_modes
    participant Normalizer as normalize_search_principal
    participant Semantic as validate_formula
    participant Planner as compile_rule_plans

    User->>Engine: compile_yaml(yaml)
    Engine->>YamlParser: parse_rule_file(yaml, None)
    YamlParser-->>Engine: RuleFile or DiagnosticReport
    alt parse_error
        Engine-->>User: Err(DiagnosticReport)
    else parse_ok
        Engine->>ModeValidator: validate_supported_modes(file)
        ModeValidator-->>Engine: Ok or DiagnosticReport
        alt mode_error
            Engine-->>User: Err(DiagnosticReport)
        else modes_ok
            loop for each Rule in file.rules()
                Engine->>Engine: principal = rule.principal()
                alt principal_is_search
                    Engine->>Normalizer: normalize_search_principal(principal, rule.rule_span())
                    Normalizer-->>Engine: Decorated_Formula or DiagnosticReport
                    alt normalization_error
                        Engine-->>User: Err(DiagnosticReport)
                    else normalized
                        Engine->>Semantic: validate_formula(&formula)
                        Semantic-->>Engine: Ok or DiagnosticReport
                        alt semantic_error
                            Engine-->>User: Err(DiagnosticReport)
                        else validated
                            Engine->>Planner: compile_rule_plans(rule, &formula)
                            Planner-->>Engine: List~QueryPlan~ or DiagnosticReport
                            alt plan_error
                                Engine-->>User: Err(DiagnosticReport)
                            else plans_ok
                                Engine->>Engine: append plans to result
                            end
                        end
                    end
                else non_search_principal
                    Engine->>Engine: skip or unsupported mode handled earlier
                end
            end
            Engine-->>User: Ok(List~QueryPlan~)
        end
    end
Loading

Class diagram for canonical Formula model and decorators

classDiagram
    class Formula {
        +Atom atom
        +Not not
        +Inside inside
        +Anywhere anywhere
        +And and
        +Or or
    }

    class Atom {
        +Pattern pattern
        +Regex regex
        +TreeSitterQuery tree_sitter_query
    }

    class PatternAtom {
        +String text
    }

    class RegexAtom {
        +String pattern
    }

    class TreeSitterQueryAtom {
        +String query
    }

    class Decorated_Formula {
        +Formula node
        +List~WhereClause~ where_clauses
        +Option~String~ as_name
        +Option~String~ fix
        +Option~SourceSpan~ span
    }

    class WhereClause {
        +Value raw
    }

    class SourceSpan

    Formula --> Atom : uses
    Atom --> PatternAtom : has
    Atom --> RegexAtom : has
    Atom --> TreeSitterQueryAtom : has
    Decorated_Formula --> Formula : wraps
    Decorated_Formula --> WhereClause : contains
    Decorated_Formula --> SourceSpan : optional
Loading

Class diagram for Engine normalization and QueryPlan

classDiagram
    class Engine {
        -EngineConfig config
        +compile_yaml(yaml : &str) Result~List~QueryPlan~~, DiagnosticReport~
    }

    class QueryPlan {
        -String rule_id
        -Language language
        -Decorated_Formula formula
        +new(rule_id : String, language : Language, formula : Decorated_Formula) QueryPlan
        +rule_id() String
        +language() Language
        +formula() &Decorated_Formula
    }

    class Decorated_Formula {
        +Formula node
        +List~WhereClause~ where_clauses
        +Option~String~ as_name
        +Option~String~ fix
        +Option~SourceSpan~ span
    }

    class Formula
    class WhereClause
    class SourceSpan
    class EngineConfig
    class DiagnosticReport
    class Language
    class Rule {
        +id() &str
        +languages() &List~String~
        +rule_span() Option~SourceSpan~
        +principal() &RulePrincipal
    }

    class RulePrincipal {
        +Search search
        +Other other
    }

    class SearchQueryPrincipal

    class NormalizeModule {
        +normalize_search_principal(principal : &SearchQueryPrincipal, rule_span : Option~&SourceSpan~) Result~Decorated_Formula, DiagnosticReport~
    }

    class SemanticCheckModule {
        +validate_formula(formula : &Decorated_Formula) Result~(), DiagnosticReport~
    }

    Engine --> EngineConfig : has
    Engine ..> QueryPlan : creates
    Engine ..> Rule : uses
    Engine ..> NormalizeModule : calls
    Engine ..> SemanticCheckModule : calls
    Engine ..> DiagnosticReport : returns

    QueryPlan --> Language : has
    QueryPlan --> Decorated_Formula : has

    Rule --> RulePrincipal : has
    RulePrincipal --> SearchQueryPrincipal : may_wrap

    NormalizeModule ..> Decorated_Formula : returns
    NormalizeModule ..> DiagnosticReport : error

    SemanticCheckModule ..> DiagnosticReport : error
Loading

File-Level Changes

Change Details Files
Introduce canonical Formula AST and decoration model in sempai-core for use by normalization and engine layers.
  • Add formula.rs defining Formula, Atom, PatternAtom, RegexAtom, TreeSitterQueryAtom, Decorated, and WhereClause types with serde_json-backed where clauses and optional spans.
  • Expose the new formula module from sempai-core by adding a public mod and re-exporting it.
  • Add serde_json as a dependency of sempai-core for WhereClause raw JSON storage.
crates/sempai-core/src/formula.rs
crates/sempai-core/src/lib.rs
crates/sempai-core/Cargo.toml
Implement normalization from legacy and v2 search principals into Decorated and semantic validation over the canonical formula tree, then wire this into Engine::compile_yaml and QueryPlan.
  • Add normalize.rs implementing normalize_search_principal plus helpers to map LegacyFormula/MatchFormula/ProjectDependsOn into Decorated, including handling of patterns, regexes, boolean combinators, context operators, constraints as WhereClause, and a degenerate dependency formula.
  • Add semantic_check.rs with validate_formula enforcing InvalidNotInOr and MissingPositiveTermInAnd via recursive checks that emit structured DiagnosticReports with the appropriate DiagnosticCode and spans.
  • Refactor Engine::compile_yaml to parse rules, validate modes, normalize each Search principal to Decorated, run validate_formula, and then build per-language QueryPlans via a new compile_rule_plans helper; replace the previous NOT_IMPLEMENTED diagnostic return path.
  • Change QueryPlan to hold a Decorated instead of the unit _plan placeholder, add a formula() accessor, and update its constructor signature and all call sites (including tests and BDD world) to pass a dummy formula where needed.
  • Import the new normalization and semantic validation modules from lib.rs so they are available within the sempai crate.
crates/sempai/src/normalize.rs
crates/sempai/src/semantic_check.rs
crates/sempai/src/engine.rs
crates/sempai/src/lib.rs
crates/sempai/src/tests/behaviour.rs
crates/sempai/src/tests/engine_tests.rs
Add focused unit tests for normalization and semantic validation behaviour over the canonical Formula model.
  • Create normalization_tests.rs to assert that various LegacyFormula and MatchFormula inputs normalize into the expected Formula shapes (Atoms, And/Or, Not/Inside/Anywhere, compound legacy operators like pattern-not-inside and pattern-not-regex).
  • Create semantic_validation_tests.rs to exercise validate_formula success and failure paths, including Or with Not branches, And without positive terms, and nested Or-in-And cases.
  • Wire the new test modules into the sempai test suite via mod declarations in tests/mod.rs and adjust existing engine tests to use dummy Decorated instances when constructing QueryPlan.
  • Add helper functions in tests (e.g., dummy_formula) to construct minimal Decorated required by the new QueryPlan API.
crates/sempai/src/tests/normalization_tests.rs
crates/sempai/src/tests/semantic_validation_tests.rs
crates/sempai/src/tests/mod.rs
crates/sempai/src/tests/engine_tests.rs
Update documentation and roadmap to describe the live normalization path, semantic checks, and completed milestone, and add an execution plan document for this work.
  • Revise the users guide to describe compile_yaml as producing canonical Formula-based QueryPlans for valid search rules, detailing semantic diagnostics and behaviour for unsupported modes and project-depends-on rules.
  • Add an implementation note to sempai-query-language-design.md explaining where normalization and semantic validation live in the codebase, how project-depends-on is represented, and how WhereClause is currently handled as opaque JSON.
  • Mark roadmap milestone 4.1.5 as completed and add a new execplans/4-1-5-normalization-into-canonical-formula-model.md document laying out constraints, risks, stages, interfaces, and validation criteria for this normalization work.
  • Ensure references in docs to NOT_IMPLEMENTED placeholders are updated to reflect the new behaviour and error modes.
docs/users-guide.md
docs/sempai-query-language-design.md
docs/roadmap.md
docs/execplans/4-1-5-normalization-into-canonical-formula-model.md

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 changed the title Normalize legacy and v2 rules into canonical Formula model Plan normalization into canonical Formula model Apr 11, 2026
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos

leynos commented Apr 13, 2026

Copy link
Copy Markdown
Owner Author

@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/sempai/src/tests/normalization_tests.rs

Comment on file

fn legacy_pattern_normalizes_to_atom() {
    let legacy = LegacyFormula::Pattern(String::from("foo($X)"));
    let principal = SearchQueryPrincipal::Legacy(legacy);
    let result = normalize_search_principal(&principal, None).expect("should normalize");
    assert_eq!(
        result.node,
        Formula::Atom(Atom::Pattern(PatternAtom {
            text: String::from("foo($X)")
        }))
    );
}

❌ New issue: Code Duplication
The module contains 6 functions with similar structure: legacy_pattern_normalizes_to_atom,legacy_pattern_regex_normalizes_to_regex_atom,v2_all_normalizes_to_and,v2_any_normalizes_to_or and 2 more functions

@leynos

leynos commented Apr 13, 2026

Copy link
Copy Markdown
Owner Author

@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/sempai/src/normalize.rs

Comment on file

fn normalize_match(
    formula: &MatchFormula,
    fallback_span: Option<SourceSpan>,
) -> Decorated<Formula> {
    match formula {
        MatchFormula::Pattern(text) | MatchFormula::PatternObject(text) => Decorated {
            node: Formula::Atom(Atom::Pattern(PatternAtom { text: text.clone() })),
            where_clauses: vec![],
            as_name: None,
            fix: None,
            span: fallback_span,
        },
        MatchFormula::Regex(pattern) => Decorated {
            node: Formula::Atom(Atom::Regex(RegexAtom {
                pattern: pattern.clone(),
            })),
            where_clauses: vec![],
            as_name: None,
            fix: None,
            span: fallback_span,
        },
        MatchFormula::All(branches) => {
            let normalized_branches = branches
                .iter()
                .map(|branch| normalize_match(branch, fallback_span.clone()))
                .collect();
            Decorated {
                node: Formula::And(normalized_branches),
                where_clauses: vec![],
                as_name: None,
                fix: None,
                span: fallback_span,
            }
        }
        MatchFormula::Any(branches) => {
            let normalized_branches = branches
                .iter()
                .map(|branch| normalize_match(branch, fallback_span.clone()))
                .collect();
            Decorated {
                node: Formula::Or(normalized_branches),
                where_clauses: vec![],
                as_name: None,
                fix: None,
                span: fallback_span,
            }
        }
        MatchFormula::Not(inner) => {
            let normalized_inner = normalize_match(inner, fallback_span.clone());
            Decorated {
                node: Formula::Not(Box::new(normalized_inner)),
                where_clauses: vec![],
                as_name: None,
                fix: None,
                span: fallback_span,
            }
        }
        MatchFormula::Inside(inner) => {
            let normalized_inner = normalize_match(inner, fallback_span.clone());
            Decorated {
                node: Formula::Inside(Box::new(normalized_inner)),
                where_clauses: vec![],
                as_name: None,
                fix: None,
                span: fallback_span,
            }
        }
        MatchFormula::Anywhere(inner) => {
            let normalized_inner = normalize_match(inner, fallback_span.clone());
            Decorated {
                node: Formula::Anywhere(Box::new(normalized_inner)),
                where_clauses: vec![],
                as_name: None,
                fix: None,
                span: fallback_span,
            }
        }
        MatchFormula::Decorated {
            formula: inner_formula,
            where_clauses: raw_where,
            as_name,
            fix,
        } => {
            let mut normalized = normalize_match(inner_formula, fallback_span);
            normalized.where_clauses = raw_where
                .iter()
                .map(|raw| WhereClause { raw: raw.clone() })
                .collect();
            normalized.as_name.clone_from(as_name);
            normalized.fix.clone_from(fix);
            normalized
        }
    }
}

❌ New issue: Large Method
normalize_match has 94 lines, threshold = 70

@leynos

leynos commented Apr 13, 2026

Copy link
Copy Markdown
Owner Author

@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/sempai/src/normalize.rs

Comment on file

fn normalize_legacy(
    formula: &LegacyFormula,
    fallback_span: Option<SourceSpan>,
) -> Decorated<Formula> {
    match formula {
        LegacyFormula::Pattern(text) => Decorated {
            node: Formula::Atom(Atom::Pattern(PatternAtom { text: text.clone() })),
            where_clauses: vec![],
            as_name: None,
            fix: None,
            span: fallback_span,
        },
        LegacyFormula::PatternRegex(pattern) => Decorated {
            node: Formula::Atom(Atom::Regex(RegexAtom {
                pattern: pattern.clone(),
            })),
            where_clauses: vec![],
            as_name: None,
            fix: None,
            span: fallback_span,
        },
        LegacyFormula::Patterns(clauses) => normalize_legacy_patterns(clauses, fallback_span),
        LegacyFormula::PatternEither(branches) => {
            let normalized_branches = branches
                .iter()
                .map(|branch| normalize_legacy(branch, fallback_span.clone()))
                .collect();
            Decorated {
                node: Formula::Or(normalized_branches),
                where_clauses: vec![],
                as_name: None,
                fix: None,
                span: fallback_span,
            }
        }
        LegacyFormula::PatternNot(value) => {
            let inner = normalize_legacy_value(value, fallback_span.clone());
            Decorated {
                node: Formula::Not(Box::new(inner)),
                where_clauses: vec![],
                as_name: None,
                fix: None,
                span: fallback_span,
            }
        }
        LegacyFormula::PatternInside(value) => {
            let inner = normalize_legacy_value(value, fallback_span.clone());
            Decorated {
                node: Formula::Inside(Box::new(inner)),
                where_clauses: vec![],
                as_name: None,
                fix: None,
                span: fallback_span,
            }
        }
        LegacyFormula::PatternNotInside(value) => {
            let inner = normalize_legacy_value(value, fallback_span.clone());
            let inside = Decorated {
                node: Formula::Inside(Box::new(inner)),
                where_clauses: vec![],
                as_name: None,
                fix: None,
                span: fallback_span.clone(),
            };
            Decorated {
                node: Formula::Not(Box::new(inside)),
                where_clauses: vec![],
                as_name: None,
                fix: None,
                span: fallback_span,
            }
        }
        LegacyFormula::PatternNotRegex(pattern) => {
            let regex_atom = Decorated {
                node: Formula::Atom(Atom::Regex(RegexAtom {
                    pattern: pattern.clone(),
                })),
                where_clauses: vec![],
                as_name: None,
                fix: None,
                span: fallback_span.clone(),
            };
            Decorated {
                node: Formula::Not(Box::new(regex_atom)),
                where_clauses: vec![],
                as_name: None,
                fix: None,
                span: fallback_span,
            }
        }
        LegacyFormula::Anywhere(value) => {
            let inner = normalize_legacy_value(value, fallback_span.clone());
            Decorated {
                node: Formula::Anywhere(Box::new(inner)),
                where_clauses: vec![],
                as_name: None,
                fix: None,
                span: fallback_span,
            }
        }
    }
}

❌ New issue: Large Method
normalize_legacy has 102 lines, threshold = 70

@coderabbitai

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

@leynos leynos changed the title Plan normalization into canonical Formula model Normalize legacy and v2 into canonical Formula with semantic checks Apr 17, 2026
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos
leynos marked this pull request as ready for review April 22, 2026 08:55
sourcery-ai[bot]

This comment was marked as resolved.

@coderabbitai coderabbitai Bot added the Roadmap label Apr 22, 2026
chatgpt-codex-connector[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

codescene-delta-analysis[bot]

This comment was marked as outdated.

coderabbitai[bot]

This comment was marked as resolved.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (1)
crates/sempai/src/normalize.rs (1)

252-258: ⚠️ Potential issue | 🟠 Major

Replace the matchable (ERROR) dependency placeholder.

Fix Line 257. Stop emitting a query that can match parser recovery nodes in malformed source. Encode a guaranteed non-match placeholder path instead of a live query shape.

Safer placeholder strategy (non-match by contradiction)
-        Formula::Atom(Atom::TreeSitterQuery(TreeSitterQueryAtom {
-            query: String::from("(ERROR) `@_dependency_check`"),
-        })),
+        Formula::Atom(Atom::TreeSitterQuery(TreeSitterQueryAtom {
+            // Keep this syntactically valid while making it semantically impossible to match.
+            query: String::from(
+                "(ERROR) `@_dependency_check` (`#eq`? `@_dependency_check` \"__sempai_never__\") (`#eq`? `@_dependency_check` \"__sempai_also_never__\")",
+            ),
+        })),
In Tree-sitter query semantics, does `(ERROR) `@cap`` match parse-recovery ERROR nodes in malformed syntax trees?

Are multiple predicates on the same capture conjunctive, such that contradictory `#eq?` predicates force a no-match result?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/sempai/src/normalize.rs` around lines 252 - 258, Replace the live
Tree-sitter query string "(ERROR) `@_dependency_check`" with a guaranteed
non-matching placeholder so the rule cannot accidentally match parser-recovery
nodes; update the TreeSitterQueryAtom creation in the
bare(Formula::Atom(Atom::TreeSitterQuery(TreeSitterQueryAtom { query: ... })))
expression to use a non-existent node name or an explicitly contradictory
predicate (for example a node type like "__never_node__" or a capture with a
contradiction) instead of "(ERROR) `@_dependency_check`" so the query is
impossible to satisfy at parse time.
🤖 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/sempai/src/engine.rs`:
- Around line 17-39: The code currently clones Decorated<Formula> for each
language in compile_rule_plans; change QueryPlan to hold Arc<Decorated<Formula>>
and update compile_rule_plans to wrap the incoming formula in Arc::new(...) once
and clone the Arc for each QueryPlan instead of deep-cloning the formula. Update
other call sites mentioned (the blocks at the other two places referenced) to
construct QueryPlan with Arc<Decorated<Formula>> as well, and adjust
signature/usages of QueryPlan::new and any fields to accept
Arc<Decorated<Formula>> so the formula is shared across per-language plans.

In `@crates/sempai/src/normalize.rs`:
- Line 25: Update the malformed module doc bullet that currently reads //! -
`regex: "...` → `Formula::Atom(Atom::Regex(...))` by closing the inline code
literal (change it to //! - `regex: "..."` → `Formula::Atom(Atom::Regex(...))`)
so the backtick pair is balanced; locate the module-level doc comment containing
that snippet in normalize.rs and fix the punctuation to keep generated docs
syntactically valid.

In `@crates/sempai/src/tests/normalization_tests.rs`:
- Around line 260-267: Add a new unit test that constructs a
SearchQueryPrincipal::ProjectDependsOn variant and calls
normalize_search_principal with a non‑None SourceSpan to ensure the
ProjectDependsOn branch is exercised; assert the returned normalized principal
has the canonical shape you expect for ProjectDependsOn (match the same
structural form used elsewhere in tests) and that normalized.span == Some(span)
to lock metadata propagation for this branch. Locate the normalization
entrypoint normalize_search_principal and mirror the style of the existing
span_propagates_from_search_principal_to_decorated test: create a SourceSpan,
build a ProjectDependsOn principal value, call
normalize_search_principal(&principal, Some(&span)), then add explicit
assertions for both the normalized principal variant/contents and the span
propagation.
- Around line 37-154: Multiple small tests repeat the same
normalize_legacy/normalize_v2 assertions; replace these duplicated single-case
tests (e.g., legacy_pattern_normalizes_to_atom,
legacy_pattern_regex_normalizes_to_regex_atom,
v2_pattern_shorthand_normalizes_to_atom, v2_regex_normalizes_to_regex_atom,
v2_all_normalizes_to_and, v2_any_normalizes_to_or, v2_not_normalizes_to_not,
v2_inside_normalizes_to_inside, v2_anywhere_normalizes_to_anywhere) with
parameterized rstest table cases that iterate inputs
(LegacyFormula::Pattern/PatternRegex/... and
MatchFormula::Pattern/Regex/All/Any/Not/Inside/Anywhere) and expected normalized
predicate types from normalize_legacy and normalize_v2; keep the
nested-structure tests (like legacy_pattern_not_regex_normalizes_to_not_regex
and legacy_pattern_not_inside_normalizes_to_not_inside) as bespoke tests. Use
#[rstest] with case(...) entries and assert matches!(result, ...) or equality as
appropriate, referencing normalize_legacy and normalize_v2 in the fixture.

---

Duplicate comments:
In `@crates/sempai/src/normalize.rs`:
- Around line 252-258: Replace the live Tree-sitter query string "(ERROR)
`@_dependency_check`" with a guaranteed non-matching placeholder so the rule
cannot accidentally match parser-recovery nodes; update the TreeSitterQueryAtom
creation in the bare(Formula::Atom(Atom::TreeSitterQuery(TreeSitterQueryAtom {
query: ... }))) expression to use a non-existent node name or an explicitly
contradictory predicate (for example a node type like "__never_node__" or a
capture with a contradiction) instead of "(ERROR) `@_dependency_check`" so the
query is impossible to satisfy at parse time.
🪄 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: c1d318bc-36dd-49fe-aafc-3c221f1544a6

📥 Commits

Reviewing files that changed from the base of the PR and between 46b9eb7 and bd3f5ee.

📒 Files selected for processing (3)
  • crates/sempai/src/engine.rs
  • crates/sempai/src/normalize.rs
  • crates/sempai/src/tests/normalization_tests.rs

Comment thread crates/sempai/src/engine.rs Outdated
Comment thread crates/sempai/src/normalize.rs Outdated
Comment thread crates/sempai/src/tests/normalization_tests.rs Outdated
Comment thread crates/sempai/src/tests/normalization_tests.rs
@leynos

leynos commented Apr 24, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix. Include the file and symbol names indicated in the issue at the head of your response.

Code Duplication

crates/sempai/src/tests/engine_tests.rs:

What lead to degradation?

The module contains 2 functions with similar structure: compile_yaml_returns_semantic_error_for_invalid_not_in_or,compile_yaml_returns_semantic_error_for_missing_positive_term_in_and

Why does this problem occur?

Duplicated code often leads to code that's harder to change since the same logical change has to be done in multiple functions. More duplication gives lower code health.

How to fix it?

A certain degree of duplicated code might be acceptable. The problems start when it is the same behavior that is duplicated across the functions in the module, ie. a violation of the Don't Repeat Yourself (DRY) principle. DRY violations lead to code that is changed together in predictable patterns, which is both expensive and risky. DRY violations can be identified using CodeScene's X-Ray analysis to detect clusters of change coupled functions with high code similarity. Read More
Once you have identified the similarities across functions, look to extract and encapsulate the concept that varies into its own function(s). These shared abstractions can then be re-used, which minimizes the amount of duplication and simplifies change.

@coderabbitai

This comment was marked as resolved.

codescene-delta-analysis[bot]

This comment was marked as outdated.

coderabbitai[bot]

This comment was marked as resolved.

@leynos

leynos commented Apr 24, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix. Include the file and symbol names indicated in the issue at the head of your response.

crates/sempai/src/tests/engine_tests.rs

Comment on lines +121 to +134

fn compile_yaml_returns_semantic_error_for_missing_positive_term_in_and() {
    let engine = default_engine();
    let result = engine.compile_yaml(concat!(
        "rules:\n",
        "  - id: demo.missing.positive.term.in.and\n",
        "    message: missing positive term in and\n",
        "    languages: [rust]\n",
        "    severity: ERROR\n",
        "    patterns:\n",
        "      - pattern-not: foo($X)\n",
        "      - pattern-inside: bar($Y)\n",
    ));
    let (code, _diag) = first_diagnostic_of_err(result);
    assert_eq!(code, DiagnosticCode::ESempaiMissingPositiveTermInAnd);

❌ New issue: Code Duplication
The module contains 2 functions with similar structure: compile_yaml_returns_semantic_error_for_invalid_not_in_or,compile_yaml_returns_semantic_error_for_missing_positive_term_in_and

@coderabbitai

This comment was marked as resolved.

@leynos leynos changed the title Normalize legacy and v2 into canonical Formula with semantic checks Implement normalization into canonical Formula with semantic checks Apr 24, 2026
Clone the first diagnostic before snapshotting and assert its stable code and
message fragment outside the redacted JSON payload. Keep snapshots focused on
the redacted report shape while guarding the semantic fields directly.
codescene-delta-analysis[bot]

This comment was marked as outdated.

Extend Sempai engine coverage for decorated query plan metadata and span
propagation, and add malformed metavariable-pattern coverage for the
fallible constraint parser.

Add normalization metadata tests for decorated All, Any, Not, Inside, and Anywhere wrappers so metadata stays on the outer node while child spans and empty child metadata are preserved.
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos

leynos commented May 2, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix. Include the file and symbol names indicated in the issue at the head of your response.

crates/sempai/src/tests/normalization_metadata_tests.rs

Comment on lines +88 to +108

fn v2_decorated_over_all_preserves_metadata_and_spans() {
    let span = SourceSpan::new(12, 99, Some(String::from("file:///rule.yaml")));
    let principal = decorated_match_formula(MatchFormula::All(vec![
        MatchFormula::Pattern(String::from("a")),
        MatchFormula::Pattern(String::from("b")),
    ]));

    let decorated =
        normalize_search_principal(&principal, Some(&span)).expect("formula should normalize");

    assert_decorated_metadata(&decorated, &span);
    match &decorated.node {
        Formula::And(children) => {
            assert_two_pattern_branches(children, "a", "b");
            for child in children {
                assert_empty_metadata_with_span(child, &span);
            }
        }
        other => panic!("expected And formula, got {other:?}"),
    }
}

❌ New issue: Code Duplication
The module contains 5 functions with similar structure: v2_decorated_over_all_preserves_metadata_and_spans,v2_decorated_over_any_preserves_metadata_and_spans,v2_decorated_over_anywhere_preserves_metadata_and_spans,v2_decorated_over_inside_preserves_metadata_and_spans and 1 more functions

@leynos

leynos commented May 2, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix. Include the file and symbol names indicated in the issue at the head of your response. Ensure that this is validated against the current version of the codegraph.

If further refinement to address this finding would be deleterious, please supply a clear explanatory one to two paragraph markdown message I can paste into the CodeScene web ui's diagnostic suppression function so this diagnostic can be silenced.

crates/sempai/src/tests/normalization_constraint_tests.rs

Comment on lines +67 to +88

fn legacy_patterns_with_only_constraints_produces_and_with_no_children_and_where_clauses() {
    let constraint = json!({"metavariable-regex": {"metavariable": "$X", "regex": "foo.*"}});
    let legacy = LegacyFormula::Patterns(vec![LegacyClause::Constraint(constraint)]);

    let decorated = normalize_legacy_decorated(legacy);

    assert!(matches!(&decorated.node, Formula::And(children) if children.is_empty()));
    assert_eq!(
        decorated.where_clauses.first().map(|c| &c.constraint),
        Some(&Constraint::MetavariableRegex {
            metavariable: String::from("$X"),
            regex: String::from("foo.*"),
        })
    );

    let err = validate_formula(&decorated).expect_err("constraint-only And should fail");
    let first = err.diagnostics().first().expect("expected diagnostic");
    assert_eq!(
        first.code(),
        DiagnosticCode::ESempaiMissingPositiveTermInAnd
    );
}

❌ New issue: Code Duplication
The module contains 6 functions with similar structure: compile_yaml_reports_schema_invalid_for_malformed_metavariable_pattern_where_clause,compile_yaml_reports_schema_invalid_for_malformed_where_clause,legacy_patterns_with_malformed_known_constraint_fails_normalization,legacy_patterns_with_malformed_metavariable_pattern_fails_normalization and 2 more functions

@coderabbitai

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

Extract shared legacy constraint helpers and collapse malformed known
constraint scenarios into a parameterized rstest. Keep the named
constraint-only validation tests intact while sharing their diagnostic tail.
codescene-delta-analysis[bot]

This comment was marked as outdated.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
crates/sempai/src/tests/normalization_constraint_tests.rs (1)

183-245: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Collapse these two YAML checks into one #[rstest] table.

The setup, execution, and diagnostic assertions are duplicated across both cases; parameterise the YAML snippet and expected message so this stays easy to extend.

♻️ Proposed refactor
-#[test]
-fn compile_yaml_reports_schema_invalid_for_malformed_where_clause() {
-    let yaml = concat!(
-        "rules:\n",
-        "  - id: demo.invalid.where\n",
-        "    message: invalid where\n",
-        "    languages: [rust]\n",
-        "    severity: ERROR\n",
-        "    patterns:\n",
-        "      - pattern: foo($X)\n",
-        "      - metavariable-regex:\n",
-        "          metavariable: $X\n",
-    );
-
-    let report = Engine::new(EngineConfig::default())
-        .compile_yaml(yaml)
-        .expect_err("malformed known constraint should fail");
-
-    assert_eq!(
-        first_diagnostic_code(&report),
-        DiagnosticCode::ESempaiSchemaInvalid
-    );
-    assert!(
-        report
-            .diagnostics()
-            .first()
-            .expect("expected diagnostic")
-            .message()
-            .contains("invalid where-clause")
-    );
-}
-
-#[test]
-fn compile_yaml_reports_schema_invalid_for_malformed_metavariable_pattern_where_clause() {
-    let yaml = concat!(
-        "rules:\n",
-        "  - id: demo.invalid.pattern.where\n",
-        "    message: invalid metavariable pattern\n",
-        "    languages: [rust]\n",
-        "    severity: ERROR\n",
-        "    patterns:\n",
-        "      - pattern: foo($X)\n",
-        "      - metavariable-pattern:\n",
-        "          pattern: x\n",
-    );
-
-    let report = Engine::new(EngineConfig::default())
-        .compile_yaml(yaml)
-        .expect_err("malformed known constraint should fail");
-
-    assert_eq!(
-        first_diagnostic_code(&report),
-        DiagnosticCode::ESempaiSchemaInvalid
-    );
-    assert!(
-        report
-            .diagnostics()
-            .first()
-            .expect("expected diagnostic")
-            .message()
-            .contains("expected {metavariable, pattern} string fields")
-    );
-}
+#[rstest]
+#[case::metavariable_regex(
+    concat!(
+        "rules:\n",
+        "  - id: demo.invalid.where\n",
+        "    message: invalid where\n",
+        "    languages: [rust]\n",
+        "    severity: ERROR\n",
+        "    patterns:\n",
+        "      - pattern: foo($X)\n",
+        "      - metavariable-regex:\n",
+        "          metavariable: $X\n",
+    ),
+    "invalid where-clause",
+)]
+#[case::metavariable_pattern(
+    concat!(
+        "rules:\n",
+        "  - id: demo.invalid.pattern.where\n",
+        "    message: invalid metavariable pattern\n",
+        "    languages: [rust]\n",
+        "    severity: ERROR\n",
+        "    patterns:\n",
+        "      - pattern: foo($X)\n",
+        "      - metavariable-pattern:\n",
+        "          pattern: x\n",
+    ),
+    "expected {metavariable, pattern} string fields",
+)]
+fn compile_yaml_reports_schema_invalid_for_malformed_constraint(
+    #[case] yaml: &str,
+    #[case] expected_message: &str,
+) {
+    let report = Engine::new(EngineConfig::default())
+        .compile_yaml(yaml)
+        .expect_err("malformed known constraint should fail");
+
+    assert_eq!(
+        first_diagnostic_code(&report),
+        DiagnosticCode::ESempaiSchemaInvalid
+    );
+    assert!(
+        report
+            .diagnostics()
+            .first()
+            .expect("expected diagnostic")
+            .message()
+            .contains(expected_message)
+    );
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/sempai/src/tests/normalization_constraint_tests.rs` around lines 183 -
245, Collapse the two tests
compile_yaml_reports_schema_invalid_for_malformed_where_clause and
compile_yaml_reports_schema_invalid_for_malformed_metavariable_pattern_where_clause
into a single parameterised rstest: create a new function
compile_yaml_reports_schema_invalid_for_malformed_constraint annotated with
#[rstest] and two #[case] entries (e.g. metavariable_regex and
metavariable_pattern) that supply the YAML string and expected_message; keep the
shared test body that calls Engine::new(...).compile_yaml(yaml).expect_err(...),
asserts first_diagnostic_code(&report) == DiagnosticCode::ESempaiSchemaInvalid,
and checks
report.diagnostics().first().unwrap().message().contains(expected_message);
remove the duplicate #[test] functions and ensure you reference the same helper
symbols first_diagnostic_code, Engine, compile_yaml and
DiagnosticCode::ESempaiSchemaInvalid.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@crates/sempai/src/tests/normalization_constraint_tests.rs`:
- Around line 183-245: Collapse the two tests
compile_yaml_reports_schema_invalid_for_malformed_where_clause and
compile_yaml_reports_schema_invalid_for_malformed_metavariable_pattern_where_clause
into a single parameterised rstest: create a new function
compile_yaml_reports_schema_invalid_for_malformed_constraint annotated with
#[rstest] and two #[case] entries (e.g. metavariable_regex and
metavariable_pattern) that supply the YAML string and expected_message; keep the
shared test body that calls Engine::new(...).compile_yaml(yaml).expect_err(...),
asserts first_diagnostic_code(&report) == DiagnosticCode::ESempaiSchemaInvalid,
and checks
report.diagnostics().first().unwrap().message().contains(expected_message);
remove the duplicate #[test] functions and ensure you reference the same helper
symbols first_diagnostic_code, Engine, compile_yaml and
DiagnosticCode::ESempaiSchemaInvalid.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 7dd30e3e-b2ad-4026-ae6d-8ba99fbe9dbc

📥 Commits

Reviewing files that changed from the base of the PR and between 5a670e8 and f8fc6f4.

📒 Files selected for processing (1)
  • crates/sempai/src/tests/normalization_constraint_tests.rs

@leynos

leynos commented May 3, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix. Include the file and symbol names indicated in the issue at the head of your response. Ensure that this is validated against the current version of the codegraph.

If further refinement to address this finding would be deleterious, please supply a clear explanatory one to two paragraph markdown message I can paste into the CodeScene web ui's diagnostic suppression function so this diagnostic can be silenced.

crates/sempai/src/tests/normalization_constraint_tests.rs

Comment on lines +109 to +125

fn legacy_patterns_with_only_constraints_produces_and_with_no_children_and_where_clauses() {
    let constraint = json!({"metavariable-regex": {"metavariable": "$X", "regex": "foo.*"}});
    let legacy = make_legacy_patterns_with_constraints([constraint]);

    let decorated = normalize_legacy_decorated(legacy);

    assert!(matches!(&decorated.node, Formula::And(children) if children.is_empty()));
    assert_eq!(
        decorated.where_clauses.first().map(|c| &c.constraint),
        Some(&Constraint::MetavariableRegex {
            metavariable: String::from("$X"),
            regex: String::from("foo.*"),
        })
    );

    assert_missing_positive_term_in_and_for_decorated(&decorated);
}

❌ New issue: Code Duplication
The module contains 4 functions with similar structure: compile_yaml_reports_schema_invalid_for_malformed_metavariable_pattern_where_clause,compile_yaml_reports_schema_invalid_for_malformed_where_clause,legacy_patterns_with_only_constraints_produces_and_with_no_children_and_where_clauses,legacy_patterns_with_only_metavariable_pattern_constraint_fails_validation

Collapse malformed where-clause engine scenarios into a parameterized
rstest and share their schema-invalid assertion helper. Preserve the
normalization scenario names and expected diagnostics while reducing repeated
assertion code in the constraint test module.
@coderabbitai

This comment was marked as resolved.

codescene-delta-analysis[bot]

This comment was marked as outdated.

Replace repeated decorated All, Any, Not, Inside, and Anywhere span tests
with named rstest cases. Keep the same metadata and child-span assertions
while sharing normalization and extraction helpers.
codescene-delta-analysis[bot]

This comment was marked as outdated.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In `@crates/sempai/src/tests/normalization_metadata_tests.rs`:
- Around line 185-224: The test duplicates decorated-match construction and
manual branch extraction; replace the inline
SearchQueryPrincipal::Match(MatchFormula::Decorated { .. }) setup with the
existing helper decorated_match_formula(MatchFormula::All(...)) and use
extract_and_branches(&decorated.node) instead of the manual match block, then
keep the assertions via assert_two_pattern_branches(children, "a", "b") and
assert_empty_metadata for each child; update
v2_decorated_over_all_wraps_preserves_metadata to call decorated_match_formula,
call normalize_search_principal as before, get children via
extract_and_branches, and run the existing branch assertions.
🪄 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 Plus

Run ID: cbb25ba5-41db-46bd-b179-7e674fb4f909

📥 Commits

Reviewing files that changed from the base of the PR and between f8fc6f4 and 4bd98e3.

📒 Files selected for processing (2)
  • crates/sempai/src/tests/normalization_constraint_tests.rs
  • crates/sempai/src/tests/normalization_metadata_tests.rs

Comment thread crates/sempai/src/tests/normalization_metadata_tests.rs
@leynos

leynos commented May 3, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix. Include the file and symbol names indicated in the issue at the head of your response. Ensure that this is validated against the current version of the codegraph.

If further refinement to address this finding would be deleterious, please supply a clear explanatory one to two paragraph markdown message I can paste into the CodeScene web ui's diagnostic suppression function so this diagnostic can be silenced.

Code Duplication

crates/sempai/src/tests/semantic_validation_tests.rs:

What lead to degradation?

The module contains 3 functions with similar structure: missing_positive_term_in_and_prefers_node_span,missing_positive_term_in_and_uses_first_available_child_span,missing_positive_term_in_and_uses_first_child_span_when_node_span_none

Why does this problem occur?

Duplicated code often leads to code that's harder to change since the same logical change has to be done in multiple functions. More duplication gives lower code health.

How to fix it?

A certain degree of duplicated code might be acceptable. The problems start when it is the same behavior that is duplicated across the functions in the module, ie. a violation of the Don't Repeat Yourself (DRY) principle. DRY violations lead to code that is changed together in predictable patterns, which is both expensive and risky. DRY violations can be identified using CodeScene's X-Ray analysis to detect clusters of change coupled functions with high code similarity. Read More
Once you have identified the similarities across functions, look to extract and encapsulate the concept that varies into its own function(s). These shared abstractions can then be re-used, which minimizes the amount of duplication and simplifies change.

@leynos

leynos commented May 3, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix. Include the file and symbol names indicated in the issue at the head of your response. Ensure that this is validated against the current version of the codegraph.

If further refinement to address this finding would be deleterious, please supply a clear explanatory one to two paragraph markdown message I can paste into the CodeScene web ui's diagnostic suppression function so this diagnostic can be silenced.

Code Duplication

crates/sempai/src/tests/normalization_constraint_tests.rs:

What lead to degradation?

The module contains 2 functions with similar structure: legacy_patterns_with_only_constraints_produces_and_with_no_children_and_where_clauses,legacy_patterns_with_only_metavariable_pattern_constraint_fails_validation

Why does this problem occur?

Duplicated code often leads to code that's harder to change since the same logical change has to be done in multiple functions. More duplication gives lower code health.

How to fix it?

A certain degree of duplicated code might be acceptable. The problems start when it is the same behavior that is duplicated across the functions in the module, ie. a violation of the Don't Repeat Yourself (DRY) principle. DRY violations lead to code that is changed together in predictable patterns, which is both expensive and risky. DRY violations can be identified using CodeScene's X-Ray analysis to detect clusters of change coupled functions with high code similarity. Read More
Once you have identified the similarities across functions, look to extract and encapsulate the concept that varies into its own function(s). These shared abstractions can then be re-used, which minimizes the amount of duplication and simplifies change.

@coderabbitai

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

Fold legacy constraint-only pattern tests into one rstest, reuse
decorated match helpers for v2 All metadata coverage, and parameterise
missing-positive primary span cases with shared builders.

Take `make_legacy_patterns_with_constraints` as a const-sized `Value`
array to match the call sites without changing behaviour.

Co-authored-by: Cursor <cursoragent@cursor.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

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

Inline comments:
In `@crates/sempai/src/tests/normalization_constraint_tests.rs`:
- Around line 110-116: Add an explicit cardinality assertion before inspecting
the first clause: assert_eq!(decorated.where_clauses.len(), 1); then keep the
existing assertion that compares decorated.where_clauses.first().map(|c|
&c.constraint) to Some(&Constraint::MetavariableRegex { metavariable:
String::from("$X"), regex: String::from("foo.*"), }); do the same change for the
other test locations that assert decorated.where_clauses.first() (the
occurrences around the other noted ranges) so duplicate or leaked clauses will
fail loudly.

In `@crates/sempai/src/tests/normalization_metadata_tests.rs`:
- Around line 191-209: The test should explicitly assert that no spans are
present when calling normalize_search_principal(&principal, None): add
assertions that decorated.span is None (or decorated.span.as_ref().is_none())
immediately after normalization and for each child returned by
extract_and_branches(&decorated.node) assert that child.span is None (or
child.span.as_ref().is_none()) in addition to the existing assert_empty_metadata
checks so the root node and all pattern-branch children are verified to carry no
span in the no-fallback path; locate symbols normalize_search_principal,
decorated, decorated.span, extract_and_branches, children and add the None-span
assertions near where assert_two_pattern_branches and assert_empty_metadata are
invoked.
🪄 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 Plus

Run ID: daa250af-6185-4b0b-9c19-5adb934aa887

📥 Commits

Reviewing files that changed from the base of the PR and between 4bd98e3 and a3faf60.

📒 Files selected for processing (3)
  • crates/sempai/src/tests/normalization_constraint_tests.rs
  • crates/sempai/src/tests/normalization_metadata_tests.rs
  • crates/sempai/src/tests/semantic_validation_tests.rs

Comment thread crates/sempai/src/tests/normalization_constraint_tests.rs
Comment thread crates/sempai/src/tests/normalization_metadata_tests.rs
Require exactly one `where_clause` before reading `.first()` in legacy and
v2 normalisation tests so duplicate constraints fail clearly.

Assert no spans on the root and And-branch children when normalising
without a rule span in `v2_decorated_over_all_wraps_preserves_metadata`.

Co-authored-by: Cursor <cursoragent@cursor.com>
@leynos
leynos merged commit 1de4d6b into main May 4, 2026
11 checks passed
@leynos
leynos deleted the implement-normalization-canonical-formula-6op9mx branch May 4, 2026 00:01
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