Audience: the Pyrefly team, and anyone trying to understand why tainie forks
Pyrefly. For the model side — evidence and asks for the Mellum team — see
mellum.md.
A common agentic-coding task: "I renamed a parameter — now update every call site." Or: "We added a required field to a dataclass; fix every constructor." These are named-symbol fan-out edits: one structural change ripples across tens or hundreds of use-sites scattered through a codebase.
General-purpose coding agents handle this badly. They use grep, which finds too much (decoys, comments, unrelated names) and misses too much (type-resolved binding sites invisible to text search). They let the model decide scope, which is unreliable at scale. They verify with tests, which only catch value errors after the fact.
A small model makes this worse, not better. A 2.5B-active model is a strong local generator and a weak global reasoner. The more of the scope and correctness burden you put on it, the less reliable the whole loop becomes.
tainie's answer is a generate-and-verify split:
- Pyrefly finds the work and proves it correct. Discovery is type-resolved, not text-matched. Verification is type-checker gating, not tests.
- The model makes one bounded mechanical edit per site, never deciding scope.
Every fork change below obeys one principle: the verifier carries the correctness burden so the model doesn't have to. The smaller the model, the larger the win.
From an orchestrator's perspective tainie is a three-parameter call — a workspace, a symbol, and an instruction. The orchestrator decides when tainie is the right tool; tainie handles everything in between, in one of two tiers:
- Deterministic tier (
run_fanout, no model) — mechanical edits needing no judgment: field/parameter reference retargeting. Discovery → span-precise transform → forkedpyrefly overlay-checkof the whole candidate in memory → atomic commit. Nothing touches disk until the full candidate type-checks. - Model tier (
run_demo, Mellum per site) — uniform edits needing a little judgment: add a keyword argument. Discovery → one bounded edit per site → per-site type gate → commit.
Both tiers share one spine — Pyrefly finds, classifies, and proves; the model fills the hole:
- Locate — Pyrefly's Glean exporter (
pyrefly check --report-glean) emitsXRefsViaNameByTargetfacts: the exact type-resolved use-sites for the named symbol. No grep, no heuristics. - Edit — a deterministic span-precise transform, or a per-site Mellum micro-agent that sees one file, one line, and the instruction. It never sees the worklist or decides scope.
- Verify — the deterministic tier assembles all edited files in memory and
runs the forked
pyrefly overlay-checkin one transaction, writing only a fully-passing candidate. The model tier gates each site against a single-filepyrefly check. - Report — sites applied, skipped (with reasons), unreachable
(
unresolvable), and whether the gate passed.
The fork exists to make steps 1 and 3 possible: richer discovery facts and an atomic verification surface that stock Pyrefly does not expose.
The fork (~/PycharmProjects/pyrefly, branch tainie-fork) adds four
capabilities. The file-level manifest is the fork's
TDOM.md; this section is the honest
high-level surface (≈16 source files, not the "2 files" earlier drafts
claimed).
A non-interactive batch surface for "check this candidate edit set without
touching disk." Takes a JSON edit-set, swaps it in via Transaction::set_memory,
runs a full-project check in memory, and returns the new diagnostics (optionally
diffed against a --baseline). Lives in pyrefly/lib/commands/check.rs
(~221 lines for run_with_overlay + OverlayCheckArgs) plus load-bearing
overlay plumbing in pyrefly/lib/state/{load,memory,state}.rs — the most
divergent, least-upstream-aligned part of the fork. It turns the gate from "is
this one file still OK?" into "is this atomic edit set still OK?" and makes
failed retries pure computation. (Fork-side coverage:
pyrefly/tests/overlay_check.rs.)
Five new xref paths, all emitted into the existing XRefsViaNameByTarget fact
(no new fact type, no consumer change), in pyrefly/lib/report/glean/convert.rs
(+ mod.rs):
- keyword → constructor field (
Complaint(agent_name=...)→Complaint.agent_name) - keyword → function/method parameter (
render(verbose=True)→render.<locals>.verbose) - keyword → TypedDict field
- positional argument → parameter (
render("x", True)→render.<locals>.verbose) - structural Protocol implementors (same-module, via the existing subtype oracle)
Together these make field rename, parameter rename, and change-signature into type-resolved worklists that grep cannot produce.
pyrefly/lib/alt/tdom.rs (~372 lines) — the largest area and the one least
likely to upstream as-is, since it is tdom-specific. It type-checks PEP 750
HTML-component t-strings (html(t'<{Heading} name={x}>...')) as ordinary
keyword calls by synthesizing a call from the interpolation expressions and
feeding it to the existing inference engine, so every argument error lands on the
right source span. Purely additive — standard t-strings are unaffected.
pyrefly/lib/alt/special_calls.rs + crates/pyrefly_{config,types} adds a
distinct error kind assert-never (ErrorKind::AssertNever, default
Severity::Error) that fires when an assert_never(x) arg is reachable
(!is_never() && !is_any()). This catches non-exhaustive matches on
frozen-dataclass and pydantic discriminated unions — richer subjects than the
stock non-exhaustive-match (enum/Literal only, default Warn), which the
fork does not change. See concepts/spm.md for the full
capability map.
The fork is a one-way snapshot, not a tracked rebase: merge-base 99c080b,
15 commits ahead / 134 behind facebook/pyrefly (as of 2026-06-29).
Permanent divergent carry is concentrated in alt/tdom.rs and the
state/{load,memory,state}.rs overlay plumbing. Posture: accept divergence;
rebase only when upstreaming a specific piece. The CWD-relative Glean path
behavior is upstream-by-design; tainie resolves paths against the repo root to
account for it (documented at the fork's file_fact).
Ranked by leverage for SLM-driven agentic coding:
- Upstream the
overlay-checksurface (highest leverage) — accept it as a first-class command. ~221 lines of adapter on machinery that already works in the LSP path; today it carries merge-conflict cost againstcheck.rs+state/. - Reference-kind tags on xrefs — tag each xref
def | import | call | attribute-read | keyword-arg | parameter | …. Lets tainie drop the O(refs) source re-read and route the correct transform per kind. - Upstream the positional argument-slot export — completes change-signature;
reuses matching
call.rsalready computes; no new fact type. - Completeness certificate (
prove-complete) — expose Pyrefly's implicit "did we find them all?" signal as a queryable proof. - Edit-invariant diagnostic identity — anchor diagnostics to a stable
semantic id so edits don't shift
(path, line, col)and overlay checks compose. - Untyped-reach / blind-spot signal — alongside resolved xrefs, emit a lexical-candidate set (name appears but is outside the type graph), clearly labeled, so a silent blind spot becomes a machine-readable hand-off list.
- Canonicalize overlay input paths — ✅ fixed in fork
(
246f66c). On macOS a symlink-form overlay key (/tmp/…) missed the canonical handle (/private/tmp/…) and the file was silently checked against disk — an undetectable false-green. The fork now canonicalizes at the input boundary. Upstream ask: accept the fix. - Error on orphaned overlay entries — ✅ fixed in fork
(
80c92aa). An overlay entry matching no project handle was silently discarded (another false-green, with no client-side workaround). The fork nowbail!s on existing-but-unmatched paths. Upstream ask: accept the fix.
Model-tier asks (Mellum team and Mellum 2.1 training properties) live in
mellum.md.