Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7f833a7
feat(sql): Add Phase 3 procedural SQL metrics
tinovyatkin Aug 20, 2026
d5f66dc
fix(sql): Correct five procedural state-machine findings
tinovyatkin Aug 20, 2026
9c19900
fix(sql): Correct object-touch scoping and block risk scans
tinovyatkin Aug 21, 2026
82a0439
test(sql): Add MySQL and BigQuery procedural fixtures
tinovyatkin Aug 21, 2026
657058b
style(sql): Reword comments that trip the typos checker
tinovyatkin Aug 21, 2026
cb7dcff
fix(sql): Address Codex round-2 procedural findings
tinovyatkin Aug 21, 2026
082963c
fix(sql): Address Codex round-3 procedural findings
tinovyatkin Aug 21, 2026
3acb3aa
fix(sql): Address Codex round-4 procedural findings
tinovyatkin Aug 21, 2026
6bb92c6
fix(sql): Address Codex round-5 procedural findings
tinovyatkin Aug 21, 2026
f42eb80
fix(sql): Address Codex round-6 procedural findings
tinovyatkin Aug 21, 2026
e88fafe
fix(sql): Address Codex round-7 procedural findings
tinovyatkin Aug 22, 2026
83608fa
fix(sql): Address round-8 review findings
tinovyatkin Aug 22, 2026
8762bb9
fix(sql): Address Codex round-9 procedural findings
tinovyatkin Aug 22, 2026
2f90fc5
fix(sql): Address Codex round-10 procedural findings
tinovyatkin Aug 22, 2026
ea051a5
fix(sql): Address Codex round-11 procedural findings
tinovyatkin Aug 22, 2026
f3f9eed
fix(sql): Address Codex round-12 procedural findings
tinovyatkin Aug 23, 2026
0f44cfb
fix(sql): Address Codex round-13 findings
tinovyatkin Aug 23, 2026
0ea450f
fix(sql): Address Codex round-14 procedural findings
tinovyatkin Aug 23, 2026
6e3d0cb
fix(sql): Address Codex round-15 procedural findings
tinovyatkin Aug 23, 2026
687a166
fix(sql): Address Codex round-16 procedural findings
tinovyatkin Aug 23, 2026
af92c43
fix(sql): Address Codex round-17 findings
tinovyatkin Aug 25, 2026
d8a602d
fix(sql): Address Codex round-18 findings
tinovyatkin Aug 25, 2026
5d38d7d
fix(sql): Address Codex round-19 findings
tinovyatkin Aug 25, 2026
863d487
fix(sql): Address Codex round-20 findings
tinovyatkin Aug 25, 2026
1d38867
fix(sql): Address Codex round-21 findings
tinovyatkin Aug 27, 2026
220e9d4
fix(sql): Address Codex round-22 findings
tinovyatkin Aug 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions crates/mehen-sql/src/composite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,10 @@ pub(crate) fn compute(
}
}

