index · ← 04 Structure judgment — tree-scale entropy, eight axes · → 06 Graph liveness and dead-code verdicts
Everything in this section is computed in ce-core (Haskell), from the fact tables that arrive over the verdict.request wire. It is pure integer/Rational arithmetic — no floating point, no logarithms — so the same request always yields the same verdict.
Seven axes, indexed by a fixed code. Since proto 2.17.0 (the density migration, M9 batch 6) each axis pairs a non-negative violation mass v with its opportunity count n and charges the bounded density floor(scale · v/(v+n)) per-mille (Score.hs:118-147) — strictly monotone in v, never reaching the scale, scale-free across repository sizes, and 0 when n = 0 (no opportunity table — the honest-absence stance, Score.hs:151-156). The migration's cause is recorded in the wire ledger: under the old raw-mass fold two ordinary real repositories both measured 0/1000 (VERSIONING.md, 2.17.0 entry).
| code | axis | violation mass v |
opportunity n |
knob(s) and value |
|---|---|---|---|---|
| 0 | size | convex soft-zone penalty summed over metricCode = 0 rows, exact Rational (Score.hs:179-185) |
files (metricCode = 0 rows) |
see soft zone below |
| 1 | complexity | count of metricCode = 1 rows with v > cocCeil (Score.hs:189-195) |
functions (metricCode = 1 rows) |
cocCeil = 15 (Cost.hs:193-194) |
| 2 | clone | distinct files touched by sim rows with kind <= 1 — kind 0 the T1/T2 blocks, kind 1 the T3 near-miss pairs, live since plan v2.30 step 5b-9 — and n * cloneDen >= d * cloneNum (Score.hs:208-210, touched at Score.hs:216-217) |
code files: the pos rows outside the doc set (Score.hs:151-156) | tsedNum/tsedDen = 85/100 (Clone/Cost.hs:22, Clone/Cost.hs:25) |
| 3 | docdup | distinct files touched by sim rows with kind == 2 and n * dupDen >= d * dupNum (Score.hs:231-238) |
documentation files: docFiles — since plan v2.30 step 5b-8 a plain-text file among them holds no pos row and is a docdup opportunity and no node (Score.hs:148) |
jaccardNum/jaccardDen = 80/100 (Docdup/Cost.hs:31-32, Docdup/Cost.hs:34-35) |
| 4 | deadcode | count of pos rows with reachIn == 0 and indeg <= deadIndegCeil (Score.hs:219-220) |
graph file nodes (pos rows) | deadIndegCeil = 0 (Cost.hs:200-201) |
| 5 | churn | count of churn rows with rw + ap > 0 and rw * rewriteDen >= (rw + ap) * rewriteNum (Score.hs:222-224) |
churned entities (churn rows) | rewriteNum/rewriteDen = 50/100 (Cost.hs:102-103, Cost.hs:105-106) |
| 6 | graph_cycle | count of code-file pos rows with sccSize >= cycleFloor; docFiles indices are excluded (Score.hs:212-219) |
graph file nodes minus documented files | sccFloor = 2 shipped — [graph] scc_floor overrides both faces at once as threshold code 7 cycleFloor (6.4.0), and at floor 1 a singleton counts exactly when the cycleSelfLoops table the graph reply projects lists it (Score.hs:237) (Graph/Cost.hs:104) |
Where the complexity values come from. A metricCode = 1 value is the function's cognitive complexity as scan/1 derived it in the core from the measuring side's structural events — the SonarSource whitepaper v1.7 rules (Complexity.hs:1-24) plus the recursion increment (Cycles.hs:1-20). The derivation is held to a second spelling that reads structure trees instead of events — a fold with the nesting level as an explicit argument, and the cycle set from a closure of the call relation — and the core must echo its numbers on 1,200 seeded programs in three table families, the whitepaper register's 38 lines and 300 call graphs (ReferenceScan.hs:1-27, ScanReferenceProps.hs:1-11).
Axes 2 and 3 since proto 7.0.0 (plan v2.29 step 8, O22 / O46). Their mass is files, not pairs — a pair is one verified fact, and one file duplicated into three others charged six pairs against a universe of files — and each reads its own opportunity set: the clone axis the code files, the docdup axis the documentation files alone, so a repository with no documentation has no docdup opportunity and charges nothing (Score.hs:197-207). Before 7.0.0 both axes counted pairs over every node, and ce check sent no kind = 2 rows at all, so the docdup axis was dead in the product; the rows now ride from the same snapshot the clone rows do (mod.rs:97-101). Scores are not comparable across the change (CHANGELOG, v1.6.0 → v1.7.0). Since plan v2.30 step 5b-9 the clone axis also reads the T3 family's verified pairs, kind 1 rows off the same snapshot (mod.rs:106, mod.rs:323-340) — until then no live road sent kind 1, and a tree of near-miss pairs without a block charged the axis nothing (booklet 02, § T3 in the gate and the verdict cache); scores are again not comparable across that change (CHANGELOG, 1.8.0).
All ratio thresholds are cross-multiplied rather than divided, so no rounding enters the predicates (Score.hs:210, Score.hs:213, Score.hs:224). Note the axis-2 and axis-3 thresholds are not owned by the verdict family: they are re-exported from the clone and docdup cost modules so that one authority defines "is a clone" (Score.hs:23-25, Cost.hs:7-10).
Weights. Each axis carries an effective weight: the wire's weights table supplies [axisCode, w] rows, and the first matching row wins; an unlisted axis takes defaultWeight (Score.hs:253-256), which is 1 (Cost.hs:242-243) — equal weights are the opening stance. The same lookup that folds the score also builds the echoed table 0..6 returned in the reply, so the echo cannot diverge from the computation (Score.hs:259-260, Verdict.hs:122-125).
The fold. With p_i the axis charges (per-mille, bounded) and w_i the effective weights (Score.hs:270-277):
raw = sum_i (w_i * p_i * violCost)
wTotal = sum_i w_i -- derived, never a literal
score = max 0 (scoreScale - raw `div` (violCostNeutral * wTotal))
violCost = 10 (Cost.hs:212-213), violCostNeutral = 10 (Cost.hs:236-237) and scoreScale = 1000 (Cost.hs:250-251) — i.e. the score is an integer per-mille value opening at 1000, polarity higher-is-better (DEVELOPMENT_PLAN.md:71). At the neutral default the score is exactly the weighted mean of the bounded axis charges, so the structural max 0 is unreachable; viol_cost remains a live ce.toml dial — a repo declaring it above neutral asks for harsher scores and may saturate by that explicit choice (Score.hs:270-277). div is Haskell floor division. wTotal is summed from the effective weights rather than declared, so a weight can never be silently dropped from the divisor; validation refuses an all-zero weight table, making the divisor non-zero — that refusal lives in the boundary contract CE.Verdict.Table.weightsOffence (Table.hs:98-103), asserted there by the source comment.
An over-cap request never gets a partial judgment: it returns a fully-shaped degraded reply with score = 0, empty axes, and fail = true with reason verdict_too_large (Verdict.hs:201-246). Caps are verdictNodeCap = 131072 nodes and verdictRowCap = 524288 rows (Cost.hs:259-263), the row count summing every fact and knob table plus the baseline's rows (Verdict.hs:54-81).
Axis 0 is the one axis whose mass is not a count. For a file of x LOC, with soft line S and hard line H (Soft.hs:59-65):
p(x) = 0 if x <= S
= pMax if H <= S -- degenerate fallback
= pMax * ((x - S) / (H - S))^2 if S < x <= H
= pMax * (1 + 2 * (x - H)/(H - S)) if x > H -- C¹ linear arm
The curve is exact Rational; past H it continues linearly at exactly the slope the quadratic reached at the wall (2·pMax/(H−S)) — monotone, no kink, still charging every added line, but never quadratic outside the contracted (S,H] domain (Soft.hs:44-58, size-advisory.md §A). The quadratic extrapolation this replaced is what saturated both field-test repositories at 0/1000 (proto 2.17.0 ledger). Denying at H is Rust's job (scan fail tier, guard budget); here H only scales the curve (Cost.hs:151-154). The degenerate H <= S branch reproduces the pre-v0.6 binary behaviour instead of dividing by zero or flipping the curve's sign (Soft.hs:61-62).
Constants: sizeHard = 750 (Cost.hs:155-156), sizePMax = 10 (Cost.hs:158-166) — one file at the hard line weighs like ten of any other axis's violations, which is what lets the size mass share the counting axes' odds scale in the density map. The per-file penalties are summed as Rational across all metricCode = 0 rows (Score.hs:179-185) and the axis floors exactly once, inside charge (Score.hs:148-153), so the wire's axes rows stay Integer.
Where S comes from. S is relative to the repository, not a constant. Over the multiset of judged-language LOC values (positives only), with m the exact median and r the multiplicative MAD (Soft.hs:33-42):
m = median(x)
r = median( max(x/m, m/x) ) -- >= 1 by construction
S = clamp(floor(m * r^k), [softMin, softMax])
This is the identity S = clamp(median + k·MAD, ...) in log-LOC space, re-expressed multiplicatively so no logarithm is ever taken; all order statistics are over Rational (Soft.hs:1-8, Soft.hs:19-25). k = softLineK = 2, calibrated over self + requests + ripgrep, where it put S near the historical 300 on each — an observation on three trees, never an invariant. S travels with the distribution it is derived from, and this repository's own has since left that neighbourhood; what bounds it is the clamp, and where the live value lives is the committed baseline (Cost.hs:168-173). The clamp fence is [softMin, softMax] = [200, 500], declared structural rather than a knob (Cost.hs:182-189). floor is the conservative direction: a lower S opens the graded zone earlier (Soft.hs:27-32). An empty or all-empty LOC set yields Nothing — absence, never a fabricated line (Soft.hs:34-35).
S is derived only at establish (no baseline present) and then frozen into the new baseline; every later run judges with the committed S, and a pre-v0.6 baseline carrying no softLine falls back to the sizeCeil knob 300 (Verdict.hs:140-147, Score.hs:187, Cost.hs:144-149). Because only the establish path reaches the derivation, re-anchoring the soft line requires CE_ACCEPT_BASELINE by construction (Faces.hs:53-56). Since plan v2.18 step #14 the CLI reaches that path only under the act: a missing ce-baseline.json refuses by name instead of establishing, a present file that is not a baseline document is an error before any measurement, and ce baseline persists at the project root alone — a scope below it refuses before the core is spawned (main_score.rs:125-139, main_score.rs:186-204, baseline.rs:139-151, baseline.rs:223-235).
Per-class lines (proto 3.1.0, plan v2.13 ①). A continuous row may carry a fourth column, the file's path class — the 1-based index of the first [[rules.class]] whose globs match it, 0 for none — and the request may carry a classKnobs table [classId, code, value] whose first three codes are the ceilings' own 0 / 1 / 2 (sizeCeil / cocCeil / sizeHard) under a class, joined at 5.1.0 by classTolCode = 3 — the class's own ADR-006 ratchet allowance — and at 6.4.0 by classCocTolCode = 4, its cognitive-complexity sibling, which where declared replaces code 3 for metric 1 alone; those two are the class knobs whose value 0 is meaningful (Cost.hs:121-128, Cost.hs:130-142). The rows fold into one Map per judgment (Score.hs:58-61, built once in Verdict.hs:135-136) and a row's class is its fourth column or 0 (Score.hs:65-68); sizeMass measures a classed row against its class's own opening edge and hard line where declared, falling back to the global S and H (Score.hs:179-187), and cocOver likewise against the class's ceiling (Score.hs:189-195). The charge law is untouched — only the two lines a row is measured against move — so an unclassed repository judges byte-for-byte as before. The ratchet reads the class off the current row and spends it on the allowance alone — a declared classTol replaces both global legs (Ratchet.hs:128-136, bound at Verdict.hs:152-158) — while what it writes back is still the three-column prefix (Ratchet.hs:148): a class is a charging parameter, never a baseline fact, and the baseline stays three columns. At the boundary a table mixing three- and four-column rows refuses (Table.hs:48-52), a class at or past classCap = 64 refuses (Cost.hs:85-86, Rows.hs:86-98), and the knob rows obey the ceilings grammar one class dimension wider — class 0 has no override channel, (classId, code) strictly ascending, the code bounded at classKnobMaxCode = 4 and the value floor judged per code, 0 for the two allowances and 1 for the ceilings (Table.hs:61-79); that ordering is a validation fact, never a judgment fact (ClassProps pins the permutation). Names and globs never cross the wire; the index does.
ADR-006 defines per-file/per-function ceilings on continuous metrics (file LOC, function CoC): the ceiling is the baseline value; exceeding it fails, and coming in under it tightens the ceiling automatically. A single edit is allowed +2% or +10 lines, whichever is larger, and consumed tolerance is reported in the ce check ratchet line (DEVELOPMENT_PLAN.md:205-212).
As built (Ratchet.hs:73-75) the law has two arms — the row's class allowance first, the global legs only when the class declares none:
tolerated(Just t, c) = c + t -- the class's own allowance (5.1.0)
tolerated(Nothing, c) = max (c * tolNum `div` tolDen) (c + tolAbs) -- the global legs
with tolNum/tolDen = 102/100 and tolAbs = 10 (Cost.hs:112-119). On the global arm, integer div truncates down — the conservative side, the "ties don't open" stance — and the two legs cross at ceiling 500, with one property assertion pinned on each side (Ratchet.hs:61-64, Cost.hs:108-111). A class that declares ratchet_tolerance (classKnobs code 3, or cognitive_ratchet_tolerance — code 4, 6.4.0 — which answers the fn-CoC rows alone where declared) replaces both legs with that absolute allowance — absolute rather than proportional because the classes wanting the knob are vendored trees and frozen fixtures, and a percentage of a large file is exactly the unearned growth the plan objected to; t = 0 therefore means any growth at all is over, and the global max(+2%, +10) is never consulted (Ratchet.hs:66-75, K14 at ClassProps.hs:216-222).
For each current row [u, metricCode, v] matched against the baseline ceiling bv for the same (entity, metric) key (Ratchet.hs:77-124):
v > tolerated(bv)→ over, emitted as[u, c, v, allowed](Ratchet.hs:103-108). Note the comparison is strict, sov == tolerated(bv)is tolerated, not over.bv < v <= tolerated(bv)→ tolerance drawn, emitted as[u, c, v - bv]for the Stop summary (Ratchet.hs:108-113, Ratchet.hs:49-51).- new ceiling =
min(v, bv)— auto-tighten; an entity the baseline never saw adopts its current value as its ceiling (bootstrap, not a violation) (Ratchet.hs:123, Ratchet.hs:77-81).
Tolerance is drawn per run against the baseline ceiling, and the new ceiling is min(v, bv), so a drawn edit never raises the committed ceiling: the allowance does not accumulate across runs.
For discrete violations (clone instances, deadcode symbols) the baseline is a set of violation fingerprints; a new member fails, a removed member shrinks the baseline (DEVELOPMENT_PLAN.md:211-212). The implementation is plain set difference both ways over Data.Set, with the results returned in ascending order (Ratchet.hs:114-115, Ratchet.hs:141-142):
added = current \ baseline -- non-empty => fail
removed = baseline \ current -- informational; drives the shrink
newDisc = current -- verbatim, not intersected
newDisc is the current set verbatim (Ratchet.hs:124); the "only shrink" invariant (new ⊆ old) is enforced by the caller's acceptance gate, not faked inside this function (Ratchet.hs:81-84).
The clone members are T1/T2 blocks. A member is a block's two sides attributed to their owning units and hashed (score/mod.rs:398-413); the T3 pairs the same run judges (plan v2.30 step 5b-9) are not members — a near-miss pair is a verdict over two whole trees that moves with any edit to either unit, so seating it would fail every committed baseline's fence at the upgrade and again at every reshaping edit. Those pairs charge axis 2 and ride the candidates table instead.
Member identity (7.0.0). Since proto 7.0.0 a continuous row's member identity is the §7.2 container anchor — the keys of the units enclosing it, outermost first, hashed, then its order under that chain — rather than nth, the occurrence order by start line: the anchor is "the CONTAINER CHAIN instead" (fourclass/anchor.rs:1-22). Under nth, deleting an earlier same-key sibling shifted every survivor's index and the ratchet read one removal plus one addition for a clone nobody touched; under the chain, impl A { fn add } and impl B { fn add } differ by their impl, and a deletion elsewhere in the file moves no identity. The baseline file therefore stamps SCHEMA_ID = "ce.baseline/2" (baseline.rs:27-33), and a 6.x /1 file is "refused by name" so that the identity change is a "visible one-time re-establish", never a mass removal plus addition (baseline.rs:10-15). The index's own nth column is untouched — the persisted identity every face prints — while the churn ledger, the join and the seam pricer key on this same anchor since plan v2.30 step 5b item 29 (anchor.rs:1-10).
Establish. With no baseline, ratchet returns all five report lists empty — rOver, rDrawn, rAdded, rRemoved, rDropped — and promotes the current facts wholesale to the new baseline — nothing can fail on the establishing run (Ratchet.hs:92, Ratchet.hs:3-6). The request carries null there only when the CLI pinned nothing: ce check and the routine ce baseline read the committed file once and send it verbatim, CE_ACCEPT_BASELINE=1 sends null on purpose, and ce trend sends a pinned identity baseline — every ceiling the point's own measurement, its own member set, the run's digest — so its verdict is exact and a failing one is a tripwire that refuses the point rather than caching it (score/mod.rs:189-193, pinned.rs:21-40, trend/mod.rs:147-158).
ADR-006 makes the ratchet the primary gate for a repository that has a baseline and --fail-under a floor underneath it; either alone fails (DEVELOPMENT_PLAN.md:217-218). As built, the fail bit is the disjunction of six named conditions (Faces.hs:23-31):
| name | holds when |
|---|---|
ratchet_over |
the over list is non-empty |
discrete_added |
the added set is non-empty |
floor |
score < reqFloor — the --fail-under value, when supplied (Verdict.hs:160) |
dedup_budget |
the request carried a [blocks, budget] pair and the judged count exceeds budget — since proto 2.19.0 that count is the core's own derivation from the shipped distinct rows, falling back to the client's blocks only when they are absent (Verdict.hs:179-188) |
knobs_digest |
the digest the baseline's ceilings were established under disagrees with the one this run declares — the 5.1.0 rulepack fence, widened at 6.0.0 to the whole parsed config and canonical since O39: the digest is the effective knob set, the values that differ from the shipped defaults (canonical.rs:76-104), so comments, key order, a knob spelled at its default and an undeclared option leave it alone. Plain Maybe inequality and total: both absent agrees; a changed rulepack, one declared against a pre-fence baseline, and one removed that the baseline recorded all fail by this same name, so a human names the new configuration with CE_ACCEPT_FENCE=1 ce baseline — the narrow act, which re-pins the SAME ceilings (min with the old) under the declared knobs and refuses when any other condition held — or the new floor with CE_ACCEPT_BASELINE=1 (main_score.rs:48, main_score.rs:174-178, Verdict.hs:162-169, baseline field at Ratchet.hs:33) |
rows_dropped |
the request carried the provenance table present (6.4.0) — every file entity on disk under the scope that owns no continuous row this run, walked with NO ignore file and no exclude because those are the roads being watched — and a committed row's file is in it while this run measured no row for that (entity, code): an exclusion hid a file that still exists, the one edit that could retire a ceiling in silence. A deleted file's rows are simply gone and hold nothing. The rows ride back as ratchet.dropped, and only the narrow act owns them, writing the baseline WITHOUT them (Ratchet.hs:116-122, Faces.hs:30, the table at provenance.rs:58-71) |
fail = any of them, and the reply also carries the list of the names that held, so a consumer attributes the failure by name rather than by reconstructing the conjunction (Faces.hs:39-51, carried in the reply at Verdict.hs:107). The console prints that same list verbatim after FAIL, in the core's order, never sorted or filtered (Score/Lines.hs:29-31, Check.hs:18). The removed and toleranceDrawn lists are reported but never contribute to the fail bit; dropped (6.4.0) is reported beside them and contributes by name. Note also that dedup_budget is only judged when the pair is present — ce dedup --check sends it, the ce check path does not (Faces.hs:17-21).
- The knob digest is a function of the parsed config AND the shipped defaults, never of the file: the serialized config is pruned against the serialized effective default —
nullleaves, leaves equal to the default's, empty objects go; arrays compare whole with the same rules inside their objects, a class's name being a label outside the tree (canonical.rs:118-141) — and the core's own constants stand in for every absent score / trend / graph knob through a Rust mirror the wire gate pins against the core's echo (knobs.rs:41-68). One fixed declaration's literal is frozen in the contract suite, so a serialization change cannot move downstream baselines unannounced. - Every knob above travels into the pure functions as a parameter; production binds them to the
Costconstants exactly once, atscoreBound(Score.hs:89-108) andratchetBound(Ratchet.hs:42-43). That is what lets the perturbation batteries move one constant and watch the census move without touching production code (Cost.hs:1-6). - All arithmetic is
IntegerorRational; the bounded-arithmetic ban is a recorded 2026-08-12 decision (Cost.hs:12-14). - The baseline crosses the wire verbatim from
ce-baseline.jsonand is parsed exactly once, inrespond(Verdict.hs:9-11, Verdict.hs:52); Rust never interprets it.