Repository navigation
control_flow_ratio is the corpus's worst metric (33%): it divides a pinned numerator by structural_boundaries, an ungoverned 0-45 vocabulary tally #2770
Description
Same pass as #2769 (2026-09-05, engine 3b3568e0, corpus main 6183c4b). Ranked by consistency score, control_flow_ratio is the worst metric in the corpus — 33%, twenty-five points below the next one. It is currently ungated on the grounds that its denominator is unplanted (#2689 bucket B), which is a correct description of the state and not a resolution: it is still charted, still recorded, and dependency_density (which is gated) is derived from it.
The measurement
total_control_flow_ratio = round(branch_hits / max(branch_hits + linear_hits, 1), 3) # detector.py:1416linear_hits is equations["structural_boundaries"]. Over the 46 languages' identical 12-probe program, branch is pinned at 3–5 by the SPEC and structural_boundaries is not pinned at all:
| language | structural_boundaries |
language | structural_boundaries |
|
|---|---|---|---|---|
| solidity | 45 | shell | 16 | |
| ada | 40 | c | 14 | |
| perl | 37 | jcl | 11 | |
| embedded_python | 35 | haskell | 10 | |
| swift | 33 | livecode | 7 | |
| kotlin | 32 | yaml | 6 | |
| javascript / groovy | 31 | sqlite | 4 | |
| python / lua / tcl | 30–31 | scala | 3 | |
| typescript / php | 29–30 | dockerfile | 2 | |
| … | … | yacc | 1 | |
| assembly / matlab / ruby | 16 | m4 / makefile / scheme / markdown | 0 |
A fifteen-fold spread with the numerator held constant. The resulting ratio ranges 0.0 (markdown) to 0.585 (assembly) against a median of 0.094 — sixteen red cells, more than any other metric.
Why: structural_boundaries has no contract either
The rules are not measuring the same thing, and unlike api (#2730), state_mutation (#2765) or encapsulation (#2766) nobody has written down what they should measure. Reading them side by side:
- solidity/ada/perl/swift/kotlin (30–45) — every declaration keyword, every modifier, every visibility token. The rule behaves like a token-class tally over the language's whole vocabulary.
- shell (16) —
local|readonly|export|declare|typeset|return|exit|source|read|cd|pwd|ls|cp|mv|rm|mkdir|touch, i.e. a list of shell builtins and coreutils commands, plus the pipe operator. That is a command inventory, not a structural boundary. - assembly (16) —
mov|lea|push|pop|add|sub|inc|dec|mul|div|nop|…, the ISA's non-branch mnemonics. - m4 / makefile / scheme / markdown (0) — no rule contributions at all, so their ratio is
branch / branch= 1.0 wherever a branch exists.
Three different ideas ("a declaration", "a command", "a non-branch instruction") plus an absence, in a denominator that is supposed to normalise the numerator.
Two consequences worth separating
control_flow_ratiois not comparable across languages and should not be, until the denominator means one thing. Design (D.1): shape-descriptor comparability — the 160 cells decompose into five mechanisms, only 15 of which are a real design question #2689 established this; what it did not do is open the fix.dependency_densityinherits it and is gated.dependency_density = import / (coding_loc × control_flow_ratio)— so a gated metric divides by an ungoverned vocabulary tally. That is why assembly reads 0.063 (−88%) and embedded_python 1.35 (+154%) while theirimportcounts are 3 and 7. Whatever is decided about (1), (2) is a live defect in a metric the gate does read.
The three candidate shapes, in the order they should be considered
(a) Give structural_boundaries a contract and audit 46 languages — the #2730 treatment, third application. The sentence has to be chosen: "a statement-level boundary that is not a decision" is the reading control_flow_ratio needs (branch ÷ all statements = decision density), and it is the reading _slice_by_labels's Mode A consumers already assume. Under it, shell's ls/cp/mv/touch leave the rule, solidity's modifier tokens leave, and the four zero languages gain a real statement count. This is the largest piece of work and the only one that makes the metric mean something.
(b) Change control_flow_ratio's denominator to a structural count that is already language-neutral — coding_loc, or functions_found, or the satellite statement count. Decision density per line is directly comparable and needs no new rule contract. This is cheap and defensible on its own, and it is what the metric's name promises.
(c) Break the dependency_density derivation regardless of (a)/(b), since a coupling measure has no reason to read a control-flow ratio at all. import / coding_loc is the measure its name describes.
(b)+(c) can ship without (a) and would take the worst metric in the corpus from 33% to something readable in one PR; (a) is the honest fix and is the one that also repays #2689's five other consumers of structural_boundaries.
Corpus pairing
structural_boundaries is not planted — SPEC.md deliberately declined to plant it (D.1's withdrawn recommendation, "a per-language vocabulary tally, not a structural count"). Under shape (a) that decision should be revisited in the same PR: a rule with a contract is plantable, and the corpus can hold the denominator equal the way it already holds branch equal. Ledger: control-flow-ratio-denominator-is-a-vocabulary-tally narrows or retires depending on which shape lands.
Activity
squid-protocol commentedon Sep 6, 2026
Design pass, with the code read and the corpus measured. Recommendation: (c) first as its own PR — it is a live defect, not a comparability question — then (b) with
coding_loc, and (a) split off as its own issue rather than a blocker.(c) is now open as #2789. Findings below are the ones that changed the ordering.
(c) is worse than this issue describes, and it is a discontinuity
record_keeper.py'slogic_loc_denom = max(int(coding_loc * control_flow_ratio), 1). Measured per file on the rosetta corpus (engined79ff39e):control_flow_ratiois 0 for any file with no branches, so 4 of the 5 files in every one of the 46 languages divide by themax(..., 1)floor and the column reports the raw import count.assembly 0.063andembedded_python 1.35are the two ends of that collapse, not two ungoverned vocabulary tallies. Three failures stack: thebranch == 0discontinuity (up to 20× on one addedif, and within a language, so the ledger entry's "fine within one repository" reading does not cover it);int()truncating the product before the floor; andcfr == 1.0whereverstructural_boundariesnever fires, which silently makes the denominatorcoding_loc— the right answer by accident.The fix needed no new design.
analysis_lens.ENGINE_CONSTANTSalready says "every per-file density equation divides bymax(coding_loc, EVIDENCE_MASS_FLOOR)" (#2655);dependency_densitywas simply missed by it, six lines below the siblingfunc_internal_densitythat got the per-function analog in #2705.Also worth recording: the corpus's current 82.6% consistency on
dependency_densityis a floor artifact, not evidence of health — the denominator is the constant 1 for most cells. It goes to 95.6% under the fix.The second site this issue does not name
security_auditor.py:359carries the samecoding_loc * control_flow_ratioidiom:cfris bounded[0, 1]by construction, sologic_loc <= coding_localways andsafe_denomcould never selectlogic_loc— that half is dead and #2789 writes it as themax(coding_loc, 1)it always computed.logic_locsurvives aslog_logic_loc, an input to the pre-trained model, carrying the samebranch == 0discontinuity into a scored feature. #2789 leaves it alone and records the defect in place: redefining a trained input is a retrain, per the frozenfunc_internal_density: 0.0precedent 20 lines down. Someone should decide whether that retrain is worth scheduling.(b): take it, with
coding_loc, and don't wait for (a)Two arguments.
(a) converges on (b) anyway. The contract sentence proposed above — "a statement-level boundary that is not a decision" — makes
branch + structural_boundaries≈ the file's statement count. That is a regex-approximated, 46-language-audited reconstruction of a numberprism.py:318already computes exactly and language-aware:coding_loc, taken offfinal_codeafter the language's own comment stripping (not the crude#///fallback in_calculate_block_metrics). Spending the #2730 treatment to rebuildcoding_locbadly is the wrong trade for this metric.Measured, from
docs/bias_data.jsonthroughbias_report._row_stats' own ±25% band:branch + structural_boundaries(current)coding_locmax(coding_loc, 50)functions_foundavg_func_complexity, already its own column at 67.4%Worst-in-corpus to mid-pack in one line, no rule audit. Take the floored form for consistency with the #2655 doctrine; do not add a second
avg_func_complexity.Two implementation points not in the issue text:
detector.py:1447and per-functiondetector.py:6927— and they must move together, or thecontrol_flow_ratioandcf_ratiocolumns stop meaning the same thing. The per-function one wantsmax(loc, FUNC_EVIDENCE_MASS_FLOOR), the 12 that already exists.angle = 22.5 + (1.0 - control_flow_ratio) * 67.5(detector.py:6928). Todaycfris bounded[0, 1];branch / coding_locis not — a one-lineif/elsegives 2.0 — and the angle would go under its 22.5 floor. Needs an explicit clamp plus a test pinning the bound.Blast radius is
anglefor every function in every golden master, plustel_cfr, the archetype vector atsignal_processor.py:598, and the security feature row — which is why it wants to be its own PR. (#2789, by contrast, changed no golden master in either mode:dependency_densityis sqlite-only.)(a): keep it, unblock it
structural_boundariesstill deserves the contract on its own merits — aSIGNAL_SCHEMAslot, charted as "Sequential Logic Declarations" (analysis_lens.py:1270), one of the statistical auditor's 32 signal keys, inllm_recorder'sstructure_keys, and an input tolog_density_hit_structural_boundaries. shell'sls|cp|mv|rm|touchis a coreutils inventory and should go regardless. But once (b) lands,control_flow_rationo longer reads it, so the audit leaves the critical path and can be scheduled against #2669 alongside the other rule contracts.Under (b),
control_flow_ratioalso becomes plantable (branchis already pinned,coding_locis measured), socontrol-flow-ratio-denominator-is-a-vocabulary-tallyretires rather than narrows and the metric can leaveungated_metrics.Two caveats on the numbers above
docs/bias_data.jsonpredates the assembly/agc_assembly: branch counts unconditional transfers — 23 of 25 hits are call/ret/jmp, not decisions (#2545's shape) #2764 assembly re-bless — it still carriesbranch 24 / cfr 0.585for assembly, where a live scan now readsmain.asmat 0.214. The current-vs-candidate comparison is like-for-like so the ranking conclusion holds, but the exact percentages will shift on regeneration, and this issue's "0.585 (assembly)" headline is stale.control_flow_ratiobetween files WITHIN one repository, where the vocabulary is held constant" — is weaker than it reads.security_auditor._build_feature_frameone-hot encodes language but does no per-language normalization and no groupby, so in a polyglot repo the vocabularies are not held constant. Worth noting in the entry either way.fix(core-engine): dependency_density is coupling per line, not per co…
fix(core-engine): split dependency_links into raw capture surface vs …
fix(metrics): plumb meta["popularity"] so the risk formulas' network …