The analysis track lets the reader ask, over the same index and judged in the same core.
Each family on the how page judges one shape of entropy already in the tree; questions keep the same terms — arithmetic over measured facts, no model or floating point, every constant cited to its source line. Numbering continues past the fifteenth, one booklet each, and every heading links its derivation in docs/reference/methodology.md.
A judgment family compares a measured fact with a written threshold; an analysis family answers a program — a question or a rules file — over the index's facts: the threshold is the reader's own. Rust measures and labels; the Haskell core judges. Since protocol 9.0.0 the texts the core reads (a program's source, the measured paths) cross over the local pipe; names in the fact tables still cross as hashes.
A CE Datalog question over the index's facts — files, directories, references, units,
complexity, judged clone and docdup pairs, mentions, path classes — answered with derivations. A rules
file's assertions are a gate: each violation is a witness row with its proof, and
ce rules exits 1 on the first. Five common checks need no Datalog: [rules.pack]
in ce.toml switches on directory cycles, fan-in and fan-out caps, layers and forbidden
pairs, which the core renders to the same language, under reserved pack_ heads, and
judges beside the file.
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
Rust sends the program's three texts (prelude, rules file, question) and only the fact tables the program
names; names in the tables go as fnv1a64 hashes and request-local ids. The core lexes the texts into
[kind, value] tokens, 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 stratum by stratum, and returns goal tuples,
violations, proof rows and errors, each error at a token mapped back to line:column.
Proofs are whole trees or nothing: over the node budget they are counted, never cut. Tokens and fact
rows are priced first (over the cap: a degraded reply); excess derived tuples abort the evaluation; a
core without the family is a named non-judgment. The built-in dead(F) is
ce deadcode's file-tier verdict; nine assertions in ce.rules are CI's
seventh dogfood leg.
Dead code inside one function: statements no path reaches, stores no path reads, unread
locals and, as advice, unread parameters. ce flow lists them, the guard
counts those a write adds, and the Stop line records them without 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;
with a finally, body and catch statements may also throw on out through it;
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
A FlowSpec table per language lowers every unit of ten languages (Python,
TypeScript and TSX, Rust, Go, C, C++, Java, Lua, R) into four integer tables. 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. Approximations only add paths (a finally is one hub
over the pending targets; expression-position exits are not lowered), so a finding can be missed,
never invented. A language is judged once its blind precision exam shows no false positive in a gated
kind: all ten are admitted (each gated kind passes or is vacuous — no positive in the sample and no
false positive), C++, R, Rust and TypeScript on a second exam generation; an unused parameter stays
advice everywhere. The guard class [flow] tier ships at observe; novel findings are a multiset
difference on (unit, kind, variable), so a moved finding does not count.
How each clone group folds into one function: where members differ becomes a parameter,
and the answer gives the parameter count, the member to keep, the lines saved, and feasibility with
the reason when it fails. 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
Each member's tree goes up in clone/1's encoding plus a leaf's source hash and
position class (one table per language, read off the flow tables); answers come back with every
member's text at every parameter, a gap's whole forest included. Comments are left out; a fragment
family keeps the top-node run every member holds, so a boundary cut does not split the shape, and the
merge is priced by the kept lines. The core aligns the members, finds the
holes, numbers the parameters and prices the merge. Groups it would refuse — Markdown and HTML (no
function to fold into), a tree not built, members not one shape — are counted by reason, never sent.
A T3 skeleton's line count is an honest integer estimate. Suggestion sets of this repository and of
four external corpora, each at a pinned commit, are frozen; a hundred drawn from them are
blind-audited for feasibility.
How directories depend on each other: the bottom layers, the cheapest arcs to cut to break
cycles, files that belong beside their partners, 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 measured paths go up with their line counts, the graph's references by path and the
--impact paths; the core builds the directory tree and the five tables (files with directory
and line count, directories, file-to-file and file-to-package references, the focus) itself, and each
cut arc comes back with its file references. The core folds the
references onto the directory graph, finds a feedback arc set per strongly connected component, layers
the rest, clusters the file graph, walks the impact and measures every directory. A greedy cut is
minimal, not proven minimum, and says so. This repository's reading at one pinned commit is frozen;
a drift means the measurement or the core moved.