CodeEraser 1.4.0
CodeEraser 1.4.0
Cognitive complexity now charges the recursion increment. Every function inside
a recursion cycle costs one more point than it did in 1.3.x, so scores from
this release are not comparable with scores from 1.3.x on any repository that
recurses.
The rule, and why it was missing
The S3776 whitepaper (v1.7, p.8 and Appendix B1) says the increment applies to
each method in a recursion cycle, whether direct or indirect. This
implementation had carried that as a known gap since M1, with the head note of
cognitive.rs saying so.
SonarSource's own analyzers do not implement it either — java, python and
javascript were read at source on 2026-08-31 and recurs matches nothing in any
of the three. So this release takes the side of the specification and records the
consequence: on a function inside a recursion cycle, CodeEraser now scores one
point higher than a real SonarQube would. That is not a defect and will not
be "fixed" on some corpus later; it is a difference of reading between two
implementations of one spec, and it lives in the attribution column of
contracts/fixtures/crosscheck/DIVERGENCES.md.
Where the work lives
ADR-008's fourth instalment splits the two halves along the language line this
project has held from the start.
- Call arcs are a measurement-side fact — lexical, per parse unit, in Rust
(cli/src/scan/calls.rs). - The cycle is a verdict — Haskell,
core/app/CE/Scan/Cycles.hs, computing
its own SCCs. It deliberately does not reuseCE.Graph.Cycles, whose first
line says cycles there are REPORTED and never judged (RG9); the two faces must
not be crossed. What it does copy, verbatim, is that module's reading of a
single vertex — so direct recursion needs no special case at all: it is a cycle
of length one.
The policy constant +1 appears in exactly one place in the repository.
Scope is within a file. Cross-file cycles are a named non-goal, and the
position is kept as an assertion rather than a sentence, so it cannot rot.
Wire: scan/1 6.5.0, additive
Request gains callEdges = [[from, to], …] — both ends are row indices into the
existing rows table, strictly ascending, so no name and no path crosses the
wire. Reply gains cocBumped = [[rowIndex, effectiveValue]].
It sends the value, not the increment. The measurement side then renders the
number the core judged and never re-derives a cycle or an increment of its own;
cli/src/scan/coc.rs does index arithmetic and one monotonicity check. All four
readers — findings, the pinned evaluate mirror, the ADR-006 ratchet's
complexity column, and the JSON report — therefore read the same integer, because
scan::settle is the only road it travels.
Chunking gained a testable invariant along the way: a file's rows may not span
two chunks. Arcs are stated as row indices, so a boundary inside a file cuts an
arc in half, and losing one arc is losing one cycle. Chunking now splits on files;
a single file too large to fit is refused by name rather than split.
What it measured here
27 functions in this repository each gained exactly one point — walk, go,
loop, visit, flatten, pairs, the recursive shapes, found by running the
old and the new binary over the same tree and comparing function by function.
ce check still reads 953, and the complexity axis still contributes 0. That
is this repository's arithmetic, not a general result: a repository whose
recursive functions sit near the ceiling will score lower under 1.4.0 than it did
under 1.3.x.
Two real defects surfaced from measuring rather than from reading:
impl Drop { fn drop(&mut self) { drop(...) } }was mis-charged. The bare
dropinside is the prelude's free function, and a bare name cannot reach a
method.LangSpec::call_member_scopesnow keeps bare names out of member
scope, probed syntax-first for all five languages.- A bare call whose name is bound by an import inside the same unit was
mis-charged the same way — found while recalibrating against the four
crosscheck corpora, onignore'sfn symlink { use std::os::unix::fs::symlink; … }.LangSpec::call_import_kindsnow refuses those.
Both carry a paired probe: remove the rule and the exact mis-charge returns;
change only the name being imported and the recursion is still charged.
After those fixes the four crosscheck corpora (go 52, python 118, rust 319,
typescript 25 units) moved not one unit, and every published divergence row
against gocyclo, lizard, RCA and gocognit stands unchanged.
How it is anchored
None of the whitepaper's six worked examples recurses, so there is no scored
example to copy. The anchor is derived instead, and derived so that both sides
are checkable: take p.10's sumOfPrimes, whose margin score of 7 is already
pinned by an existing test against that same page, and add one self-call whose
result is discarded — a call is not itself a structural increment. The same
source must therefore still read 7 before the cycle settles and 8 after. The
difference is the rule under test.
core/test/ScanCyclesProps.hs carries seven legs, the main one comparing all
65536 four-vertex graphs against an independent oracle that never touches
Data.Graph.
Also in this release
Both READMEs now open with the product doing its visible thing rather than with a
diagram of it: a terminal card in which an agent asks to create a file and ce
answers with the indexed region that content already duplicates, and the ordering
that would pass. The card is generated, not photographed — the same demo run that
produces the console block further down the page is also drawn to SVG, so there
is no second artefact that could drift from the first, and node demo/run.js --check compares both cards byte for byte. The architecture diagram moves down to
the tech-stack section, which is the section about the machine.
Score comparability
Not comparable with 1.3.x. Cognitive complexity is a judged axis and its
value changed for every function inside a recursion cycle. Repositories with no
recursion are unaffected, but there is no way to tell from a score alone which
case a repository is in, so treat 1.3.x numbers as a different series.
No default guard tier changed, so the false-positive figures stand where 1.2.0
left them. The index schema stays at 15 and no other request family changed
shape; the shared wire version goes to 6.5.0 for scan/1's two additive
keys. A pre-6.5.0 core is refused by name when its reply arrives without
cocBumped — it would otherwise have judged the call edges silently.
Latency
v1.4.0 changes cli/src and core/app, so it joins the benchmark series. The
series is replayed WHOLE, in one sitting, from each tag's own tree and each tag's
own binaries — that replay runs against this tag and lands on
the dashboard and in
docs/BENCH.md
shortly after publication. Until it does, those surfaces say so in generated text
rather than quoting a stale row under a new version number.
Unsigned, on purpose
Code signing and notarisation stay out of scope (ADR-007/R1). The SHA-256 chain
is the permanent trust anchor: every asset is pinned in plugin/bin/manifest.env
at the release commit, the tag job verifies each pin before publishing, and
SHA256SUMS covers the set for offline checking. ce update reads the latest tag
and that tag's committed pins, so it needs nothing published beyond this release.