Handbook · index-and-refer
Fux
A search index for your written knowledge — decisions, runbooks, specs, wiki pages — committed to git as text you can read, diff and review. Nothing to run, nothing to call, and no second copy of your documents.
00 · Start here
What it is, why it exists, how it works, and the vocabulary.
01 · How it works
Ingest, the two planes, the query and refer path.
02 · Confidence
How much the index believes its own answer.
03 · Testing
Four layers, and what a quality number means.
04 · Reference
Merge · fetcher · decoder · CLI.
Two ways to read this. Scroll it, or hit
▶ Present to step through it as slides from the top — what, why,
how, then the detail. → / ← to move, Esc to leave,
? for the keys; p presents from wherever you happen to be reading.
New to this? Nothing here assumes you have built a search system
before. Every slide opens with a plain-language line, and the vocabulary
slide defines every term the rest of the deck uses. Version: this describes
main — 2.0.0-alpha.2 plus the unreleased confidence-floor change of 2026-08-28.
00 · Start here
What Fux is
Rank from a small committed index · fetch content from the systems that own it · verify at answer time.
Your documents stay put
- The repo, a wiki, a web page
- The text is read, counted, then dropped
- One narrow exception: an explicit per-source
snapshotpolicy (L2)
The index is committed
.fux/index/, 256 JSONL shards- One canonical line per document
- Term hashes and counts — plus the path, and the title in clear when the source is a git repo
Answers quote the original
- The index says which document
fux answerre-opens it and quotes today's bytesaskandfinddo not — they rank, and say so
00 · Start here
Why Fux exists
The usual way to make documents searchable for an agent is a vector database. A vector database is an index, and that is not the objection. The objection is what the surrounding stack has to be true for it to work.
You cannot review the change
- An embedding is an array of floats
- A diff over it is unreadable, so a ranking change cannot be argued in a PR
- Fux's index is JSON text — the change is a changed line
It is hard to reproduce
- Approximate nearest-neighbour search trades exactness for speed by design
- Embedding models are versioned, and float maths varies across builds
- Fux: same inputs → byte-identical index, on any machine
"Only vectors" is a weak privacy claim
- In practice the chunk text is stored too — the model needs it
- And an embedding is not a one-way hash: published inversion work recovers much of the source text from embeddings alone
- Fux keeps term hashes and counts, and never the prose
There is something to run
- A store, a model, usually a key and a per-query bill
- Embedding locally is possible — so this is a default, not a law
- Fux's runtime is the standard library and nothing else
The bet
- For a project's own documents, lexical ranking is enough. ⚠ Held narrowly: the one thing measured here was a bundled static-embedding lane, gated at 0 fixed / 2 broken and deleted (the wheel went 6.84 MB → 233 KB). That retires that lane. It is not a result about dense retrieval in general.
- Freshness and provenance are the part that actually hurts — and a derived copy makes both harder, because nothing in it can notice the source moved on. So leave the documents where they live, keep only statistics, and re-read before quoting.
- An index plain enough to commit turns retrieval into something a human reviews in a diff and CI regression-tests byte for byte.
The honest counter-case — where a vector store wins and Fux does not pretend otherwise
The honest counter-case
Three places a vector store is the right tool and Fux is not.
- Paraphrase. A question sharing no words with the document that answers
it is exactly what dense retrieval is for. Fux stems and weights fields; it does not
understand synonyms.
fux enrichexists to let an agent add vocabulary to a document — which is Fux conceding the point and refusing to solve it with a model at ingest time. - Scale. The design point is 10 000 documents. A hundred million passages is a different problem and this is the wrong tool for it.
- Cross-language and multi-modal retrieval. Not addressed at all.
Inversion reference: Morris, Kuleshov, Shmatikov & Rush, Text Embeddings Reveal
(Almost) As Much As Text, EMNLP 2023 — the vec2text line of work.
00 · Start here
Twelve words, before anything else
| Term | What it means here |
|---|---|
| index | A file of statistics about documents that makes them findable. It is not the documents, and it cannot reproduce them. |
| term | A word after standardisation — lower-cased, stripped to its stem. “Running” and “runs” become one term. Fux stores the hash of it, not the word. |
| posting | One entry saying this term appears in that document, this many times. The list of all of them for one term is a posting list — the core of any search index. |
| field | Which part of a document a word sat in: body, heading, title, path or context. Fux weights them differently, so a word in a heading counts for more. |
| BM25 / BM25F | The standard model-free formula for “how well does this document match these words”. The F means each field gets its own weight. |
| embedding / vector | An array of numbers a model produces from text so that similar text lands nearby. Fux does not use one — it is here because the comparison keeps coming up. |
| chunk / passage | A slice of a document small enough to quote. Fux cuts on headings, and only at answer time, on freshly fetched bytes. |
| shard | One of the 256 files the index is split across, chosen by document id — so two people editing different documents rarely touch the same file. |
| sha | A fingerprint of exact bytes. Same bytes → same sha; one character different → an entirely different sha. It is how “has this changed?” is answered without a diff. |
| committed / derived | Committed = goes into git, is the real thing. Derived = a rebuildable cache, ignored by git, safe to delete. Every file Fux writes is declared as one or the other. |
| freshness verdict | Fux's answer to “did I actually look at that document just now?” — one of
current, stale, cached, unverified. |
| RAG · MCP | RAG — retrieval-augmented generation: find relevant documents, put
them in a model's prompt. Fux is the find half and refuses the rest.
MCP — the protocol a coding agent uses to call a tool;
fux mcp serves the index over it. |
00 · Start here
How it works — the whole thing in five beats
Every slide after this one is detail on one of these five.
- Ingest. Two committed text files —
.fux/sources/dirsand.fux/sources/urls, one entry per line — say what to read. Each document is fetched, decoded, analysed into five fields, written out as statistics, and the content is thrown away. - The committed plane.
.fux/index/— 256 JSONL shards, one canonical line per document, full postings. Sorted and content-sharded, so git itself diffs and merges it line by line. - The derived plane.
.fux/runtime/— an accelerator rebuilt from those shards, never committed. Bound by a differential law: its results are byte-identical to the reference scan's, asserted over thousands of comparisons rather than spot-checked. - Query.
ask·find·answerrank with BM25F over the five fields and hand back documents and locations. A confidence band travels with the result in--jsonand over MCP always; on a plain terminal it needs--band. - Refer. The words come from the live document today: fetch it, compare the
sha, quote
path:L12-L40, attach a freshness verdict. A differing sha marks the document dirty, and a narrowed re-ingest repairs the ranking.
ask came in at a worst-case p95 of 27.2 ms
against a pre-registered 150 ms bar, where the reference scan takes 4.2 s. One corpus, not a
general claim.
Next: those same five beats as one diagram — then each of them in turn.
01 · How Fux works
The mechanism, end to end
- Blue — committed to git. Plain — derived and disposable. Green — the freshness loop back into ingest.
- Read it left to right, top to bottom: ingest → the committed plane → the derived plane → query → refer.
01 · How Fux works
Ingest — five steps, and the content is thrown away
- Walk — entries are deduped and sorted, so file order can never change a committed byte.
- Fetch — a thread pool bounded by
min(declared, configured); network is opt-in and fenced. - Decode — 19 built-in decoders; a consumer file overrides any of them.
- Analyse — the same analyzer the query uses. Two copies would drift, and a drifted analyzer misses silently.
- Write — same sources → byte-identical shards, on any machine, on any OS.
01 · How Fux works
The analyzer, and the five weighted fields
.fux/tune.toml; these are the defaults.loc (its path or URL) and, when the source's meta is
plain — the default for a git repo — its title and heading-derived
phrases, in clear. Set meta = hashed (the enforced default for
non-git sources, L5) and those become a single title_h instead, with the readable
title kept only in the gitignored display cache. So: a shard never reveals a document's
prose; whether it reveals a document's name is a per-source policy you choose.
See SR-RECORD (0010).
01 · How Fux works
Two planes — one holds truth, one holds speed
01 · How Fux works
Line ranges come from answer, never from ask
ask tells you which documents. answer opens a document and tells you which lines. Only answer can give line numbers, because only answer reads the file.The single most confusing thing about the surface, so it goes first.
fux ask and expected :L12-L40, nothing is broken — it is the
wrong verb for that question.
01 · How Fux works
What a citation actually is
- Not a document with a wish of luck — a span.
- A heading-delimited passage, a line range, and the sha of the bytes just read.
The captured run, and the JSON an agent should read — verbatim, not illustrated
$ fux answer "why did we choose helix"
# Service mesh
We adopted a service mesh to stop a slow dependency taking down checkout.
## Why we chose Helix
Helix gave us per-route timeouts and circuit breaking without touching
application code. The alternative was a library in every service.
-- docs/mesh.md:L1-L8 (sha 516bef067812, current)
"citation": {
"id": "file:docs/mesh.md",
"loc": "docs/mesh.md:L10-L13",
"sha": "516bef06781262a0a64b10027316c18d668eeccb",
"freshness": "current"
}
Examples that are typed rather than captured rot silently, and this project has paid for that once already.
How the spans are chosen — four rules, all cheap and model-free
| Step | Rule | Why |
|---|---|---|
| Split | On markdown headings | A document's headings are the author's own segmentation — boundaries a human already agreed with, at no cost and with no model. |
| Merge | Sections under 120 bytes fold forward | A two-line passage is a citation nobody can read in isolation. |
| Split again | Sections over 4 000 bytes split on paragraphs | One 40 KB section would otherwise eat the whole byte budget by itself. |
| Address | path:L<start>-L<end> |
An agent acts on a citation by opening a file at a line. An ordinal alone would force a second call. |
- Text before the first heading is its own passage — a preamble is content, and dropping it silently is how the one sentence that answers the question disappears.
- The passages are transient: never written to
.fux/, never cached, never indexed. That is L2. - It also makes the byte budget honest — chunking runs on fetched bytes, so the assembler knows every candidate's real size instead of estimating it. A web-scale system has to guess here; this one does not.
01 · How Fux works
Four freshness verdicts
The fourth exists precisely so the other three stay honest.
01 · How Fux works
Using it
Install and set up
pip install fux-engine
fux setup
fux ingest && fux build
Tell it what to index
fux add docs/
fux add README.md
fux add https://wiki.corp/page
fux remove docs/legacy/
fux update
Ask
fux ask "…" --json
fux find "…"
fux answer "…"
fux doctor
fux setupwrites files you own —fux.toml, the source lists, the fetcher and decoder templates, the agent files. Commit them, edit them; fux will not overwrite them.fux add <URL>both records the URL and fetches it.fux update --checkis read-only: it reports drift, offline, without fetching.
Which read verb to reach for — six verbs, one table
| Command | Gives you | Reach for it when |
|---|---|---|
fux ask | Ranked results with score, loc, archived, confidence | You want candidates and will judge them |
fux find | Bare paths | You are piping into something else |
fux answer | One cited answer, re-scored on current bytes | You want the answer, with a freshness verdict |
fux explain | Edges into and out of one document | You are asking what something depends on |
fux graph | The neighbourhood around the best answers | You are orienting in unfamiliar territory |
fux path | How two documents connect | You suspect a relationship and want the chain |
02 · Confidence
How much the index believes its own answer
- The second case is where an agent invents an answer and cites a real file while doing it.
- Every query verb therefore returns a confidence block that makes the difference machine-readable.
- It is a pure function of what ranking already produced — the query's term
hashes, the
dfstatistics BM25F needed anyway, the scored list, and (onansweronly) the refer plane's verdict. - Nothing in it fetches, samples, calls a model, or reads a clock. L1, L3 and L4 are untouched.
02 · Confidence
Five signals in, one band out
none, partial, weak, grounded.stale demotes to partial rather than weak,
because stale bytes are a knowable defect a consumer can name.02 · Confidence
The six fields, and the one to read first
missing— read this first. It is the difference between hedging vaguely and saying "nothing here mentionsmTLS".coverageis idf-weighted — missingtheis nothing; missingmTLSis the question.doc_coverageis the same idf mass measured over the top-ranked document instead of the corpus. A question whose four words sit in four different documents scorescoverage: 1.0— correctly — and it isdoc_coveragethat says no single document contains the question. ⚠ It reports; it does not gate (see the floors).separationis1.0when exactly one document scored — the strongest separation there is, not the weakest.askandfindalways reportunverified— "we did not look", never "it was fine".
The full field table, with the reasoning behind each shape
| Field | Answers | Why it is shaped that way |
|---|---|---|
coverage | Did the corpus contain the words you asked about? | A query term the corpus has never seen has df == 0, which idf scores as
the rarest possible term. Weighting by idf therefore makes a missed rare word
cost far more than a missed common one — which is correct, because the rare word is what
made the question specific. |
doc_coverage | Did the cited document contain the question? | Added 2026-08-28, after a decoy control found "what is the SLA we publish for the
payments API" reaching grounded at coverage: 1.0 — every
term present, in four different documents, none of them about it.
It is published, not enforced: the 37 real answers that reach this
clause span 0.401–1.000 and the one decoy sits at 0.710,
inside them. There is no gap to put a threshold in. |
separation | Can the ranking tell first place from second? | (top1 − top2) / top1, clamped to [0,1]. |
support | How many documents came back at all | Bounded by --top, deliberately. A corpus-wide count
would differ between --fast and --scan, because the accelerator
skips documents it has proved cannot reach the top k. That is the
differential-law break the design forbids — the law is worth more than the better
number. |
verified | Were the cited bytes still what the index recorded? | Filled in after the fetch, because answer ranks before it fetches. The
band is a derived property, so it re-computes and cannot go stale against it. |
missing | Which of your words are nowhere in the corpus | Reports the surface form, never the analyzed one. "mtl
is not in this corpus" is worse than silence — a reader cannot tell whether fux
misunderstood the question or the corpus is genuinely missing the topic. |
02 · Confidence
Every number, written out
There is no one confidence formula. There are the four named signals plus
doc_coverage, one shared weight underneath them, and a five-clause ladder that stops
at the first clause that fires.
idf BM25F already needed — so nothing here costs an extra pass over
anything.
idf(t) = ln( (N − df(t) + 0.5) / (df(t) + 0.5) + 1 )
A term the corpus has never seen has df = 0, which this
scores as the rarest possible term. That is the whole point: missing
the is nothing, missing mTLS is the question.
The signals
Q = the query's distinct term hashes D = the top-ranked document's term hashes
N = documents in the corpus S = scores > 0, sorted descending
Σ idf(t) for t ∈ Q where df(t) > 0
coverage = ──────────────────────────────────── # clamped [0,1], 0 if Σ idf = 0
Σ idf(t) for t ∈ Q
Σ idf(t) for t ∈ Q ∩ D
doc_coverage= ───────────────────────── # 1.0 when no results — never
Σ idf(t) for t ∈ Q # demote for "not computed"
⎧ (S₀ − S₁) / S₀ if |S| ≥ 2 and S₀ > 0
separation = ⎨ 1.0 if |S| = 1 # nothing competes: the STRONGEST
⎩ 0.0 if |S| = 0 # separation, not the weakest
support = |S| # bounded by --top, deliberately
missing = ⟨ surface(t) : t ∈ Q, df(t) = 0 ⟩ # the user's spelling, in order
verified = current | stale | cached | unverified # from the refer plane, or "we did not look"
The ladder — first true clause wins
support == 0 → none abstain; answerable = false
missing ≠ ∅ or verified == "stale" → partial answer, and name what is missing
doc_coverage < doc_coverage_floor → partial ⚠ floor is 0.0 by default: OFF
separation < separation_floor → weak do not answer; say what was searched
otherwise → grounded use it and cite it
answerable = (band ≠ none)
- All four numbers are rounded to 4 dp and clamped to
[0,1]before they are stored, so the band is computed from exactly what the caller is shown. stalelands inpartial, notweak— stale bytes are a nameable defect, which is whatpartialmeans. Aweakresult has nothing identifiably wrong; the ranking simply cannot choose.- Nothing above fetches, samples, calls a model or reads a clock. Every input is something ranking already produced, which is why L1, L3 and L4 are untouched.
Why separation is a two-point number, and what it is a cheap version of
The literature's version is NQC — the standard deviation of the top-k scores, normalised — which predicts query difficulty from the retrieval distribution alone, with no relevance judgments (Shtok, Kurland, Carmel, Raiber & Markovits, TOIS 2012). The idea goes back to the clarity score (Cronen-Townsend, Zhou & Croft, SIGIR 2002).
(top1 − top2)/top1 is the two-point form of the same intuition: it asks whether
the winner is decisively the winner. ⚠ And that is also its known failure
— a corpus of near-misses is perfectly decisive about its best near-miss, which is exactly the
case doc_coverage was added to report.
⚠ separation is ordinal, not a calibrated probability. Chow's
rule — the theory behind an optimal abstention threshold — assumes the latter. That gap is real
and the record states it rather than closing it.
02 · Confidence
Getting it out
In --json
"confidence": {
"band": "partial",
"answerable": true,
"coverage": 0.6127,
"separation": 0.3402,
"separation_floor": 0.1,
"doc_coverage": 0.4413,
"doc_coverage_floor": 0.0,
"support": 3,
"verified": "unverified",
"missing": ["mTLS"]
}
On a terminal — stderr only
confidence: partial - answer, but
say what is missing.
Not in this corpus: mTLS.
confidence: weak - the ranking cannot
separate the top results
(separation 0.04, floor 0.10).
confidence: none - nothing in the
index scored for this query.
bandandanswerableare written out rather than left derivable — a consumer re-implementing the rules would be a second copy of this policy, drifting from the day it was written.- The two
_floorfields are there for the opposite reason — not so you can re-derive the band, but so you can see it is not comparable across repos that tuned differently. See Tuning the floors. - On the CLI the block needs
--band(orband = truein.fux/output.toml); thefux_searchMCP result carries it always, because a tool call has no flags to pass. Absent means "not asked for" — it never means "not confident". - Silent at
grounded, on purpose. A note that fires on every healthy query is a note nobody reads by the second day. - Never on stdout:
findpipes bare paths and--jsonis a contract. ASCII only — a Windows console's default codepage crashes on a fancy dash rather than degrading. answerableis a boolean: an agent handed0.3uses it anyway and hedges in prose; an agent handedfalsehas nothing to hedge with.
02 · Confidence
Which parts are facts, and which is a guess
Facts — no threshold involved
none: nothing scored above zeropartial: a term matched nothing anywherepartial: the cited bytes changed
⚠ The one guess
separation_floor = 0.10is provisional and unmeasured- Registered as prediction R10
- Must not be cited as calibrated until that verdict is filed
- ⚠ And a repo can now move it — which changes who owns the guess, not whether it is one
- ⚠ It WAS deliberately not a
tune.tomlkey — that was reversed on 2026-08-28. The argument for the lock is still true: a consumer who lowers the floor until their answers readgroundedis tuning away the signal rather than the ranking, and the honest fix for a wrong floor is still to measure it once, for everyone. What changed is who decides — see Tuning the floors for the price of that. - ⚠
separationis ordinal, and Chow's rule — the theory behind an abstention threshold — assumes a calibrated probability. The record states the gap rather than closing it. - Confidence can never reach a score or an ordering. It is computed from
rank()'s output and handed to the caller; nothing downstream feeds back. - SR-CONFIDENCE is accepted (ratified 2026-08-27, amended 2026-08-28). What is still owed is R10 — the measurement, not the ratification.
02 · Confidence
Moving the two floors — and what that costs
Both cutoffs live in .fux/tune.toml. Neither can move a score or an
ordering: confidence is computed from the ranking and nothing feeds back.
[confidence]
separation_floor = 0.1 # the `grounded`/`weak` cutoff
doc_coverage_floor = 0.0 # 0.0 = the clause is OFF
⚠ separation_floor — cost unmeasured
- Lowering it does not make answers better. It makes fux quieter about not knowing.
- At
0.0, no answer is everweakagain. - The default
0.10is provisional and unmeasured — prediction R10, still owed. Setting it locally does not settle it.
doc_coverage_floor — cost measured
0.0is a ruling, not an unset value: the gate was built, measured, and switched off.- At
1.0— the only value that reads structural — 19 of 50 correct answers turnpartial. - The one decoy it could catch sits at
0.710, inside the real answers'0.401–1.000range.
grounded is tuning away the signal rather than the
ranking — and nothing mechanical catches that. It was opened because fux's
standing rule on knobs is state the cost, do not clamp: refuse what is broken or
duplicates a tool, warn about what is merely strong. Neither floor is broken at any legal value.
- 🔴 The band now travels with the floor that judged it. Every block carries
separation_flooranddoc_coverage_floor, so agroundedat0.02is distinguishable from one at0.10. Without that, exposing the knob would have made the band silently mean a different thing in every repo — worse than either the lock or the knob. - Read the floor before comparing two bands from different repositories. Comparing them without it is a comparison fux told you not to make.
fux ask --no-tunerecomputes the band at the engine defaults — the same "is it me or the config?" switch it already was for ranking.- ⚠ A measured run may never compare two arms with different floors. That is a pre-registered threshold moving inside a comparison, and it is the reopen trigger for this decision.
fux setupis write-if-missing, so an existing repo never gains this table on upgrade. An absent[confidence]means the engine floors — nothing breaks; what is lost is discoverability.
03 · How it is tested
Two questions, not one
- Does the code do what it says? → the suites.
- Is what it says any good? → the quality contract.
- A run can pass the first and fail the second. It has happened twice, and a human caught both.
uv sync --extra dev
uv run pytest -q tests # fast unit — hermetic, no network, no subprocess
uv run pytest -q tests_e2e # the package as a user: the real CLI via subprocess
03 · How it is tested
Determinism is asserted through the CLI, on three operating systems
- Prediction R1 — double-ingest must produce byte-identical shards.
tests_e2e/test_determinism.pyruns the realfux ingesttwice in a subprocess and compares SHA-256 per shard.- It runs on the CI matrix's Linux, macOS and Windows runners — so a green run is a genuine cross-platform assertion, not a same-machine self-comparison.
top values. A mutation test on a
real corpus showed that at top=5 the rarest query term already decides the answer —
so replacing the block bound with a constant zero still produced byte-identical
output. The bound only becomes load-bearing at larger top. A differential
that checked one value would have certified an unsound bound as proven.
Determinism defended in the small — newlines, randomness, sort order
- Newlines — the merge driver reads with universal-newline translation and
writes with
newline="\n", so a CRLF checkout on Windows merges to the same bytes as an LF checkout everywhere else. - Randomness was removed, not seeded — a fixed seed does not survive a Python version that reorders a set. A test asserts the absence of the import by parsing the AST.
- Sort order, not sequencing —
fetch_allsorts its results before returning, so completion order never reaches a committed byte. That is what makes a thread pool cheap to reason about. - No wall clock on the maintenance path — timestamps derive from
SOURCE_DATE_EPOCHor source mtime. The TTL fetch-cache is the only clock, and it lives in the gitignored plane.
03 · How it is tested
The guards that are not about code at all
A surprising share of the suite tests the project rather than the engine — the failure modes that hurt most here are documentary.
| Guard | Fails the build when… |
|---|---|
test_sr_freshness.py |
A commit changes a component a decision record owns without touching that record. The
escape hatch is a literal no SR affected in the commit message — a claim under
your name in git history. Also a commit-msg hook. |
test_sr_ownership.py |
A source component is owned by zero records or by two. |
test_archive_law.py |
A second archive/ directory appears, or a live document points into one. |
test_doc_registry.py · test_doc_links.py |
A maintained document has no freshness row, a row points at a missing file, or an internal link is dead. |
test_regression_runs.py |
A measured run is filed without its blind/informed
classification, or a verdict is written as an SR instead of a VERDICT.md. |
| Import fences | Anything under src/fux/ imports a network library. L4 is asserted, not
trusted. |
test_windows_console_safe.py |
A fancy dash reaches a Windows console's default codepage, where print()
crashes rather than degrading. |
03 · How it is tested
What a fux quality number means
- Fux measured carefully for months and never wrote down what it was measuring.
- Every number therefore carried an undeclared query distribution and an implicit cost model in which a fabricated citation and an honest decline score identically.
03 · How it is tested
The six calls that make a number mean something
recall@kis the headline — retrieval bounds the whole system, and it is the half fux fully controls. Compared at equal byte budget or not at all: recall bought with a larger window is not recall earned.nDCGandMRRare diagnostics — a reranker follows retrieval, and LLM attention over long context is U-shaped, so a decaying discount describes a consumer that does not exist.- The query prior is declared and versioned, and starts uniform. The fork was never uniform-vs-weighted — it was declared vs undeclared.
unanswerablequeries are inside the gate — accuracy-only scoring does not merely fail to notice a guess, it rewards one over an abstention.- Every verdict publishes a weight-stability interval — the range of
cover which the verdict does not change. - The judged series is pinned and never fused — it may inform a prediction, never adjudicate one.
The evidence behind each call — why these are not preferences
- Retrieval bounds the system — a generator cannot recover a passage that was never retrieved, and the retrieval divergence term dominates the error bound (RAGChecker, NeurIPS 2024).
- Accuracy-only scoring pays for fabrication — Nature (2026): evaluating for accuracy alone incentivises hallucination.
- A judge model drifts silently — identical evaluator re-runs measured zero coupling one month apart. Hence: pin model AND prompt AND version, never compare across judge versions.
- Only the cost ratio is identifiable — Chow's rule fixes the reject
threshold at
(C_r − C_c)/(C_e − C_c); absolute costs do not move the boundary. - The confidence-target form is what makes it arguable — it converts "what is a stale citation worth?" into "how sure should fux be before it cites?"
- Publishing a metric makes it a target (Goodhart) — but an unpublished rubric makes the headline unauditable, and for a tool whose pitch is an auditable supply chain that is the worse trade. The stability interval is what makes publication safe.
03 · How it is tested
Measurement discipline
Pre-register, then measure
Threshold, metric definitions and slice definitions are committed before a number exists. A pre-registered threshold may never move.
A negative is a success
A recorded FAIL that stops months of building is a good outcome, reported plainly. The pruned index died that way — permanently, not deferred.
Ambiguous goes to a human
A result between "clearly passes" and "clearly fails" is written up as ambiguous and handed over — never adjudicated by restating the threshold in looser words.
Blind or informed, always stated
Blind only if every artifact — corpus, prompts, config, and the analysis — was authored with no access to queries, judgments or prior scores. Anything else is informed.
- An informed run is reclassified, not banned: file it, cite it — but never compare it with a blind run and never use it to state a delta.
- An informed number is not an "upper bound"; label it "not a generalisation estimate."
03 · How it is tested
What is not verified
| Gap | Consequence |
|---|---|
recall@k is not computed today |
The headline metric of the quality contract has never been produced. It needs known-relevant sets annotated per query. |
The unanswerable class does not exist |
And it must be authored blind, or it contaminates the set it is meant to test. |
| ⚠ The ±2-query (4 pp) resolution floor is a placeholder | Every "no detected change" ruling currently rests on a number nobody measured. |
| The unit suite's last recorded green run | Ran under a harness-only shim backporting one stdlib module, because the build environment had Python 3.10 — so it tested 3.10, not the supported floor. |
| Two eval corpora are gone | acme and orbit were lost in a lab wipe along with their
generator, so part of the measurement work is blocked on inputs no record can supply. |
04 · Reference
Merge — machine planes should never conflict on adjacency
- A shard is a header line plus one JSON line per document, sorted by
id. - Two branches that each added documents produce two line sets whose union is the correct answer — a textual three-way merge cannot see that.
fux hooks --installwires a custom git merge driver:fux-merge-index %O %A %B.
Two design notes worth knowing — the failure mode, and the ancestor check
- A merge driver is the piece a user cannot debug when it goes wrong, so
its failure mode must be refuse and leave both sides. Its error message names the
offending ids and the actual fix:
fux ingest, which derives the index from the merged content rather than from either side's copy. - Why the ancestor check precedes
ver. Relying onveralone means a document whose version was not incremented — a hand repair, an external edit, an ingest edge case — reads as "same ver, different bytes" and is refused, even though one side provably did not touch it.
04 · Reference
One boundary, used three times
- Fux refuses to own network I/O, model calls, and third-party parsing libraries.
- Each is a file the consumer writes, committed in their repo, that fux loads by path and never rewrites.
- That is how
src/fux/stays stdlib-only (L1) and offline by default (L4).
04 · Reference
Fetcher — the contract
# .fux/fetchers/<name>.py
def fetch(url: str) -> tuple[bytes, str]:
"""The bytes the server sent, plus the Content-Type it declared.
May raise — that records the URL as skipped, never a crash."""
# optional
MAX_PARALLEL = 4 # how many threads fux may run against you
def connect() -> None: ... # called once before this fetcher's batch
def close() -> None: ... # called once after
def configure(config: dict) -> None: # [sources.url.config], verbatim
... # fux never reads a key inside this table
- Routing is declared, never detected. Nothing escalates from one fetcher to
another: a plain GET that returns a rendered shell returns a rendered shell, and a
human writes
fetch=cdpon that line. - Network only inside two fenced paths —
fux add <URL>(scoped to that one URL) andfux update. A plainfux ingestnever imports a fetcher. - Verify uses the ingest fetcher. A document fetched two ways is two documents.
- A bare
strreturn still works — read as already-prose, so older fetchers keep running.
The three-layer attribute resolution, and two hazards — thread safety, and a false staleness
- Three layers, same order for every attribute: the built-in default → the
source-wide
[sources.url]setting → the line. A line beats both, and only for its own URL.
cdp.py holds one WebSocket in a module global that every fetch reuses; two
threads writing frames onto it produce plausible documents attributed to the wrong
URLs. That lands in the committed index, passes every determinism check,
and is caught only by a human reading an answer.
sanitize. A one-character divergence between two copies would mark every URL
document permanently stale — a defect that presents as a working freshness feature.
04 · Reference
Decoder — bytes become Markdown, or become a queue item
# .fux/decoders/<name>.py (or a built-in of the same module name)
EXTENSIONS = (".html", ".htm") # required; lowercase, with the dot
def decode(raw: bytes, rel_path: str) -> str | None:
"""Markdown, or None when a model is needed to read it."""
WANTS_PATH = True # opt-in, for a library that insists on a file
def decode(path: Path, rel_path: str) -> str | None: ...
- Markdown, not flat text — headings are their own weighted field, so the heading syntax is the interface.
Noneis not an error — it means this needs a model to read it, and it is the signal the enrichment queue is built on.- A decoder never raises for malformed input — one corrupt file in a 10 000-document corpus must not stop the other 9 999.
- Adding a decoder must not change what is indexed — the default type allowlist is derived from built-ins only.
The nineteen built-ins, all stdlib-only:
csvdoc · docxdoc · drawiodoc · htmldoc ·
imagedoc · inidoc · ipynbdoc · jsondoc ·
jsonldoc · maildoc · odtdoc · pdfdoc ·
pptxdoc · rtfdoc · svgdoc · tomldoc ·
xlsxdoc · xmldoc · yamldoc.
The rules behind those choices — determinism, two failure reasons, one fence gap
- Two distinct "unreadable" reasons, never conflated. No decoder for
.xyzmeans someone could write one.pdfdoc: nothing readable in.pdfmeans only a model will help. The queue's whole value is that difference. - Determinism is the decoder's obligation, not the dispatcher's: same bytes
→ same string, byte for byte. Sort every iteration, never rely on
setorder, never read a clock. - The built-in list is an explicit tuple, not a directory scan. A directory listing is filesystem order, and a plane whose dispatch depends on filesystem order is a plane whose committed index depends on it too.
- Dropping a
logdoc.pyinto.fux/decoders/must not silently start walking every.login the repo — what counts as a document is a committed line a human wrote in.fux/formats.toml. - ⚠ The import fence cannot reach
.fux/decoders/. Stated rather than papered over: a consumer decoder's offline behaviour is a documented obligation, checked by reviewing a committed diff.
04 · Reference
CLI — setup and corpus
fux <verb> <subverb> does not exist
and is a standing constraint — fux graph path would have been the first subcommand
tree on this surface, so it became fux path.
| Command | Does | Flags |
|---|---|---|
fux setup |
Writes the consumer-owned files into .fux/ — write-if-missing. |
--no-agents |
fux doctor |
Install health, index health, URL health, and where the background re-index stands. | --json |
fux tune |
Prints the tunables file to paste into .fux/tune.toml. Reads and writes
nothing. | — |
fux add [entry] |
List a directory, a file or a URL — and ingest it. Omit the entry to list everything. | --types --cdp --http --plain --hashed --archived --no-ingest --no-fetch --dry-run |
fux remove <entry> |
Delete its line, or exclude it if an ancestor is listed. | --types --no-ingest --dry-run |
fux update [entry] |
Re-read what is already listed, re-fetching URLs. | --check |
fux ingest |
Walk the configured sources into the committed index. | --full --list-skipped --no-accelerator --stop |
fux build |
Rebuild the derived accelerator from the committed index. | progress flags |
04 · Reference
CLI — read, maintain, serve
| Command | Does | Flags |
|---|---|---|
fux ask <q> | Ranked documents, citations, confidence block. | --json --top N --explain --fast|--scan --no-tune |
fux find <q> | Ranked locations, one bare path per line. | --json --top N --fast|--scan --no-tune |
fux answer <q> | The single best answer — fetched and re-scored. | --json --no-refer --fast|--scan --no-tune |
fux explain <doc> | One document's outbound edges and community. | --json |
fux graph <q> | The neighbourhood around a query's best answers. | --json --fast|--scan --no-tune |
fux path <a> <b> | How two documents connect, most reliable route first. | --hops N --json --no-tune |
fux hooks | Install the git hooks and the index merge driver. | --install --status --uninstall --json |
fux daemon [start|stop|status] |
Run the URL freshness clock in the background. Positional, because the states are
mutually exclusive and --start --stop should not parse. |
--json |
fux enrich | Plan and validate what an agent skill generates.
No --model flag — fux never calls a model, so there is nothing
to fence. | --plan --check |
fux mcp | Serve the index over MCP on stdio, for coding agents. | — |
--scanis the default — the reference path, no build step needed. Kept as an explicit flag because it is what a bug report reproduces against.--fastuses the accelerator when one exists: same results, only faster.- Exit codes —
0ok ·1error ·2blocking (strict) ·130interrupted. - Errors are rendered only at the boundary; one
FuxError, no subclass hierarchy.
05 · Context
Running it as an agent
.venv/, an agent got command not found, concluded not
installed, and silently fell back to grep — while the engine sat there and
the committed index sat beside it. It did not error. It degraded, and the degradation read
exactly like an honest answer.
Where fux setup writes the agent files — and two Kiro traps
| Vendor | Files |
|---|---|
| Claude | .claude/skills/fux-usage/ ·
fux-archived-results/ · fux-enrich/ · fux-decoder/ |
| Kiro | .kiro/skills/fux-usage/ ·
.kiro/steering/fux-archived-results.md |
| Copilot | .github/agents/ · .github/instructions/ |
- Kiro implements the same open Agent Skills standard Claude does, so the usage skill is one template at two paths — agreement by construction rather than by a conformance test comparing two files that could drift.
- ⚠ Kiro CLI does not support steering inclusion modes, so every file in
.kiro/steering/enters every interaction — which is why the usage guidance ships as a skill. - ⚠ Kiro custom agents load neither skills nor steering by default: they
need explicit
skill:///file://entries inresources. Fux cannot write someone's agent config, so the skill says it. - Agents can also talk to the index directly —
fux mcpserves it over MCP on stdio, andfux_searchcarries the confidence block.
05 · Context
The eight laws
Not guidelines. Every record cites them by number and none restates them — a paraphrase drifts, and a drifted paraphrase in an accepted record reads as authority.
snapshot policy. The whole architecture rests on this.fux add <URL> and fux update.$0/stdlib is a
trivially auditable supply chain and no procurement. Offline / no-API means no data ever leaves
the tenant. Deterministic means compliance-grade reproducibility.
.fux/: index/,
sources/, fetchers/, decoders/, tune.toml and
.fuxignore are all committed; .fux/runtime/ is the only derived
directory under it. SR-LAWS decision 8 carries each pass and what it traded away, including the
gap the last one leaves open.
05 · Context
Decisions — made, and still open
0141_confidence.md has since been deleted, and
0043 is SR-LOCKS. Re-derive before quoting:
grep '^status:' records/*.md.
- The pruned index is dead, not deferred — a pre-registered gate closed FAIL. Full postings, permanently.
- The dense lane and its model were deleted — 0 fixed / 2 broken at every setting that fires. The model mean-pooled static vectors, so the lane was as order-blind as BM25F.
- A cross-encoder reranker was refused —
onnxruntimeis not byte-identical across architectures, which would break clone it and run the query. - The
post-commithook defers — an inline re-index cost 44 s on a 20-document commit, because cost tracks corpus size, not delta size. - Answer-time fetching is unconditional — and that single call withdrew two proposed flags outright.
- No local content store — copying the corpus blunts the wedge for two narrow wins.
What is still open — roughly two dozen forks, none an agent's to pick
| Area | The one that bites first |
|---|---|
| URL freshness | Whether the fetcher contract gains an optional validate() — gated on a
measurement that needs a real URL corpus. |
| What "right" means | recall@k is declared and uncomputed; the unanswerable class
does not exist yet and must be written blind. |
| Confidence calibration | SEPARATION_FLOOR is provisional (R10), and separation is
ordinal where the theory wants a calibrated probability. |
| The records themselves | A record whose premise died and now stands unargued — the one state a record should never be in. |
| Measurement discipline | The ±2-query resolution floor is assumed, not measured. |
| Naming, while it is free | The narrowed-refresh flag has no name yet — --dirty, --stale,
--changed. Free today, a deprecation cycle once anything ships against it. |
A metric chosen badly is wrong quietly for months. The live queue is
work/OPEN-WORK.md.
05 · Context
Where things live
What the project is
records/— the decision recordsdocs/GLOSSARY.md— every recurring termdocs/index.md— the OKF bundle root
What is happening to it
work/OPEN-WORK.md— the single live queuework/INTERVIEW.md— the state of playwork/WORKLOG.md— the append-only trail
The evidence
work/regression/— measured runs + verdictswork/compare/— live forkswork/paper/— the architecture of record
The code
src/fux/— every component owned by a recordtests/+tests_e2e/— both maintainedarchive/— the one archive; never evidence
Fux 2.0.0-alpha.2 · rank from a small committed index · fetch from the systems that own it · verify at answer time.