Handbook · index-and-refer

Fux

in plain termsFux makes everything you have written down searchable by an agent — without copying it anywhere, running a server, or calling a model.

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.

2.0.0-alpha.2 + unreleased fux.index.v2 analyzer v2 fux.runtime.v5 lexical only — the engine never calls a model

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

in plain termsFux writes a small file into your project recording which of your documents contain which words. To answer a question it uses that file to pick documents, then opens the real documents to get the actual text.

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 snapshot policy (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 answer re-opens it and quotes today's bytes
  • ask and find do not — they rank, and say so
Say "index", not "database" (L6). Load-bearing vocabulary: "database" makes people expect a thing that holds and serves the documents. A vector database is an index too — the difference is not the word, it is what this index contains and where it lives.

00 · Start here

Why Fux exists

in plain termsMost tools answer questions about your documents by building a second, opaque copy of them somewhere else. Fux keeps its notes in your repo as readable text, and re-reads the real document before it quotes one.

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.

Stated precisely, so the rest of this slide is arguable. A vector store keeps an embedding — a fixed-length array of numbers a model produced from a passage — with an id and some metadata, and finds neighbours in that number space. Storing the passage text beside it is optional in the database and near-universal in practice, because a retrieval-augmented-generation stack needs the words to put in the prompt. Everything below is about that stack, not about the data structure.

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.
What falls out of the bet. $0 by default · a stdlib-only runtime · offline unless a line asks for the network · byte-deterministic, so the same corpus and the same question produce the same bytes on any machine.
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 enrich exists 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

in plain termsTwelve words this handbook leans on. Nothing later assumes you already knew them — and you can come back here.
TermWhat 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.
The one distinction to carry forward. An index holds statistics and points at documents. A store holds the documents. Everything Fux does differently follows from being strictly the first.

00 · Start here

How it works — the whole thing in five beats

in plain termsRead every document once and write down statistics about it. Use those statistics to pick documents. Open the picked documents to get the words.

Every slide after this one is detail on one of these five.

  1. Ingest. Two committed text files — .fux/sources/dirs and .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.
  2. 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.
  3. 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.
  4. Query. ask · find · answer rank with BM25F over the five fields and hand back documents and locations. A confidence band travels with the result in --json and over MCP always; on a plain terminal it needs --band.
  5. 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.
The one-line version. Rank from a small committed index · fetch content from the systems that own it · verify at answer time. On one measured corpus — 8 870 RFC documents — warm 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

in plain termsThe same five steps as the last slide, drawn once. Blue boxes are files that go into git. Plain boxes are files you can delete and rebuild in seconds.
  • 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.
Fux — the mechanism 2.0.0-alpha.2 · fux.index.v2 / analyzer v2 / fux.runtime.v5 · lexical only, no model anywhere committed to git derived / disposable INGEST — WALK, FETCH, DECODE, ANALYSE, DISCARD .fux/sources/ dirs · urls · types one entry per line sorted — file order cannot leak fetch_all consumer fetcher, parallel min(declared, configured) absent = 1 · cdp declares 1 decode 19 built-ins → Markdown pdf · docx · pptx · xlsx … None → enrichment queue analyzer v2 split · lower · stopwords Porter stem · blake2b 16-hex order is load-bearing priors supersedes: · archived git commit recency no wall clock at write time THE COMMITTED PLANE · .fux/index/{00..ff}.jsonl 256 shards, doc-major, one canonical JSON line per document, sorted by id · the header pins schema + analyzer identity id · src · loc · sha · ver mode · meta retrieval terms{hash: [tf x5]} · flen[5] title · phrases priors & edges archived · superseded mtime · edges five weighted fields body 1.0 · heading 3.0 · title 2.0 path 1.5 · ctx 1.0 no prose · no floats · no clocks · no vectors — the dense lane and its model were deleted 2026-08-25 THE DERIVED PLANE · .fux/runtime/ — gitignored, rebuilt by `fux build` in seconds postings term-major, 128/block 62-byte offset table docs table loc · title · flen archived · superseded graph CSR + communities lazy PPR fetch-cache TTL-bounded the ONLY wall clock dirty list post-commit writes it refer writes it too url-state fail_streak · last_seen counted in RUNS The differential law — every result the accelerator returns is byte-identical to the reference scan. Delete the derived plane and you lose speed, never an answer. That is what makes it disposable. QUERY — EVERY CALL ask · find · answer explain · graph · path shares the ingest analyzer accelerator or scan bisect → block-max → decode or a byte prefilter over shards rank() — BM25F k1 1.2 · b 0.75 weight, then saturate — once confidence + rerank band computed from rank() proximity rerank: opt-in refer → answer fetch the live document, compare sha, cite path:L12-L40 sha differs → mark dirty → narrowed re-ingest fixes the ranking THE LOOP THAT CLOSES THE GAP refer used to compute the freshness verdict and throw it away — so a changed URL kept its old terms, stopped ranking, was never cited, and nothing noticed. It buys RECALL, not correctness: a changed document could never be mis-answered, only fail to surface. L1 stdlib only · L2 no durable content · L3 deterministic — the sort, not the loop · L4 offline by default · L5 hashed meta · L6 say "index" · L7 Python ≥ 3.11 · L8 no use record committed

01 · How Fux works

Ingest — five steps, and the content is thrown away

in plain termsThe reading stage. Fux opens each document, turns it into plain Markdown, counts the words, writes the counts down — and then forgets the document.
ONE PASS, FIVE STEPS 01 walk .fux/sources/ — three committed line files, deduped and sorted 02 fetch URLs only, through YOUR fetcher file fux ships no HTTP client 03 decode anything not prose becomes Markdown headings must survive 04 analyse terms + field lengths, five weighted fields same code as query time 05 write one canonical line into one of 256 shards byte-identical everywhere Then the text is GONE. Never written to .fux/, never cached, never committed — that is L2. It is why the index can live in a repo cloned by people whose document permissions differ from yours.
  • 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

in plain termsBefore counting, every word is cut to a standard form, so “Running” and “runs” count as the same thing. A word in a heading counts for three times as much as the same word in the body.
ANALYZER v2 — THE ORDER IS LOAD-BEARING, AND IT IS PINNED IN EVERY SHARD HEADER split unicode-aware lowercase NFC-normalised stopwords a fixed list Porter stem hand-rolled, stdlib blake2b 16 hex characters term hash the committed key The index stores the HASH of every term, never the word — so no shard can be read back as prose. It is not anonymous: see below.
BM25F — WEIGHT, THEN SATURATE. ONCE. heading 3.0 title 2.0 path 1.5 body 1.0 ctx 1.0 k1 = 1.2 · b = 0.75 never sum per-field BM25 — weight the term frequencies first, saturate the total.
Weights are tunable in .fux/tune.toml; these are the defaults.
"Hashed" is not "anonymous" — be precise about which. The terms are hashed, so a shard cannot be read back as prose. But the same line also carries the document's 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

in plain termsTwo folders. One goes into git and is the real thing. The other is a speed cache you can delete whenever you like.
COMMITTED · IN YOUR REPO .fux/index/{00..ff}.jsonl · 256 shards of canonical JSON, one line per document · diffable, mergeable, reviewable in a pull request · statistics only — no prose, no floats, no clocks · merged by a custom git driver, not by line adjacency THIS is the artifact. Everything else is rebuildable. DERIVED · GITIGNORED .fux/runtime/ · postings, docs table, graph, caches, dirty list · mmap'd binary segments, optimised for query speed · rebuilt by `fux build` in seconds · holds the only wall clock in the engine (the TTL cache) Delete it whenever you like. fux build rm -rf costs time THE DIFFERENTIAL LAW every result the accelerator returns is byte-identical to the reference scan, at every --top. That single assertion is what makes the derived plane disposable rather than load-bearing — and it is asserted by a test suite, not by intent.

01 · How Fux works

Line ranges come from answer, never from ask

in plain termsask 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.

your question analysed by the same pipeline the docs were ask · find — DOCUMENT GRANULARITY, NO NETWORK rank in the index offline, always returns: docs/mesh.md a document, a score, and a confidence block — no line numbers, ever answer — SPAN GRANULARITY, FETCHES EVERY CITED SOURCE rank in the index same scores fetch the source git dir · URL compare the sha chunk + re-score on the FETCHED bytes transient, never stored returns: docs/mesh.md:L10-L13 A line range can only be computed by chunking the FETCHED bytes. The index holds statistics, not text — it has nothing to count lines in. The split is L4 showing through the surface.
If you ran fux ask and expected :L12-L40, nothing is broken — it is the wrong verb for that question.
Why an index cannot produce a line number. Ingest counted the words and threw the text away, so nothing is left in the index to count lines in. A line range can only be computed from bytes, and the only place those bytes exist is the document itself — which is why the verb that returns line ranges is also the verb that goes and fetches.

01 · How Fux works

What a citation actually is

in plain termsA citation here is three things: a file, a line range, and a fingerprint of the exact bytes that were read — so you can prove nothing changed underneath it.
  • 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.
ANATOMY OF A CITATION docs/mesh.md :L10-L13 sha 516bef067812 current where the loc, as the source system addresses it which lines 1-based, inclusive — an agent can open the file here of what bytes hashed at fetch time, not at ingest time how sure current · stale · cached · unverified + ordinal line numbers move on a reflow
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
StepRuleWhy
SplitOn 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.
MergeSections under 120 bytes fold forward A two-line passage is a citation nobody can read in isolation.
Split againSections over 4 000 bytes split on paragraphs One 40 KB section would otherwise eat the whole byte budget by itself.
Addresspath: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

in plain termsFour honest answers to one question: did you actually look at the document just now?

The fourth exists precisely so the other three stay honest.

DID WE LOOK, AND WHAT DID WE SEE? current we looked just now, and it still matches the index → cite it plainly stale we looked, and the source changed since ingest → the ANSWER is right; the index is behind. Say so. cached we looked RECENTLY — served from a TTL cache → which is not "we looked just now" unverified we did not look, or could not reach it → never present it as confirmed Collapsing cached into current — "we did not look" into "we looked and it was fine" — is the exact failure the whole refer plane exists to prevent. A knob that lies about freshness is worse than no knob.

01 · How Fux works

Using it

in plain termsThe commands, roughly in the order you would type them the first time.

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 setup writes files you ownfux.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 --check is read-only: it reports drift, offline, without fetching.
Which read verb to reach for — six verbs, one table
CommandGives youReach for it when
fux askRanked results with score, loc, archived, confidence You want candidates and will judge them
fux findBare pathsYou are piping into something else
fux answerOne cited answer, re-scored on current bytes You want the answer, with a freshness verdict
fux explainEdges into and out of one document You are asking what something depends on
fux graphThe neighbourhood around the best answers You are orienting in unfamiliar territory
fux pathHow two documents connect You suspect a relationship and want the chain

02 · Confidence

How much the index believes its own answer

in plain termsEvery result carries a note saying how much Fux trusts it — so an agent can tell “this is the answer” from “this is the closest thing I could find.”
The problem, in one sentence. An agent handed a ranked list cannot tell "these three documents answer your question" from "these three are the closest thing in a corpus that does not discuss this at all." Both look identical: a score, a title, a citation.
  • 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 df statistics BM25F needed anyway, the scored list, and (on answer only) 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

in plain termsFive measurements go in. One of four words comes out: none, partial, weak, grounded.
The confidence block the ladder is read top to bottom, and the first true clause wins THE FIVE SIGNALS coverage · doc_coverage did the corpus — and the top doc — contain your words? idf-weighted separation · 0.0–1.0 (top1 − top2) / top1 — can the ranking tell first from second? support · int how many results scored above zero · bounded by --top verified · four-state current · stale · cached · unverified — from the refer plane THE BAND LADDER — FIRST TRUE CLAUSE WINS support == 0 nothing scored above zero at all. A fact, not a threshold. missing, or verified == stale a query term is nowhere in the corpus, or the cited bytes changed. Both facts. separation < separation_floor 0.10 — PROVISIONAL AND UNMEASURED the only number here that is not a fact (R10) otherwise every signal is clear: use the result and cite it. WHAT A CONSUMER DOES none answerable: false. Do not proceed — a refusal, not a low number. partial answer, and say what is missing. `missing` gives you the exact words. weak nothing is identifiably wrong and the ranking cannot choose. Report the search. grounded cite it plainly. The stderr note is silent here, on purpose. THREE of the four boundaries are FACTS. Only the grounded/weak cut needs a measured cutoff — and it does not have one yet. The block is computed from rank()'s output and handed to the caller. Nothing downstream feeds back, so the differential law is untouched.
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

in plain termsThe six numbers inside that note, and what each one is actually measuring.
  • missingread this first. It is the difference between hedging vaguely and saying "nothing here mentions mTLS".
  • coverage is idf-weighted — missing the is nothing; missing mTLS is the question.
  • doc_coverage is the same idf mass measured over the top-ranked document instead of the corpus. A question whose four words sit in four different documents scores coverage: 1.0 — correctly — and it is doc_coverage that says no single document contains the question. ⚠ It reports; it does not gate (see the floors).
  • separation is 1.0 when exactly one document scored — the strongest separation there is, not the weakest.
  • ask and find always report unverified"we did not look", never "it was fine".
The full field table, with the reasoning behind each shape
FieldAnswersWhy it is shaped that way
coverageDid 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_coverageDid 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.
separationCan the ranking tell first place from second? (top1 − top2) / top1, clamped to [0,1].
supportHow 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.
verifiedWere 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.
missingWhich 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

in plain termsThe exact arithmetic, written out. No model, no learned weights, nothing you cannot recompute by hand.

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.

One weight underneath all of it. Every coverage number is idf-weighted, using the same 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.
  • stale lands in partial, not weak — stale bytes are a nameable defect, which is what partial means. A weak result 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 plain termsWhere to find the confidence note — in JSON, on the terminal, and through MCP — and why it is silent when everything is fine.

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.
  • band and answerable are 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 _floor fields 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 (or band = true in .fux/output.toml); the fux_search MCP 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: find pipes bare paths and --json is a contract. ASCII only — a Windows console's default codepage crashes on a fancy dash rather than degrading.
  • answerable is a boolean: an agent handed 0.3 uses it anyway and hedges in prose; an agent handed false has nothing to hedge with.

02 · Confidence

Which parts are facts, and which is a guess

in plain termsThree of the four confidence rules are plain facts. One is a guessed cutoff, and it is labelled as a guess.

Facts — no threshold involved

  • none: nothing scored above zero
  • partial: a term matched nothing anywhere
  • partial: the cited bytes changed

⚠ The one guess

  • separation_floor = 0.10 is 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.toml key — that was reversed on 2026-08-28. The argument for the lock is still true: a consumer who lowers the floor until their answers read grounded is 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.
  • separation is 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

in plain termsYou can move the two cutoffs. Here is what each move costs — including the one whose cost has never been measured.

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 ever weak again.
  • The default 0.10 is provisional and unmeasured — prediction R10, still owed. Setting it locally does not settle it.

doc_coverage_floor — cost measured

  • 0.0 is 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 turn partial.
  • The one decoy it could catch sits at 0.710, inside the real answers' 0.401–1.000 range.
This reverses a decision, and the reversal has a price. Until 2026-08-28 the separation floor was deliberately not tunable, because a consumer who lowers it until their answers read 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_floor and doc_coverage_floor, so a grounded at 0.02 is distinguishable from one at 0.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-tune recomputes 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 setup is 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

in plain termsFour kinds of check, each able to catch a mistake the other three are blind to.
  • 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.
FOUR LAYERS — EACH BLIND TO SOMETHING THE NEXT ONE SEES LAYER 1 unit tests/ 105 test modules — a count at a moment, not a contract every codec, the analyzer, BM25F, chunking, confidence blind to: anything that only appears in a real process LAYER 2 end-to-end tests_e2e/ the real CLI in a subprocess against a fixture corpus exit codes, stdout/stderr split, the on-disk shapes users see blind to: whether the answer is a GOOD answer LAYER 3 differential tools/differential/ accelerator vs reference scan, byte for byte swept at top = 1, 5, 20, 50 — one value would not do blind to: the scan itself. It proves agreement, not truth LAYER 4 measured evidence work/regression/ report + ANALYSIS.md + raw evidence + a VERDICT.md judged against a threshold frozen BEFORE the number blind to: whatever the corpus does not contain What NO check can prove: the SR freshness gate proves a record was TOUCHED. It never reads the record. A record has been amended into self-contradiction in the same commit, the code implemented the wrong sentence, and CI stayed green the whole way.
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

in plain termsRun the same command twice over the same documents and you get the same bytes — asserted on Linux, macOS and Windows, through the real CLI.
  • Prediction R1 — double-ingest must produce byte-identical shards.
  • tests_e2e/test_determinism.py runs the real fux ingest twice 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.
Why the differential sweeps four 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 sequencingfetch_all sorts 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_EPOCH or 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

in plain termsSome tests check the documentation rather than the code, because the mistakes that hurt most on this project are written ones.

A surprising share of the suite tests the project rather than the engine — the failure modes that hurt most here are documentary.

GuardFails 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

in plain terms“How good is it?” means nothing until you say what you measured and what a wrong answer costs. Both are written down before any score exists.
  • 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.
The four-gate funnel one number per gate, never one blended score — each gate attributes failure to a different owner tools/quality/mix.toml @ version the declared query prior + the cost model — frozen the way a pre-registration is frozen reachable is it in the index at all? miss = an INGEST gap in window recall@k — THE HEADLINE a curve against context bytes; miss = a RANKING gap placed nDCG, MRR — diagnostics only miss = a RERANKER gap answered a judged series — model AND prompt AND version pinned NEVER fused into the headline reported BESIDE the scalar, never instead of it the risk–coverage curve (with AURC) + the weight-stability interval The cost of an error is published BEFORE any score exists: confidence target t = 0.75, so the penalty is c = t/(1−t) = 2. A correct answer +1 · an honest decline 0 · a wrong answer −2. Weights set after a score is seen are tuning, and a metric chosen to flatter is undetectable later.
A single blended score says something got worse and nothing about what. The funnel attributes the drop to an owner.

03 · How it is tested

The six calls that make a number mean something

in plain termsSix choices about how quality gets measured — and the published evidence behind each one.
  • recall@k is 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.
  • nDCG and MRR are 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.
  • unanswerable queries 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 c over 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 fabricationNature (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

in plain termsDecide the passing mark before you look at the score, and say whether whoever ran it had already seen the answers.

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.

Two runs passed their number and failed their claim. The pruning gate returned a 0.00 delta from a treatment that touched 0–2.5 % of documents — an aggregate delta of zero over an untreated population is not evidence. A later budget sweep was "satisfied by its letter and violated by its purpose." Neither was caught by a gate, because no gate knew what it was looking at.
  • 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

in plain termsThe things this project has not established. Listed on purpose, so nobody has to discover them later.
As of 2026-08-28, the end-to-end suite has not been seen green. It fails identically on a clean tree in the environment this release was built in — identical before and after, so the change introduces no regression. That is not the same claim as passing.
GapConsequence
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.
Why this section exists at all. Stating what a measurement did not establish is cheaper than discovering it later, and a project that hides its unverified edges teaches everyone to distrust the verified ones.

04 · Reference

Merge — machine planes should never conflict on adjacency

in plain termsIf two people each add documents on different branches, git would call that a conflict. A small program shipped with Fux merges them correctly instead.
  • 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 --install wires a custom git merge driver: fux-merge-index %O %A %B.
The merge driver, one document id at a time checked in this order — and its failure mode is REFUSE AND LEAVE BOTH SIDES, never silently pick one %O · the common ancestor may not exist — that is legal %A · ours git reads the RESULT back out of this file %B · theirs read only for each id in sorted(base ∪ ours ∪ theirs) ours == theirs they agree, or both deleted it → keep whatever is there not in the ancestor, present on one side a one-sided ADD → take it. The common case, and not a conflict not in the ancestor, added on BOTH sides, differently REFUSED in the ancestor, gone one side, untouched on the other a DELETE beats an unmodified side deleted here, modified there REFUSED one side is byte-identical to the ancestor the other side's bytes win — checked BEFORE `ver`, on purpose different `ver` the higher one wins — `ver` bumps when a document's own sha changes same `ver`, different bytes REFUSED — one side ingested content the other did not have Output is re-sorted by id, so two machines merging the same three inputs produce the same bytes. Without that, the driver would be a hole in L3 the size of every shared repo.
Green resolves, orange refuses. On refusal the driver writes ordinary conflict markers and exits non-zero — the thing a human already knows how to fix.
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 on ver alone 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

in plain termsThree jobs Fux refuses to do itself — going on the network, calling a model, and parsing exotic file formats. You write those as small files in your own repo, and Fux calls them.
  • 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).
fux owns the contract · you own the code loaded by path, called by signature, never rewritten by fux FUX — STDLIB ONLY, NEVER OPENS A SOCKET YOUR REPO — ANYTHING YOU INSTALLED .fux/sources/urls one URL per line, plus fetch= and meta= attributes routing is DECLARED, not sniffed load by path fetch=<name> resolves to <fetchers dir>/<name>.py unusable file → loud failure fetch(url) -> (bytes, content_type) .fux/fetchers/http.py the shipped default a plain GET .fux/fetchers/cdp.py a real browser, for pages declares MAX_PARALLEL = 1 dispatch by extension one extension → one decoder, documented precedence dispatch is deterministic 19 built-in decoders pdf · docx · pptx · xlsx · odt html · csv · json · yaml · toml … explicit tuple, never a dir scan decode(raw, rel_path) -> str | None .fux/decoders/<name>.py overrides the built-in of the SAME MODULE NAME — wholesale, not per-extension, so an override is a replacement rather than a race between two files claiming .html a missing dependency FAILS LOUDLY — a silent skip would split the index across machines Parallelism is DECLARED by the fetcher, never detected by fux: min(declared, configured), and 1 when absent. A decoder returning None means "this needs a model to read it" — an image, a scanned PDF. Not an error: it is the signal the enrichment queue is built on.

04 · Reference

Fetcher — the contract

in plain termsThe file you write so Fux can read a web page. Fux itself never opens a socket.
# .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=cdp on that line.
  • Network only inside two fenced pathsfux add <URL> (scoped to that one URL) and fux update. A plain fux ingest never imports a fetcher.
  • Verify uses the ingest fetcher. A document fetched two ways is two documents.
  • A bare str return 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.
Why parallelism is declared rather than detected. The shipped 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.
Normalization is shared, not copied. The verify path calls the ingest path's sanitize. A one-character divergence between two copies would mark every URL document permanently stale — a defect that presents as a working freshness feature.
Why a thread pool is safe at all: sequential fetching was never what made the index deterministic — the trailing sort is.

04 · Reference

Decoder — bytes become Markdown, or become a queue item

in plain termsThe file that turns a PDF, a spreadsheet or a web page into Markdown, so its words can be counted.
# .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.
  • None is 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 .xyz means someone could write one. pdfdoc: nothing readable in .pdf means 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 set order, 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.py into .fux/decoders/ must not silently start walking every .log in 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

in plain termsCommands for setting Fux up and for saying what it should index.
Every verb is flat. 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.
CommandDoesFlags
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

in plain termsCommands for reading the index, keeping it current, and serving it to a coding agent.
CommandDoesFlags
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 hooksInstall 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 enrichPlan and validate what an agent skill generates. No --model flag — fux never calls a model, so there is nothing to fence.--plan --check
fux mcpServe the index over MCP on stdio, for coding agents.
  • --scan is the default — the reference path, no build step needed. Kept as an explicit flag because it is what a bug report reproduces against.
  • --fast uses the accelerator when one exists: same results, only faster.
  • Exit codes0 ok · 1 error · 2 blocking (strict) · 130 interrupted.
  • Errors are rendered only at the boundary; one FuxError, no subclass hierarchy.

05 · Context

Running it as an agent

in plain termsHow a coding agent should find and run Fux — and why silently concluding “not installed” is the dangerous failure.
The defect this closes was live and silent. In any repo whose fux lives in an unactivated .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.
THE INVOCATION LADDER — PROBE EACH RUNG WITH --version fux --version a venv is active, or fux is global (pipx, uv tool) uv run fux --version a uv-managed repo ./.venv/bin/fux --version venv present, not active · Windows: .venv\Scripts\fux.exe python -m fux --version importable, no script installed not found ↓ not found ↓ not found ↓ THE RULES · first rung that answers wins — cache it for the session · NEVER `which` — it answers "is there a file", not "does it run" · NEVER activate a venv, modify PATH, or install anything — a test enforces it · if every rung fails: say WHICH were tried and that you fell back. Never "not installed"
That last sentence turns a silent degradation into a diagnosable failure.
Where fux setup writes the agent files — and two Kiro traps
VendorFiles
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 in resources. Fux cannot write someone's agent config, so the skill says it.
  • Agents can also talk to the index directly — fux mcp serves it over MCP on stdio, and fux_search carries the confidence block.

05 · Context

The eight laws

in plain termsEight rules the project will not break. Every design record cites one by number instead of restating it.

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.

L1$0, stdlib-only runtime. No third-party runtime dependency. The frontmatter parser and every codec are hand-rolled on purpose.
L2Content is never durable outside its source system. The index holds statistics, never content. The single exception is an explicit per-source snapshot policy. The whole architecture rests on this.
L3Deterministic — no model in the maintenance path. Same sources produce a byte-identical index. Nothing calls a model to build one.
L4Offline by default. Network access only inside explicit, fenced, opt-in paths — fux add <URL> and fux update.
L5Hashed meta is the default for non-git sources, enforced at write time. It closes an ACL-mismatch leak.
L6Say "index", not "db". Load-bearing vocabulary.
L7Python ≥ 3.11.
L8A use record is never committed. Fux may record what was asked and what was answered — in plaintext, with no law-level size bound — and may print a per-answer provenance receipt on stdout. Every durable trace of use lives on a gitignored path and never reaches a committed byte.
The laws are enterprise features, not constraints. $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.
L8 is the one law that moved, and it moved three times in a day. Written, reverted, then narrowed to commits alone (Arpit, 2026-08-27). Hashing, the size bound, the stdout prohibition and a transmission clause were all in earlier drafts and none survives — if you have read an older copy of this handbook, that is the paragraph it got wrong. Gitignored is the test, not .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.
L2 governs the corpus; L8 governs the record of who went looking in it. A query is not content, and no other law reached it.

05 · Context

Decisions — made, and still open

in plain termsWhat has been settled and will not be reopened, and what is still genuinely open.
Counts are facts about a moment. Checked against the tree on 2026-08-28: 47 live records — 46 accepted, 1 proposed (SR-LOCKS, which describes shipped behaviour and is waiting on ratification). SR-CONFIDENCE was ratified 2026-08-27 and amended 2026-08-28; the stale duplicate at 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 refusedonnxruntime is not byte-identical across architectures, which would break clone it and run the query.
  • The post-commit hook 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
AreaThe 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

in plain termsWhere to look for what.

What the project is

  • records/ — the decision records
  • docs/GLOSSARY.md — every recurring term
  • docs/index.md — the OKF bundle root

What is happening to it

  • work/OPEN-WORK.md — the single live queue
  • work/INTERVIEW.md — the state of play
  • work/WORKLOG.md — the append-only trail

The evidence

  • work/regression/ — measured runs + verdicts
  • work/compare/ — live forks
  • work/paper/ — the architecture of record

The code

  • src/fux/ — every component owned by a record
  • tests/ + tests_e2e/ — both maintained
  • archive/ — 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.

1 / 1

Presentation keys