Every step below is executed by tests/test_demo_walkthrough.py on every CI
run — if this document drifts from reality, the build goes red. Expected
outputs are from real runs. The demo video in docs/media/demo.mp4 follows
this exact script.
Prerequisites: Python ≥ 3.10, git, and the target project's own test
suite runnable with pytest, plus pytest-cov in the target venv (it powers
coverage-based fault-file discovery on large codebases). fluidfix's core has
zero dependencies.
pip install --upgrade fluidfix
fluidfix selfcheckExpected: SELFCHECK PASS after the exhaustive re-derivation — 4096 routing
cases, identity, composition, 256 dispatch states, termination. This runs
offline in about a second; if it ever fails, do not use the build.
Any project where pytest runs is a target. The demo service:
# billing.py
def price_after_tax(p, rate):
return p * (1 + rate)# test_billing.py
from billing import price_after_tax
def test_tax():
assert price_after_tax(100, 0.1) == 110.00000000000001pytest -q is green. Commit and deploy. fluidfix touches nothing while the
suite is green — that is a tested guarantee, not a habit.
Someone flips the operator (1 + rate → 1 - rate) and ships it. Run the
guard — note you do not tell it which file broke:
fluidfix guard . --commitExpected output shape:
[..] billing.py: repaired line 2 in 4 suite runs (0.7s):
- return p * (1 - rate)
+ return p * (1 + rate)
committed
What happened: the guard found the fault file mechanically (traceback frames,
else failing-test coverage ranking), the kernel routed the observed fault kind
to its repair, your suite accepted the candidate, and the restoration was
committed as fluidfix: restore billing.py:2. Zero model tokens.
Deployment modes:
fluidfix guard . # one-shot: exit 0 green/repaired, exit 2 refused (CI gate)
fluidfix guard . --interval 900 # watch mode, every 15 minutes (cron/systemd)On GitHub, use the ACTION — a bare fluidfix guard . in CI validates a
repair and then throws it away with the runner; the action pushes the
restore commit back to the branch and writes the verdict (or the refusal's
rejected-candidate log) into the job summary:
# .github/workflows/fluidfix.yml
name: fluidfix
on: [push]
permissions: { contents: write }
jobs:
guard:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.12" }
- run: pip install -e . pytest pytest-cov # YOUR project's deps
- uses: devkancheti4-design/fluidfix@master # mode: push (default)Ship a different defect — say inv.get("tax") losing its , 0 default so
None poisons a sum. The shipped vocabulary has no such class:
fluidfix guard . --commitExpected: REFUSED: fault is outside the taught vocabulary ..., exit code 2,
the working tree byte-identical, and a machine-readable teach-me signal in
.fluidfix/last_refusal.json. fluidfix never guesses: the refusal is the
feature that makes the repairs trustworthy.
Write the incident down once, as a fault-class dictionary. This is the whole "how to give examples" contract — three parts per class:
# company_rules.py — version this file next to the code it maintains
# Taught from ONE worked example (incident INC-2041):
# due += inv["amount"] + inv.get("tax") <- summed None, crashed
# due += inv["amount"] + inv.get("tax", 0) <- the fix
register(
4, # kind: 0-15, one per class
"missing-get-default", # name
'a .get(key) with no default, letting None poison arithmetic', # what an
# observer must see (LLM observers get
# this sentence verbatim)
re.compile(r"\.get\((\"[^\"]+\"|'[^']+')\)"), # signal: when a line
# can exhibit the class
lambda line, o: re.sub(r"\.get\((\"[^\"]+\"|'[^']+')\)",
r".get(\1, 0)", line), # the repair transform
)Rules of thumb (all enforced by how the pipeline works):
- One class = one
register()call. The kind number is yours; the act code is inferred by the router from its single worked example — you never touch the kernel, and renumbering your classes never breaks anything. - The transform must be derivable from the line plus what the repo already
contains. A pure rule (flip, decrement, add-default) returns one
candidate. When the correct value lives elsewhere in the repo — the right
attribute name, a constant sibling code already uses — return a candidate
list mined from
obs.all_lines/obs.root(capped at 32); the suite tries each in order and refusal still happens if none passes. Only fixes needing information that exists nowhere in the repo (a new algorithm, an outside fact) stay out of scope — leave those refused. - The suite stays the judge. A wrong transform cannot land: candidates that don't green the suite are rolled back byte-exactly.
- Multi-line fixes are first-class: a candidate may contain
\n— one taught example can replace a wrong line with a whole corrected algorithm (CI-tested: mean-to-median, 5 lines from 1). - Several wrong lines? Return a
SpanEdit. When a defect spans lines that must change together, returnSpanEdit(start, end, text)(in scope in dictionary files, likere): one atomic candidate replacing lines start..end. The span must contain the observed line; rejection rolls back byte-exactly; two green spans refuse as ambiguous. Prefer it over a block-candidate ending inreturn, which can strand the old lines below as dead code no suite can see. reandregisterare in scope inside a dictionary file; nothing else is needed.
fluidfix guard . --commit --dictionary company_rules.pyExpected: dictionary company_rules.py: 1 fault class(es) registered, then
the repair and the commit. Now break a different file with a different
key (.get("ms") in metrics.py) and run the same command — it repairs that
too. One example bought the class; every member is decided in four integer
instructions at zero tokens. Commit company_rules.py to the repo so CI and
every teammate's guard share the same taught classes.
import sys
from fluidfix import MechanicalObserver, Oracle, guard_once
from fluidfix.acts import load_dictionary
load_dictionary("company_rules.py")
report = guard_once(Oracle(".", python=sys.executable), MechanicalObserver())
print(report.summary()) # repaired / green / refused — never a guessOracle(python=...) should point at the target project's interpreter
(its venv) so its test dependencies resolve.
| Symptom | Cause / fix |
|---|---|
| Repairs scored as failures | stale bytecode — fluidfix already runs -B and clears __pycache__; do the same in any wrapper you build |
| A whole library scores zero | a pytest plugin collides with a suite fixture (seen: pytest-benchmark) — fluidfix passes -p no:benchmark |
| Coverage localisation finds nothing | never run pytest-cov with -x (exit-first silently skips the JSON report — upstream bug, fluidfix avoids it) |
| Guard refuses on a suite slower than 300s | raise --suite-timeout / --candidate-timeout |
| Wrong interpreter / missing deps | pass --python path/to/project/venv/bin/python |
Fleets that gate every write behind human review can run the guard as a pure
proposer (executed by tests/test_dry_run.py):
fluidfix guard . --dry-runWhen a repair lands, the guard prints the broken→repaired unified diff,
restores the working tree byte-exactly to its broken state, writes the
diff to .fluidfix/proposed.patch, and exits 0:
[..] mod.py: repaired line 4 in 4 suite runs (0.6s): ...
--- a/mod.py
+++ b/mod.py
@@ -1,6 +1,6 @@
...
- if x >= t:
+ if x > t:
...
PROPOSED (dry-run): apply with git apply .fluidfix/proposed.patch
The suite still judged the candidate — the diff was accepted green before the
restore. Apply it after review with git apply .fluidfix/proposed.patch
(the patch is git apply-ready even in non-git trees, via the difflib
fallback). Refusals behave exactly as without the flag: exit 2, tree
untouched, teach-me signal in .fluidfix/last_refusal.json. --dry-run and
--commit are mutually exclusive — one guard either proposes or writes,
never both.