← yich.us

Fact tooling: code review from extracted facts

Yichus computes machine-checkable facts about programs, without heuristics or guesswork. Every fact is one of two things, and says which: a structural fact of the compiled program (exact by construction), or an observation of real, seeded runs (always labeled “bounded observation, not proof”). The catch: it works only on Bosatsu, a small language where every program terminates and every side effect is declared; those two guarantees are what make this possible. If you want code reviews (AI reviews above all) whose claims can be verified or refuted mechanically, this is for you.

Sixty-second version

Here is a small pricing module (it ships in the repository at demos/pricing, so every command on this page runs as written). It has three real problems, plus a fourth finding that only running the code can surface.

struct Order(id: Int, qty: Int, discount_code: Int)

def quote(o: Order) -> Int:
  _ = o          # the order is thrown away...
  42             # ...and the "computed" price is hard-coded

def shipping_fee(subtotal: Int, region: Int) -> Int:
  match cmp_Int(region, 0):
    case GT: add(subtotal, 5)
    case _:  add(subtotal, 5)   # both branches identical: region decides nothing

def surge_bonus(x: Int) -> Int:
  match cmp_Int(x, 1000):
    case GT: x        # looks input-dependent...
    case _:  7        # ...but this branch always wins in practice

One command, yichus explore --agenda demos/pricing/*.bosatsu (the “agenda” is the tool’s list of noteworthy facts, one card each), reports this verbatim:

literal-result quote: “takes arguments, none of whose data reaches the produced value; every value the result can take originates in literals” The price is fake. No opinion offered; the fact is checkable.
dead-field Order.discount_code: “the field is constructed but no compiled extraction of it exists anywhere (reads: [])” Everyone fills in the discount code; nothing ever reads it.
guard-only-parameter shipping_fee(region): “the parameter only selects among branches; its data never reaches the produced value” Region picks a branch, but both branches compute the same thing.
constant-despite-reach surge_bonus: “across 8 seeded runs (seed 6321458022832390279), Demo/Pricing/surge_bonus always produced 7; bounded observation, not proof; parameter(s) x statically reach the produced value and no literal-result fact applies — the constancy is not statically explained” Static analysis alone can’t see this one, so the tool also runs the code.

Every fact is located and machine-checkable; the run-based ones carry the seed their inputs were generated from. None of them says “bug”. You decide which of these count as bugs. Whether each fact is true has already been settled mechanically.

Every fact type

Eleven kinds of fact, each with real code and the tool’s actual output. The first eight are computed statically, so they are exact by construction. Running the code on seeded, type-derived inputs produces the last three, which always carry the label “bounded observation, not proof.”

Dig deeper: what “reaches” and “influences” mean, and which way the guarantee points

The static facts are computed on the compiled program’s dataflow structure: a value reaches a result if there is a chain of data edges from one to the other, and influences adds control edges (picking a branch counts). Structure over-approximates real influence, since every way a value could matter shows up as an edge. That is why the negative facts are the guarantees: “never reaches” and “influences nothing” mean no execution can make the value matter. A bare “reaches” only says a path exists on paper. So constant-despite-reach gets its own fact type: the structure says the input could matter, the observed runs say it didn’t, and the fact records that disagreement.

dead-field a record field that is written but never read
struct Order(id: Int, qty: Int, discount_code: Int)
# id and qty are read by accessors; discount_code is read by nothing
fact“the field is constructed but no compiled extraction of it exists anywhere (reads: [])”

Data being diligently maintained that cannot affect anything.

unused-parameter a parameter that influences nothing in its function
def quote(o: Order) -> Int:
  _ = o
  42
fact“the parameter influences nothing in the binding (neither data nor guards)”

The function promises to consider its input and doesn’t.

guard-only-parameter a parameter that picks branches but never reaches the result
def shipping_fee(subtotal: Int, region: Int) -> Int:
  match cmp_Int(region, 0):
    case GT: add(subtotal, 5)
    case _:  add(subtotal, 5)
fact“the parameter only selects among branches; its data never reaches the produced value”

Legitimate for dispatch helpers; suspicious when the spec says the value should flow into the answer.

discarded-argument a call that passes a value into a parameter the callee throws away
def summary(o: Order) -> Int:
  quote(o)      # quote discards o entirely
fact“a call passes a value into a callee parameter that influences nothing in the callee: the passed value is discarded there”

The caller passes data the callee never uses, and the fact points at the exact call.

literal-result a function whose every possible result is built from constants
def quote(o: Order) -> Int:
  _ = o
  42
fact“takes arguments, none of whose data reaches the produced value; every value the result can take originates in literals” (literals: 42)

The signature of a faked computation, including ones laundered through helper calls. The analysis follows values across function boundaries.

detached-read a state read whose value influences nothing
refresh: IO[Int] = (
  c <- cell.flat_map()
  _ <- c.read().flat_map()   # read the cell...
  pure(7)                    # ...ignore it, return 7
)
fact“the binding reads a state cell and the read value influences nothing (neither data nor guards)”

A state cell is a mutable variable managed by the runtime. This is code that looks like it consults state and doesn’t.

blind-write a state write with no read of that cell in the same code
reset: IO[Unit] = (
  c <- cell.flat_map()
  c.write(9)     # overwrites whatever was there, unconditionally
)
fact“the binding writes cell ‘cell’ without any read of that cell in this binding”

Fine for a reset; a data-loss hazard in a read-modify-write that forgot the read.

package-reach one package can transitively reach another, with the import path
package Demo/Report
from Demo/Pricing import Order, quote
fact“Demo/Report → Demo/Pricing” (path: Demo/Report → Demo/Pricing)

These facts power layering rules like “the domain layer must never reach the notification layer”. When a rule is broken, you get the exact import chain that breaks it.

run-evidence what happened when the code ran on seeded inputs
def quote(o: Order) -> Int:
  _ = o
  42
fact“across 8 seeded runs (seed 6321458022832390279), Demo/Pricing/quote always produced 42; bounded observation, not proof”
fact“varying parameter ‘o’ of Demo/Pricing/quote across 5 values with the others fixed (seed 6321458022832390279), the result never changed; bounded observation, not proof”

Every pure function can be run in isolation, because Bosatsu guarantees it terminates. Every run-based fact carries the seed its inputs were generated from. Healthy functions get the opposite fact: “runs 0 and 1 (seed 6321458022832390279) of Demo/Pricing/line_total produced 28 and -39 — the result depends on its inputs.”

unexecuted-reference referenced by code that ran, but only at a spot that never executed
def pick_quote(x: Int) -> Int:
  match cmp_Int(x, 999):
    case GT: fallback_quote(x)   # this branch never runs
    case _:  x
fact“across a sweep of 10 entry bindings x 2 runs each (seed 6321458022832390279), Demo/Pricing/fallback_quote was never referenced on an executed path of another binding; it IS statically referenced by Demo/Pricing/pick_quote — which ran — at a site that never executed; bounded observation, not proof”

Dead code that a “who references this?” search would call alive.

constant-despite-reach inputs reach the result on paper; observed constant in practice
def surge_bonus(x: Int) -> Int:
  match cmp_Int(x, 1000):
    case GT: x
    case _:  7
fact“across 8 seeded runs (seed 6321458022832390279), Demo/Pricing/surge_bonus always produced 7; bounded observation, not proof; parameter(s) x statically reach the produced value and no literal-result fact applies — the constancy is not statically explained”

The static story and the observed story disagree, which is the kind of divergence worth a human look.

Check a review against the facts

A review (yours or an AI’s) is submitted as claims, and the verifier answers from the facts. Three claims about the module above:

{"claims": [
  {"kind": "literal-result",   "package": "Demo/Pricing", "binding": "quote"},
  {"kind": "no-detached-read", "package": "Demo/Report"},
  {"kind": "dead-field",       "package": "Demo/Pricing", "struct": "Order", "field": "qty"}
]}

yichus verify-claims claims.json demos/pricing/*.bosatsu returns (actual output):

supported for literal-result quote: “every value the result can take originates in literals”
supported for no-detached-read in Demo/Report: “zero detached reads in Demo/Report — 1 read site(s) examined program-wide, every read’s value has influence in scope”
refuted for dead-field Order.qty: “the field IS read, by: Demo/Pricing/line_total, Demo/Pricing/order_qty”

The exit code is 1 because one claim was refuted, so a wrong review fails the same way a failing test does. Look at the second verdict: “nothing is wrong here” arrives backed by an enumerated count of the read sites examined. Every fact kind has a claim kind, plus no-<kind> absence forms; the full vocabulary is in the agent tooling guide.

Fact diff: what changed between two versions

yichus fact-diff computes all the facts for two versions of a program and reports which facts appeared or disappeared. Here someone replaced quote’s real computation with the hard-coded 42 (actual output, trimmed):

introduced literal-result quote: “every value the result can take originates in literals” blast radius: 2 consumers (Demo/Pricing/summary → quote, Demo/Report/report_total → quote)
introduced discarded-argument summary → quote(o): “the passed value is discarded there”
removed run-evidence quote: “runs 0 and 1 (seed 6321458022832390279) of Demo/Pricing/quote produced 5 and -9 — the result depends on its inputs”

The change’s real story: the price became a constant, the callers’ data now goes nowhere, and the “depends on its inputs” evidence vanished. Each fact lists who is affected downstream (“blast radius: 2 consumers”), and claims like “no new dead fields” fail the change with exit code 1, same as the verifier.

Results

Measured on planted-defect codebases with independent AI reviewers, scored against a hidden key. The strongest results:

The honest limit: with an unlimited budget, a strong source-reading agent also found everything, at 1.85× the cost. Comprehension is the same either way. The gain from facts is that accuracy stops depending on how much the reviewer can afford to read. All the benchmark data, including the losses →

Try it

Needs git, a JVM (17+), and sbt; sbt assembly writes yichus.jar under target/. The pricing module above ships in the repo, so this works end to end:

# one-time setup
git clone https://github.com/snoble/yichus && cd yichus
sbt assembly
alias yichus="java -jar $(pwd)/target/scala-*/yichus.jar"

yichus explore --agenda demos/pricing/*.bosatsu     # every fact, as cards
yichus explore --agenda --agenda-type dead-field \
  demos/pricing/*.bosatsu                           # one fact type

# save the three-claim JSON from the section above as claims.json:
yichus verify-claims claims.json demos/pricing/*.bosatsu

# fact-diff wants a "before": copy the module, then edit
# old/pricing.bosatsu to give quote a real computation — the page's
# diff above is exactly that edit, seen in reverse:
cp -r demos/pricing old
yichus fact-diff --before old/pricing.bosatsu \
  --after demos/pricing/pricing.bosatsu

There is also yichus tla, a separate feature that model-checks declared state-machine invariants and reports holds/violated/inconclusive verdicts with counterexample traces. See the spec-first verification demo.