The analysis track lets the reader ask, over the same index and judged in the same core.
The families on the how page each judge one shape of entropy the tree already carries: a clone, a dead file, a duplicated paragraph, a unit that plays another's part. The analysis track lets the reader ask — over the same index, judged in the same core, under the same commitments: every answer is arithmetic over measured facts, there is no model and no floating point between the evidence and the verdict, and every constant is cited to the source line that implements it.
Its families continue the numbering from the fifteenth and get a booklet each; the short form is below, the full derivation lives in docs/reference/methodology.md, which every heading here links into.
A judgment family measures a fact and compares it with a threshold that is written down. An analysis family takes a program — a question, or a file of rules — and answers it over the facts the index already holds, so the threshold is the reader's own sentence. The division of labour does not move: Rust measures and labels, the Haskell core judges, and nothing but integers crosses the wire between them.
Answers a question in CE Datalog over the facts the index already holds — files, directories,
references, units, complexity, judged clone and docdup pairs, mentions, path classes — with every
answer's derivation, and judges a rules file's assertions as a gate: a violation is a witness row
with its proof, and ce rules exits 1 on the first.
clause := atom ':-' body '.' | '?-' body '.' | 'assert' atom ':-' body '.'
lit := atom | 'not' atom | expr op expr | Var '=' expr | Var '=' agg(Vars : body)
prelude: entry(N) :- role(N, _). reach(M) :- reach(N), ref(N, M, K, _), K != asset, K != refdef.
dead(F) :- file(F), not reach(F). depends · same_dir · dir_ref
strata : a head sits above every negated or aggregated body predicate; a negative edge inside an SCC is a program error
eval : semi-naive per stratum over lazy bitmask indexes; the first derivation of a tuple is its provenance
The measuring side lexes the text into a [kind, value] token stream, assembles only the
fact tables the program names from its own index, and sends both; names go up as fnv1a64 hashes and
request-local ids, never as text, and come back the same way to be labelled through the request's own
tables. The core parses (a hand-written recursive descent), checks arity, sorts (a union-find over
variables, predicate positions and constant sorts), safety (range restriction: a variable is read only
after a positive atom or a binding bound it) and stratification, evaluates one stratum at a time, and
answers every goal's tuples, every assertion's violations, the proof rows and the program's errors —
each error at a token the measuring side maps back to line:column. A proof is a whole tree
or nothing: under the node budget a tree that would not fit is left out and counted, never cut. Tokens
and fact rows are priced before judging and answer a complete degraded reply; derived tuples abort the
evaluation whole; a core without the family is a named non-judgment in the document. The built-in
dead(F) is ce deadcode's file-tier verdict restated as a rule, and this repository
judges itself with nine assertions in ce.rules as CI's seventh dogfood leg.
Finds the dead code inside one function: statements no path reaches, stores no path reads,
locals nothing reads, and parameters nothing reads as advice. ce flow lists them, the guard
counts the ones a write brings in, and the Stop line records them without ever blocking.
tables : stmts [u, seq, parent, kind, flags, aux] · vars [u, v, declSeq, flags] · uses [u, seq, v, mode] cfg : structured kinds unfolded; a try body's every statement may reach each catch; one finally hub per try kind 0 : unreachable = no path from the entry, one finding per maximal run of seqs kind 1 : dead_store = a write no path reads before the next write or the exit (backward liveness) kind 2 : unused_local = no read · kind 3 : unused_param = no read (advice, never judged) skip : a unit with a dynamic statement is not judged in part, and counted
The measuring side lowers every unit of ten languages (Python, TypeScript and TSX, Rust, Go, C,
C++, Java, Lua, R) into four integer tables by a FlowSpec table per language; no name, path or
source text crosses. The core builds the control-flow graph, walks reachability from the entry and
backward liveness to a fixpoint, and answers [unit, kind, seq, var, seqEnd] rows the
measuring side labels again. Every approximation only adds paths — a finally is one hub whose
successors are the union of the pending targets, expression-position exits are not lowered — so a
finding can be missed, never invented. A finding is judged only in a language whose precision exam
passed (blind review, no false positive in any kind the gate reads). All ten are in: the docs
regenerated on the fixed lowering (a second exam generation for C++, R, Rust and TypeScript) read
judged for Python, TypeScript, TSX, Rust, Go, C, C++, Java, Lua and R with no false positive in the
gated kinds; an unused parameter is advice
everywhere. The
guard class [flow] tier ships at observe, counting novel findings as a multiset
difference on (unit, kind, variable) so a moved finding is not the write's doing.
Reads how each clone group would fold into one function: the places its members differ
become parameters, and the answer is the parameter count, the member worth keeping, the lines the merge
would save, and whether it is feasible — with the reason when it is not. Advice: no gate reads it,
ce merge exits 0 with a document.
groups : T1/T2 dedup families (whole units when one unit fills ≥ 9/10 of each member, else fragments trimmed to the members' common shape) · T3 judged pairs trees : clone/1's postorder (lab, lld), extras (comments) left out, + leaf = fnv1a64 of a leaf's bytes (0 inside) + slot 0 stmt · 1 expr · 2 type · 3 name · 4 other align : T1/T2 isomorphic, holes = differing leaves; T3 the tree-edit mapping narrowed top-down, relabels and unequal gaps are holes params : one per distinct value vector; feasible iff every hole is an expression or a name, params ≤ 6 and savings > 0 reason : 3 a gap across statements, 1 position (a statement or other), 2 a type position, 4 more than six parameters, 5 no line saved kept : the member whose file has the greatest in-degree; savings = Σ lines − (skeleton lines + members × 1), a fragment's lines its kept run's
The measuring side gathers the groups, builds each member's tree in clone/1's
encoding with two more integer columns — a leaf's source hash and its position class, read off one
table per language that reads the flow tables — and labels the answer back with every member's text at
every parameter, a gap's whole forest included. Its trees leave comments out, and a fragment family
keeps only the run of top nodes every member holds, so a boundary line cut mid-statement does not make
two shapes, and the merge is priced by the kept run's lines. The core lines the members up, finds the holes, numbers the parameters and prices the
merge; no name, path or source text crosses the wire. A group the core would refuse — Markdown and
HTML, with no function to fold into, a tree not built, members not one shape — is counted by why and
never sent.
A T3 skeleton's line count is an honest integer estimate, never a measured one. The suggestion sets of
this repository at one pinned commit and of four pinned external corpora are frozen, and a hundred of
them are drawn for a blind audit of the feasibility.
Reads how the directories depend on each other: which sit at the bottom, which arcs to cut
to break their cycles most cheaply, which files would sit better beside the files they talk to, each
directory's fan-in, fan-out and instability, and what a change to a file reaches. Advice: no gate reads
it, ce arch exits 0 with a document.
arcs : dir(F) → dir(G) per file reference, dir(F) → D per package reference; inside one directory uncounted
cuts : per SCC, a subset programme when it has ≤ 14 directories (exact 1), else Eades–Lin–Smyth + redundancy pass (exact 0)
layers : level(d) = 0 when nothing leaves d, else 1 + max level of what d points at, the cut arcs removed
cluster: deterministic Louvain, gains in integers, two levels; misplaced = its cluster holds strictly more files elsewhere
impact : breadth-first over the references read backwards, the focus at depth 0
metrics: fanIn, fanOut, instability = ⌊1000 · out ÷ (in + out)⌋, −1 when nothing touches the directory
The measuring side sends five integer tables — the measured files with their directory
and line count, the directory tree, the file-to-file references, the file-to-package references at
the package's directory, and the --impact files — and labels the answer back: every cut
arc carries the file references behind it. The core folds the references onto the directory graph,
finds its feedback arc set one strongly connected component at a time, layers what is left, clusters
the file graph, walks the impact and measures every directory. A cut from the greedy road is minimal,
not proven minimum, and says so. This repository's reading of one pinned commit is frozen, so a drift
means the measurement or the core moved.