/// SQL Structural Complexity (research foundation §8.1).
fn structural(f: &SqlFileFacts) -> f64 {
/// SQL Structural Complexity (research foundation §8.1). Also reused by the
/// procedural module to score the query constructs embedded in one routine
/// (`sql.structural_complexity.max_embedded_query`, §9.3).
pub(crate) fn structural(f: &SqlFileFacts) -> f64 {
1.00 * f.query_block_count as f64
+ 0.80 * f.ctes.count as f64
+ 1.20 * f.ctes.max_dependency_depth as f64
Expand Down Expand Up @@ -175,11 +177,9 @@ fn modularization_credit(f: &SqlFileFacts) -> f64 {

/// SQL Change Risk Score (research foundation §8.4).
///
/// Phase-1 deviation: the spec's `+ 5 * dynamic_sql_count` term is omitted
/// because dynamic SQL (`EXECUTE IMMEDIATE`, `sp_executesql`, …) is a
/// procedural-dialect construct not yet tracked (Phase 3). Every other term
/// matches the spec weights exactly. When dynamic-SQL detection lands, add the
/// `+ 5 * dynamic_sql_count` term here.
/// Every term matches the spec weights exactly, including the
/// `+ 5 × dynamic_sql_count` term (Phase 3 — `EXECUTE IMMEDIATE`,
/// `sp_executesql`, `EXEC('…')`, `DBMS_SQL`).
fn change_risk(f: &SqlFileFacts) -> f64 {
let o = &f.objects;
ChangeRiskFactor::Drop.amount() * o.drop_count as f64
Expand All @@ -188,6 +188,7 @@ fn change_risk(f: &SqlFileFacts) -> f64 {
+ ChangeRiskFactor::DeleteWithoutWhere.amount() * o.delete_without_where_count as f64
+ ChangeRiskFactor::UpdateWithoutWhere.amount() * o.update_without_where_count as f64
+ ChangeRiskFactor::GrantRevoke.amount() * o.grant_revoke_count as f64
+ ChangeRiskFactor::DynamicSql.amount() * f.procedural.dynamic_sql_count as f64
+ ChangeRiskFactor::Merge.amount() * o.merge_count as f64
+ ChangeRiskFactor::CreateOrReplace.amount() * o.create_or_replace_count as f64
+ ChangeRiskFactor::TransactionControl.amount() * o.transaction_control_count as f64
Expand Down
944 changes: 803 additions & 141 deletions crates/mehen-sql/src/facts.rs

Large diffs are not rendered by default.

48 changes: 47 additions & 1 deletion crates/mehen-sql/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ mod dialect;
mod facts;
mod loc;
mod metrics;
mod procedural;

use mehen_core::{
AnalysisBackend, AnalysisConfig, ContributionCollector, Language, LanguageAnalysis,
Expand Down Expand Up @@ -132,7 +133,12 @@ impl LanguageAnalyzer for SqlAnalyzer {
let line_index = &source.line_index;
let line_at = |byte: u32| line_index.line_at(byte);

let mut file_facts = facts::extract(&parsed, &dialect, line_at, config.emit_contributions);
let mut file_facts = facts::extract(
&parsed,
line_at,
config.emit_contributions,
resolution.effective == DialectKind::Tsql,
);
// Lexer errors (malformed tokens) are distinct from unparsable parse
// segments. The current sqruff release never populates this vector, but
// surface them into parser-health so a future version cannot make
Expand All @@ -158,6 +164,18 @@ impl LanguageAnalyzer for SqlAnalyzer {
item.factor.reason(),
);
}
// Procedural composites are evidence-backed too: the published value
// equals the sum of its contributions by construction (§4.7).
for item in &file_facts.procedural.evidence {
let metric = match item.metric {
procedural::ProceduralMetric::Cyclomatic => "sql.procedural.cyclomatic_complexity",
procedural::ProceduralMetric::Cognitive => "sql.procedural.cognitive_complexity",
procedural::ProceduralMetric::EmbeddedQueryMax => {
"sql.structural_complexity.max_embedded_query"
Comment on lines +170 to +174

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Emit evidence for the raw procedural metrics

When contribution output is enabled, this mapping exposes evidence only for the three procedural composites; every newly published raw metric—including if_count, loop_count, block_count, return_count, and dynamic_sql_count—has no MetricEvidence under its own key. For example, a file can publish sql.procedural.if_count = 1 while providing no span or reason explaining that value, so these new metrics do not satisfy the repository's explainability requirement. Add evidence variants and collector mappings for each raw procedural increment.

AGENTS.md reference: AGENTS.md:L9-L11

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

✅ Addressed in 2f90fc5. Thanks @chatgpt-codex-connector!

Every raw sql.procedural.*_count is now evidence-backed under its own key: the machine routes increments through a raw_count helper pairing each with span + stable reason code (block_count, loop_count, if_count, case_statement_count, exception_handler_count, return_count, raise_throw_count, dynamic_sql_count), and routine_count emits one entry per unit with its (continuation-extended) span. The contributions.rs invariant test now asserts metric == Σ evidence for all eleven procedural keys across all four fixtures. One documented exemption: max_block_depth — a high-water mark has no meaningful sum decomposition.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Outcome

  • No additional code changes were needed. The trigger confirms that commit 2f90fc5 already addressed the raw procedural evidence finding.
  • Verified that each raw procedural count has a dedicated evidence variant, with max_block_depth explicitly exempted because a high-water mark is not sum-decomposable. crates/mehen-sql/src/procedural.rsL65-L86
  • Verified that raw_count updates the metric and emits span- and reason-coded evidence for block, loop, branch, case, exception, return, raise/throw, and dynamic-SQL increments. crates/mehen-sql/src/procedural.rsL583-L610
  • Verified that routine_count emits one contribution per routine using its continuation-extended source span. crates/mehen-sql/src/procedural.rsL1639-L1653
  • Verified that all raw evidence variants map to their corresponding published metric keys in the contribution collector. crates/mehen-sql/src/lib.rsL167-L190
  • Verified that the contribution invariant covers all eleven sum-decomposable procedural metrics across the PL/SQL, T-SQL, MySQL, and BigQuery fixtures. crates/mehen-sql/tests/contributions.rsL149-L197
  • No commit or pull request was created because there were no new tracked changes. The pre-existing untracked npm/mehen/node_modules/ directory was left untouched.

Testing

  • cargo test -p mehen-sql --test contributions procedural -- --nocapture — 2 passed, 0 failed.
  • git diff --check
  • rg -n "raw_count|block_count|loop_count|if_count|case_statement_count|exception_handler_count|return_count|raise_throw_count|dynamic_sql_count|routine_count|max_block_depth" crates/mehen-sql/src/procedural.rs crates/mehen-sql/src/lib.rs crates/mehen-sql/tests/contributions.rs
  • git status --short --branch — confirmed no tracked working-tree changes.

View task →

}
};
contribution_collector.record(metric, item.span, item.amount, item.reason);
}

// Per-statement spaces so top-offenders / nested reporting can attribute
// metrics to a statement's line range (research foundation §4.4).
Expand Down Expand Up @@ -286,6 +304,21 @@ fn attach_procedural_unit_spaces(
},
);
space.name = unit.name.clone();
// Per-routine procedural composites (Phase 3): the same keys as the
// file-level aggregates, scoped to this routine — the numbers
// `mehen top-offenders` shows next to a function name, and the
// complexity denominator CRAP will use.
space.metrics.insert(
"sql.procedural.cyclomatic_complexity",
unit.cyclomatic_complexity,
);
space.metrics.insert(
"sql.procedural.cognitive_complexity",
unit.cognitive_complexity,
);
space
.metrics
.insert("sql.structural_complexity", unit.embedded_query_structural);
while let Some((_, _, open_end)) = stack.last() {
if unit.start_byte >= *open_end {
close_one(&mut stack, &mut top_level);
Expand Down Expand Up @@ -519,6 +552,18 @@ pub const PUBLISHED_METRIC_KEYS: &[&str] = &[
"sql.predicate.max_boolean_depth",
"sql.predicate.not_count",
"sql.predicate.null_semantics_risk_count",
"sql.procedural.block_count",
"sql.procedural.case_statement_count",
"sql.procedural.cognitive_complexity",
"sql.procedural.cyclomatic_complexity",
"sql.procedural.dynamic_sql_count",
"sql.procedural.exception_handler_count",
"sql.procedural.if_count",
"sql.procedural.loop_count",
"sql.procedural.max_block_depth",
"sql.procedural.raise_throw_count",
"sql.procedural.return_count",
"sql.procedural.routine_count",
"sql.query_block.avg_select_items",
"sql.query_block.count",
"sql.query_block.max_depth",
Expand All @@ -540,6 +585,7 @@ pub const PUBLISHED_METRIC_KEYS: &[&str] = &[
"sql.statement.kind_entropy",
"sql.statement.unparsed_count",
"sql.structural_complexity",
"sql.structural_complexity.max_embedded_query",
"sql.subquery.correlated_count",
"sql.subquery.count",
"sql.subquery.exists_count",
Expand Down
80 changes: 53 additions & 27 deletions crates/mehen-sql/src/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,31 +15,6 @@ use crate::dialect::{DialectResolution, dialect_label};
use crate::facts::{SqlFileFacts, StatementKind};
use crate::loc::SqlLoc;

/// All distinct statement kinds, so `kind_count.<kind>` keys are emitted with
/// an explicit `0` when absent (grepability over silent omission).
const ALL_STATEMENT_KINDS: &[StatementKind] = &[
StatementKind::Select,
StatementKind::WithSelect,
StatementKind::Insert,
StatementKind::Update,
StatementKind::Delete,
StatementKind::Merge,
StatementKind::CreateView,
StatementKind::CreateTable,
StatementKind::CreateTableAsSelect,
StatementKind::CreateOther,
StatementKind::AlterTable,
StatementKind::Drop,
StatementKind::Truncate,
StatementKind::Grant,
StatementKind::Revoke,
StatementKind::TransactionControl,
StatementKind::Explain,
StatementKind::Procedural,
StatementKind::SetOperation,
StatementKind::Unknown,
];

const JOIN_KINDS: &[&str] = &[
"inner", "left", "right", "full", "cross", "natural", "lateral",
];
Expand All @@ -66,6 +41,7 @@ pub(crate) fn publish(
publish_expressions(facts, target);
publish_output(facts, target);
publish_objects(facts, target);
publish_procedural(facts, target);
publish_dialect(facts, loc, dialect, target);
publish_parser(facts, loc, target);
publish_halstead(facts, target);
Expand Down Expand Up @@ -97,8 +73,9 @@ fn publish_loc(loc: &SqlLoc, target: &mut MetricSet) {

fn publish_statements(facts: &SqlFileFacts, target: &mut MetricSet) {
set(target, "sql.statement.count", facts.statements.len());
// kind_count.<kind>
for kind in ALL_STATEMENT_KINDS {
// kind_count.<kind> — every kind gets an explicit `0` when absent
// (grepability over silent omission).
for kind in StatementKind::ALL {
let n = facts.statements.iter().filter(|s| s.kind == *kind).count();
set(
target,
Comment on lines +78 to 81

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Emit evidence for the anonymous-block statement count

When a file contains an anonymous procedural block, iterating over the expanded StatementKind::ALL publishes the new positive sql.statement.kind_count.anonymous_block value, but no contribution is ever recorded under that key. Contribution-enabled output therefore cannot explain the newly added classification and its evidence sum is zero; record the statement span and a stable reason when this counter is incremented.

AGENTS.md reference: AGENTS.md:L9-L11

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

✅ Addressed in 1d38867. Thanks @chatgpt-codex-connector!

sql.statement.kind_count.anonymous_block records one contribution per classified block (statement span, reason sql.statement.anonymous_block) from the per-statement loop, so the classification explains itself. Covered by the same new invariant test.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Outcome

  • No additional code changes were needed. The trigger confirms that commit 1d38867 already addressed the anonymous-block evidence finding.
  • Verified that each anonymous-block classification emits a statement-span contribution under sql.statement.kind_count.anonymous_block, using the stable reason code sql.statement.anonymous_block. crates/mehen-sql/src/facts.rsL2596-L2606
  • Verified that the contribution invariant test includes the anonymous-block kind counter and asserts that its published metric equals the sum of its evidence. crates/mehen-sql/tests/contributions.rsL378-L401
  • No new commit or pull request was created because there were no new tracked changes. The pre-existing untracked npm/mehen/node_modules/ directory was left untouched.

Testing

  • cargo test -q -p mehen-sql --test contributions object_and_kind_count_evidence_sums_to_the_metrics
  • git show --stat --oneline 1d38867
  • git diff --check
  • git diff --exit-code -- . ':(exclude)npm/mehen/node_modules/'
  • git status --short --branch (confirmed no tracked working-tree changes)

View task →

Expand Down Expand Up @@ -377,6 +354,55 @@ fn publish_objects(facts: &SqlFileFacts, target: &mut MetricSet) {
);
}

/// Procedural-SQL metrics (research foundation §6.17, Phase 3). Published
/// unconditionally — a purely declarative file reports explicit zeros, the
/// same contract as every other family.
fn publish_procedural(facts: &SqlFileFacts, target: &mut MetricSet) {
let p = &facts.procedural;
set(target, "sql.procedural.block_count", p.block_count);
set(target, "sql.procedural.routine_count", p.routine_count);
set(
target,
"sql.procedural.cyclomatic_complexity",
p.cyclomatic_complexity,
);
set(
target,
"sql.procedural.cognitive_complexity",
p.cognitive_complexity,
);
set(target, "sql.procedural.max_block_depth", p.max_block_depth);
set(target, "sql.procedural.loop_count", p.loop_count);
set(target, "sql.procedural.if_count", p.if_count);
set(
target,
"sql.procedural.case_statement_count",
p.case_statement_count,
);
set(
target,
"sql.procedural.exception_handler_count",
p.exception_handler_count,
);
set(target, "sql.procedural.return_count", p.return_count);
set(
target,
"sql.procedural.raise_throw_count",
p.raise_throw_count,
);
set(
target,
"sql.procedural.dynamic_sql_count",
p.dynamic_sql_count,
);
// §9.3: the worst embedded query inside any single routine.
set(
target,
"sql.structural_complexity.max_embedded_query",
p.max_embedded_query_structural,
Comment on lines +401 to +402

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Emit evidence for the embedded-query maximum

When contributions are enabled, a routine containing an embedded query publishes a nonzero sql.structural_complexity.max_embedded_query, but the contribution collector never records anything for this newly published key, so its evidence sum remains zero and users cannot determine which routine or constructs produced the maximum. Add spanned, reason-coded evidence for the winning routine and cover the sum invariant in tests/contributions.rs.

AGENTS.md reference: AGENTS.md:L11-L11

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

✅ Addressed in cb7dcff. Thanks @chatgpt-codex-connector!

ProceduralMetric::EmbeddedQueryMax emits one spanned contribution for the winning routine (reason sql.procedural.embedded_query, amount = the published maximum), keyed to sql.structural_complexity.max_embedded_query. Sum invariant covered by embedded_query_max_has_evidence_for_the_winning_routine in tests/contributions.rs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Summary

  • Confirmed the trigger is an acknowledgment of an already-completed fix in commit cb7dcff; no additional code changes were needed.
  • Verified ProceduralMetric::EmbeddedQueryMax maps to sql.structural_complexity.max_embedded_query and is recorded through the contribution collector with its span, amount, and reason. crates/mehen-sql/src/lib.rsL162-L172
  • Verified the regression test requires exactly one winning-routine contribution, checks reason code sql.procedural.embedded_query, and confirms its amount equals the published maximum. crates/mehen-sql/tests/contributions.rsL230-L250
  • No commit or new pull request was created because the requested change is already committed and there were no new tracked modifications. The pre-existing untracked npm/mehen/node_modules/ directory was left untouched.

Testing

  • cargo test -p mehen-sql --test contributions embedded_query_max_has_evidence_for_the_winning_routine
  • git show --stat --oneline cb7dcff -- crates/mehen-sql/src/lib.rs crates/mehen-sql/src/procedural.rs crates/mehen-sql/tests/contributions.rs
  • git status --short --branch (confirmed no tracked working-tree changes)

View task →

);
}

fn publish_dialect(
facts: &SqlFileFacts,
_loc: &SqlLoc,
Expand Down
Loading
Loading