Skip to content

Emit bounded AST lowering metrics (7.3.2) (#308) - #310

Open
lodyai[bot] wants to merge 2 commits into
mainfrom
issue-308-clone-detector-7-3-2-emit-bounded-ast-lowering-latency-and-outcome-metrics
Open

Emit bounded AST lowering metrics (7.3.2) (#308)#310
lodyai[bot] wants to merge 2 commits into
mainfrom
issue-308-clone-detector-7-3-2-emit-bounded-ast-lowering-latency-and-outcome-metrics

Conversation

@lodyai

@lodyai lodyai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Summary

This branch adds bounded operational metrics for AST span lowering so roadmap
task 7.3.2 can observe latency and categorized results without exposing source,
span, path or per-node data. It centralizes the stable metric vocabulary,
instruments every parser and no-parser exit path, and proves the emitted values
with a local debugging recorder.

Closes #308.

Roadmap task: (7.3.2)

Review walkthrough

Validation

  • make check-fmt: passed.
  • make lint: passed, including Rustdoc and Clippy with warnings denied.
  • make test: passed, 1,592 tests with 3 skipped by the configured filter.
  • make markdownlint: passed.
  • make nixie: passed.
  • mbake validate Makefile: passed.
  • cargo test -p whitaker_clones_core --no-default-features --lib parser_unavailable_stub_records_outcome_and_latency: passed.
  • coderabbit review --agent: passed with zero findings after the implementation and test-module split milestones.
  • git diff --check: passed.

Notes

Feature-vector emission remains reserved for the 7.3.2 scoring and SARIF Run 1
consumer. This branch makes lowering latency and categorized outcomes observable
now, while ensuring a future vector metric counts vectors only when that
consumer actually uses them.

References

leynos added 2 commits July 24, 2026 18:13
Record one latency sample and one categorized outcome for every
lowering attempt, with parser recovery tracked orthogonally.
Keep adapter metrics scenarios separate from syntax-lowering tests so both
modules remain below the repository file-size ceiling.

@sourcery-ai sourcery-ai 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.

Sorry @LodyAI[bot], you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Summary

  • Added bounded AST lowering metrics for latency, outcomes, and parser recovery.
  • Centralised metric names and exhaustive AstError outcome categorisation without exposing source, spans, paths, or node-level data.
  • Instrumented parser and parser-unavailable paths at the lower_span boundary.
  • Added recorder-backed tests covering success, recovery, invalid/unparsable spans, and node/depth budget exhaustion.
  • Documented the operational metrics contract in docs/whitaker-clone-detector-design.md; feature-vector emission remains delegated to the scoring/SARIF Run 1 consumer.

Walkthrough

Changes

Add bounded AST lowering metrics for duration, categorised outcomes, and parser recovery. Instrument both parser-enabled and parser-disabled paths, wire shared dependencies, and add scenario-based validation and design documentation.

AST lowering metrics

Layer / File(s) Summary
Metrics dependencies and recording contract
Cargo.toml, crates/whitaker_clones_core/Cargo.toml, crates/whitaker_clones_core/src/ast/metrics.rs
Add shared metrics dependencies, bounded outcome labels, duration histograms, labelled counters, and metric-recording tests.
Lowering boundary instrumentation
crates/whitaker_clones_core/src/ast/lowering.rs, crates/whitaker_clones_core/src/ast/mod.rs
Time lower_span, propagate parser recovery state, record metrics, and instrument the no-parser fallback.
Scenario coverage and operational contract
crates/whitaker_clones_core/src/ast/lowering_metrics_tests.rs, docs/whitaker-clone-detector-design.md
Test successful, invalid, unparsable, budget-exhausted, and recovery scenarios, and document the metrics boundary and allowed labels.

Sequence Diagram(s)

sequenceDiagram
  participant lower_span
  participant lower_span_inner
  participant record_lower_span_metrics
  lower_span->>lower_span_inner: Lower the byte span
  lower_span_inner-->>lower_span: Return result and recovery flag
  lower_span->>record_lower_span_metrics: Submit duration, result, and recovery
  record_lower_span_metrics-->>lower_span: Record bounded metrics
Loading

Possibly related PRs

Suggested labels: Roadmap

Suggested reviewers: codescene-access

Poem

A span begins, the clock takes flight,
Outcomes gather, bounded tight.
Recovery leaves a measured trace,
Budgets mark their stopping place.
Metrics hum as trees arise.


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (2 errors, 1 warning)

Check name Status Explanation Resolution
Module-Level Documentation ❌ Error Fail: crates/whitaker_clones_core/src/ast/metrics.rs adds #[cfg(test)] mod tests without a module doc comment. Add a //! doc comment to that test module, or move it into a separate documented tests.rs module file.
Unit Architecture ❌ Error lower_span now hides metrics writes and Instant::now() inside a read-style lowering API, so the query boundary is no longer pure or injectable. Push timing and metrics behind an explicit boundary with an injected clock and sink; keep lower_span pure or make the side-effects part of a command wrapper.
Developer Documentation ⚠️ Warning docs/developers-guide.md omits the new ast::metrics boundary, and docs/roadmap.md still shows 7.3.2 unchecked. Update docs/developers-guide.md for the metrics boundary, then mark roadmap 7.3.2 complete once the shipped scope matches.
✅ Passed checks (17 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the PR scope and includes both the roadmap item and linked issue reference.
Description check ✅ Passed The description is directly about bounded AST lowering metrics and the linked roadmap task.
Linked Issues check ✅ Passed The implementation covers bounded lowering latency, all listed AstError outcomes, tests, and no raw-source or per-node labels.
Out of Scope Changes check ✅ Passed The changes stay within AST lowering metrics, tests, dependency wiring, and the related design docs.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Testing (Overall) ✅ Passed Approve this: the tests drive real lower_span scenarios, assert recorder snapshots for every outcome, and would fail on no-op or mislabelled metrics.
User-Facing Documentation ✅ Passed The PR only adds internal AST-lowering metrics; no user-facing API/CLI behaviour changes, and docs/users-guide.md was not changed.
Testing (Unit And Behavioural) ✅ Passed Exercise the public lower_span boundary with real inputs and a local recorder, covering success, each error class, and parser recovery.
Testing (Property / Proof) ✅ Passed PASS: Keep the finite AstError→metric-label mapping table-tested; no broad input-space invariant or proof obligation warrants proptest or proof here.
Testing (Compile-Time / Ui) ✅ Passed No compile-time behaviour is introduced; runtime metrics are covered by recorder-backed assertions, and existing insta snapshots already handle structured AST output.
Domain Architecture ✅ Passed The metrics code stays in a dedicated AST boundary, emits only bounded domain outcomes, and avoids leaking transport, storage, or raw-source details.
Observability ✅ Passed PASS: lower_span logs at recovery/failure boundaries and emits bounded latency/outcome metrics with fixed labels; no high-cardinality fields or new alerts.
Security And Privacy ✅ Passed PASS: New metrics emit only bounded outcome labels and durations; tests use fake inputs; no secrets, auth, or raw source data are logged, labelled, or stored.
Performance And Resource Use ✅ Passed Hot-path work stays constant-time: a single Once-guarded metric registration plus bounded counter/histogram writes, with no new unbounded loops, I/O, or quadratic growth.
Concurrency And State ✅ Passed PASS: Metrics emit synchronously at one boundary; the only shared state is a Once-guarded descriptor init, and tests use a local recorder with no async, locks, or background tasks.
Architectural Complexity And Maintainability ✅ Passed Accept the tiny domain-scoped metrics helper: it removes duplicate lowering-observation logic and is used immediately by both parser and stub paths.
Rust Compiler Lint Integrity ✅ Passed PASS: No new lint suppressions or artificial anchors appear in the touched AST files, and the only clone()s are cheap SyntaxNode handles for traversal/selection.
📋 Issue Planner

Built with CodeRabbit's Coding Plans for faster development and fewer bugs.

View plan used: #308

✨ 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 issue-308-clone-detector-7-3-2-emit-bounded-ast-lowering-latency-and-outcome-metrics

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

codescene-access[bot]

This comment was marked as outdated.

@codescene-access codescene-access 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.

No quality gates enabled for this code.

@leynos
leynos marked this pull request as ready for review July 26, 2026 00:36
@coderabbitai coderabbitai Bot added the Roadmap label Jul 26, 2026

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 02aaf844dc

ℹ️ 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".

}

fn describe_metrics() {
DESCRIBE_METRICS.call_once(|| {

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 Describe metrics for each active recorder

The process-global Once permanently sends these descriptions only to whichever recorder handles the first lower_span call. If that call occurs before the production recorder is installed, or inside with_local_recorder as in these tests, later measurements reach the production recorder without their unit and purpose metadata. Register the descriptions after recorder initialization or avoid a process-wide guard so each active recorder receives them.

AGENTS.md reference: AGENTS.md:L299-L300

Useful? React with 👍 / 👎.

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/whitaker_clones_core/src/ast/lowering.rs (1)

72-135: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Collapse the three early-return matches back into a ?-chain.

lower_span_inner now carries three duplicated match { Ok(x) => x, Err(error) => return (Err(error), recovered) } blocks purely to smuggle recovered out alongside the result. Capture recovered by mutation from an inner closure and let ? do the propagation — same behaviour, far fewer branches to hold in your head at once.

♻️ Proposed refactor
 fn lower_span_inner(file_text: &str, span: ByteSpan) -> (AstResult<NormalizedTree>, bool) {
-    let span = match ByteSpan::new(file_text, span.start(), span.end()).map_err(|error| {
-        trace_ast_error(
-            error,
-            "AST span lies outside the supplied source text",
-            "AST span validation failed",
-        )
-    }) {
-        Ok(span) => span,
-        Err(error) => return (Err(error), false),
-    };
-    let parse = SourceFile::parse(file_text, Edition::CURRENT);
-    let parse_errors = parse.errors();
-    let recovered = !parse_errors.is_empty();
-    if recovered {
-        warn!(
-            start = span.start(),
-            end = span.end(),
-            errors = parse_errors.len(),
-            "lowered AST span from source with parser recovery errors"
-        );
-    }
-    let root = parse.tree().syntax().clone();
-    let target_range = text_range(span);
-    let selected = match select_covering_node(&root, span).map_err(|error| {
-        trace_ast_error(
-            error,
-            "no AST syntax node covers the requested span",
-            "AST covering-node selection failed",
-        )
-    }) {
-        Ok(selected) => selected,
-        Err(error) => return (Err(error), recovered),
-    };
-    debug!(
-        kind = ?selected.kind(),
-        span_width = u32::from(selected.text_range().len()),
-        "selected AST covering node"
-    );
-    let lowered = match LoweringLimits::new(span)
-        .lower(&selected, 0)
-        .map_err(|error| {
-            if matches!(error, AstError::UnparsableSpan { .. }) {
-                error!(
-                    start = span.start(),
-                    end = span.end(),
-                    "selected AST span contains parser error elements"
-                );
-            }
-            error
-        }) {
-        Ok(lowered) => lowered,
-        Err(error) => return (Err(error), recovered),
-    };
-    debug_assert!(selected.text_range().contains_range(target_range));
-    (Ok(NormalizedTree::new(lowered, span)), recovered)
+    let mut recovered = false;
+    let result = (|| -> AstResult<NormalizedTree> {
+        let span = ByteSpan::new(file_text, span.start(), span.end()).map_err(|error| {
+            trace_ast_error(
+                error,
+                "AST span lies outside the supplied source text",
+                "AST span validation failed",
+            )
+        })?;
+        let parse = SourceFile::parse(file_text, Edition::CURRENT);
+        let parse_errors = parse.errors();
+        recovered = !parse_errors.is_empty();
+        if recovered {
+            warn!(
+                start = span.start(),
+                end = span.end(),
+                errors = parse_errors.len(),
+                "lowered AST span from source with parser recovery errors"
+            );
+        }
+        let root = parse.tree().syntax().clone();
+        let target_range = text_range(span);
+        let selected = select_covering_node(&root, span).map_err(|error| {
+            trace_ast_error(
+                error,
+                "no AST syntax node covers the requested span",
+                "AST covering-node selection failed",
+            )
+        })?;
+        debug!(
+            kind = ?selected.kind(),
+            span_width = u32::from(selected.text_range().len()),
+            "selected AST covering node"
+        );
+        let lowered = LoweringLimits::new(span).lower(&selected, 0).map_err(|error| {
+            if matches!(error, AstError::UnparsableSpan { .. }) {
+                error!(
+                    start = span.start(),
+                    end = span.end(),
+                    "selected AST span contains parser error elements"
+                );
+            }
+            error
+        })?;
+        debug_assert!(selected.text_range().contains_range(target_range));
+        Ok(NormalizedTree::new(lowered, span))
+    })();
+    (result, recovered)
 }

As per path instructions, "Seek to keep the cognitive complexity of functions no more than 9."

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

In `@crates/whitaker_clones_core/src/ast/lowering.rs` around lines 72 - 135,
Refactor lower_span_inner to eliminate the duplicated early-return match blocks
by capturing recovered through a mutable outer variable from an inner closure,
then use ? for ByteSpan validation, covering-node selection, and lowering
propagation. Preserve parser-recovery logging, error-specific logging, and the
existing (Err(error), recovered) return behavior.

Source: Path instructions

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

Inline comments:
In `@docs/whitaker-clone-detector-design.md`:
- Around line 284-289: Update the documentation sentence to use an Oxford comma
in “individual nodes, spans, paths, or raw source text” and insert a comma
before “so” in “SARIF Run 1 consumer, so a vector is counted.”

---

Outside diff comments:
In `@crates/whitaker_clones_core/src/ast/lowering.rs`:
- Around line 72-135: Refactor lower_span_inner to eliminate the duplicated
early-return match blocks by capturing recovered through a mutable outer
variable from an inner closure, then use ? for ByteSpan validation,
covering-node selection, and lowering propagation. Preserve parser-recovery
logging, error-specific logging, and the existing (Err(error), recovered) return
behavior.
🪄 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: 77a98852-4a3f-4464-bed0-481819dc3dc0

📥 Commits

Reviewing files that changed from the base of the PR and between 37e1d92 and 02aaf84.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • Cargo.toml
  • crates/whitaker_clones_core/Cargo.toml
  • crates/whitaker_clones_core/src/ast/lowering.rs
  • crates/whitaker_clones_core/src/ast/lowering_metrics_tests.rs
  • crates/whitaker_clones_core/src/ast/metrics.rs
  • crates/whitaker_clones_core/src/ast/mod.rs
  • docs/whitaker-clone-detector-design.md
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • leynos/rstest-bdd (auto-detected)

Comment on lines +284 to +289
The outcome label is limited to `success`, `invalid_span`, `unparsable_span`,
`node_budget_exhausted`, `depth_budget_exhausted` and `parser_unavailable`.
Metrics must not label individual nodes, spans, paths or raw source text.
Feature-vector emission remains owned by the 7.3.2 scoring and SARIF Run 1
consumer so a vector is counted only when that boundary consumes it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the Oxford comma and a comma before "so".

Triage: [type:grammar]

Line 286: "individual nodes, spans, paths or raw source text" needs the Oxford comma before "or". Line 288: "SARIF Run 1 consumer so a vector is counted" needs a comma before "so" since it joins two independent clauses.

✏️ Proposed fix
-Metrics must not label individual nodes, spans, paths or raw source text.
-Feature-vector emission remains owned by the 7.3.2 scoring and SARIF Run 1
-consumer so a vector is counted only when that boundary consumes it.
+Metrics must not label individual nodes, spans, paths, or raw source text.
+Feature-vector emission remains owned by the 7.3.2 scoring and SARIF Run 1
+consumer, so a vector is counted only when that boundary consumes it.

As per coding guidelines, "Use the Oxford comma in documentation: 'ships, planes, and hovercraft' where it aids comprehension."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
The outcome label is limited to `success`, `invalid_span`, `unparsable_span`,
`node_budget_exhausted`, `depth_budget_exhausted` and `parser_unavailable`.
Metrics must not label individual nodes, spans, paths or raw source text.
Feature-vector emission remains owned by the 7.3.2 scoring and SARIF Run 1
consumer so a vector is counted only when that boundary consumes it.
The outcome label is limited to `success`, `invalid_span`, `unparsable_span`,
`node_budget_exhausted`, `depth_budget_exhausted` and `parser_unavailable`.
Metrics must not label individual nodes, spans, paths, or raw source text.
Feature-vector emission remains owned by the 7.3.2 scoring and SARIF Run 1
consumer, so a vector is counted only when that boundary consumes it.
🧰 Tools
🪛 LanguageTool

[style] ~286-~286: The serial comma (Oxford comma, Harvard comma) is missing.
Context: ...must not label individual nodes, spans, paths or raw source text. Feature-vector emissio...

(SERIAL_COMMA_ON)


[uncategorized] ~288-~288: Use a comma before ‘so’ if it connects two independent clauses (unless they are closely connected and short).
Context: ...e 7.3.2 scoring and SARIF Run 1 consumer so a vector is counted only when that boun...

(COMMA_COMPOUND_SENTENCE_2)

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

In `@docs/whitaker-clone-detector-design.md` around lines 284 - 289, Update the
documentation sentence to use an Oxford comma in “individual nodes, spans,
paths, or raw source text” and insert a comma before “so” in “SARIF Run 1
consumer, so a vector is counted.”

Sources: Coding guidelines, Linters/SAST tools

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.

Clone detector 7.3.2: emit bounded AST lowering latency and outcome metrics

0 participants