{"path":"research/synthesis-build-plan.md","content":"# Inspectable Synthesis: The Build Plan, and Why It Is Mostly a Restoration\n\n*Aug 13, 2026. Founder question: \"Isn't this the same as my earlier truth graph ideas and synthesizing summaries based on that?\" Answer: **yes, and further along than either of us was treating it.** The truth graph is a shipped synthesis engine, the original design doc specified inspectable connection in those words, and the field that would deliver it was in the design sketch and is absent from the shipped model. So this is less a new feature than a regression to repair, and the repair is small because the inputs are already computed.*\n\n> **STATUS (2026-08-13, same day): Phases 1–3 are built. Phase 4 is deliberately held.** Sections 1–3 below are the archaeology and the diagnosis and stand as written. Section 4 is the plan as drafted, kept because it is what the build was measured against. **§7 records where the build departed from it**, including one bug the plan would not have caught and one place where following the plan literally would have been wrong. Not yet verified live: `POST /query` is auth-gated, so no cloud session can exercise it — the local smoke list is in `TODO.md` § Inspectable synthesis.\n\n---\n\n## 1. What the archaeology found\n\nSearched before proposing, across `docs/` and the code rather than from memory.\n\n**`deliberus/truth_graph.py` is 1,044 lines and is a synthesis engine.** It contains `QuerySynthesis`, `build_truth_graph_context`, `format_context_for_synthesis`, `synthesize_query_response`, `build_deliberation_promotion`, `_plain_graph_summary`, `_detect_gaps`, `_detect_contested_concepts`, `compute_subgraph_badges`, `detect_bridging_claims`, and a deterministic fallback synthesis. This is the \"summarizing from the truth graph\" idea, live in production, reached through the landing page's *Think with Deliberus* path.\n\n**The design doc already used the word.** `truth-graph-evidence-system.md` § Truth Graph As Read Layer sketches the response shape with a `claim_refs` array and then states the intent explicitly:\n\n> \"The important preserved detail is not the exact field names; it is that the answer should remain **inspectably connected** to supporting/challenging chains, bridging claims, gaps, contested concepts, and claim references.\"\n\nSo the founder's memory is not approximately right, it is exactly right, and the vocabulary was his first.\n\n**What shipped is narrower than what was designed.** `QuerySynthesis` carries five fields: `graph_status` (`graph_backed` / `mixed` / `background_only`), `graph_note`, `answer`, `provisional`, `confirmation_prompt`. Those deliver **epistemic provenance** — is this answer coming from the graph or from the model's background knowledge — which is a genuine and hard-won form of honesty, and it resolves the design doc's own Open Question 3. But there is no `claim_refs`, no supporting/challenging chains, and no gaps in the output object. The synthesis says *where its knowledge came from in general* and never *which claims it used in particular*.\n\n**And there is a mechanical reason the field vanished.** `format_context_for_synthesis` builds the prompt from `short_title`, `text`, badge term and strength, and evidence metadata. It never includes `claim_id`. **The model cannot cite claim identifiers because it is never shown any.** That is not an oversight in the response schema; it is a missing field in the prompt, which is a two-line fix.\n\n## 2. The precise gap\n\n| Inspectable-synthesis component | Status |\n|---|---|\n| Provenance: graph-backed vs background | **Shipped** — `graph_status` + `graph_note` |\n| Gaps / sorry markers surfaced | **Computed** in context (`_detect_gaps`), not in the output object |\n| Contested concepts surfaced | **Computed** in context, not in the output object |\n| Supporting / challenging partition | **Computed** in context (`supporting`, `challenging`), not in the output object |\n| Uncertainty per claim | **Computed** — QBAF badges per node |\n| **Which claims the answer used** | **Missing.** Designed as `claim_refs`, never shipped, and unbuildable until the prompt carries IDs |\n| **What the answer left out** | **Missing entirely, and never designed** — zero hits for \"omit\", \"omission\", \"left out\", \"excluded\" anywhere in the design doc |\n| Alternative syntheses | **Missing** |\n| Revision history | **Partial** — deliberation drafts keep turns |\n\nThe shape of the gap is worth stating plainly, because it is the difference between two kinds of honesty. **The truth graph answers \"where did this come from?\" It has never answered \"what did this leave out?\"** The first is provenance. The second is what makes flattening *measurable*, and it is the one the DeepMind steering result makes urgent.\n\n## 3. There are two omission classes, not one\n\nThis is the finding that makes the build concrete, and neither class is currently reported.\n\n**Class A — retrieval-level truncation.** `build_truth_graph_context` retrieves up to 6 / 10 / 16 claims by depth. Then `format_context_for_synthesis` truncates again, hard: `support_limit` is 3 / 5 / 8, `challenge_limit` is 2 / 3 / 5, `gap_limit` 2 / 4 / 5, `bridging_limit` 1 / 2 / 3. So at casual depth the model may see 3 supporting and 2 challenging claims out of a substantially larger retrieved set. **Claims are dropped before the model ever sees them, silently, by a depth budget.** The reader is given no signal that a budget was applied.\n\n**Class B — synthesis-level.** Of the claims the model *did* see, some are not used. Currently invisible because nothing records what was used.\n\nClass A is the more important one and it was the surprise. It means the honest omissions ledger is not only \"the model chose not to mention this\" but \"the system decided you did not have room for this\" — and the second is a design decision being made silently on the reader's behalf.\n\n**Both are computable as set differences over data the context builder already holds.** That is why this is cheap.\n\n## 4. The build plan\n\nPhased so each phase ships independently and the cheapest honesty arrives first.\n\n### Phase 1 — Make citation possible, then require it *(smallest, highest value)*\n\n1. Add `claim_id` to every claim line in `format_context_for_synthesis`, in a compact form the model can echo.\n2. Add `cited_claim_ids: list[str]` to `QuerySynthesis`, with prompt instruction to cite the claims the answer actually rests on. This is the restoration of the designed `claim_refs`.\n3. Validate against the context: any ID the model returns that was not in the shown set is a hallucinated citation and must be dropped and logged. **This is a new honesty check that did not previously exist and cannot be skipped** — an unvalidated citation field is worse than none, because it manufactures false auditability.\n\n### Phase 2 — The omissions ledger *(the load-bearing part)*\n\n4. Emit `omitted: {truncated_by_depth: [...], shown_but_unused: [...]}` computed as two set differences: retrieved-minus-shown (Class A) and shown-minus-cited (Class B).\n5. Surface a count in the user-facing answer and the full lists on the agent-readable and expert surfaces. The user-facing form should be one plain sentence, not a table — *\"three further claims were in scope and not used at this depth\"* with a link.\n6. Record the depth budget that produced the truncation, so Class A is attributable to a setting rather than looking like a judgment.\n\n### Phase 3 — The honesty instruments, pointed at the synthesis\n\n7. Run disagreement preservation against the synthesis, not only against extractions. The instrument already exists; the synthesis is currently outside its scope, which is exactly where flattening is most likely.\n8. Report the **support-to-attack ratio of the cited set against the retrieved set**. If the answer cites four supports and one attack from a set that held eight and six, the reader should see that. This is the incentive-asymmetry instrument from `incentives-analysis.md` applied where it is cheapest, because `context[\"supporting\"]` and `context[\"challenging\"]` are already partitioned.\n\n### Phase 4 — Alternatives, and the experiment\n\n9. `alternative_readings: list[str]` — short statements of syntheses the same subgraph would support. Deliberately last: it is the only phase requiring extra model work rather than bookkeeping over data already held, and it is the one most at risk of becoming decorative.\n10. Run the **three-arm experiment**: no synthesis, opaque synthesis, inspectable synthesis, scored on disagreement preservation and on whether readers can detect flattening. This is the differentiating prediction already in TODO, and Phases 1–3 are its prerequisites.\n\n## 5. What could go wrong\n\nRecorded now so it is not discovered later.\n\n- **A citations field invites citation-shaped confabulation.** Hence the Phase-1 validation gate as a hard requirement rather than a nicety.\n- **An omissions list is a rhetorical weapon**, the same way the completeness oracle is: *\"your answer omitted six claims and mine omitted one.\"* The count is not a quality signal and should not be displayed as one.\n- **Class A truncation may be load-bearing for readability.** The depth budgets exist because a casual answer with sixteen claims in it is not a casual answer. The fix is to *report* the truncation, not to remove it — resist the pull to raise the limits once they become visible. **↯ This watch-out was half wrong, and § 8 says why.** Its reasoning ties the input budget to the answer length, and those are separate caps.\n- **This adds output tokens per query.** Phases 1–3 are bookkeeping and cheap; Phase 4 is a second reasoning pass and should be measured before it is assumed affordable, per the evidence-gated spending rule.\n- **The deterministic fallback path** (`_fallback_query_synthesis`) must produce the same fields or the schema lies whenever the LLM is unavailable. Easy to forget; it is a real code path.\n\n## 6. Why now rather than earlier\n\nThree things arrived after the truth graph shipped, and together they turn a designed-but-dropped field into the most consequential piece of the system.\n\n**The steering result.** A Google DeepMind study (N=879, real money) found LLM facilitation shifted allocations by up to 5.5 percentage points while participants preferred the experience and consensus did not improve. Synthesis production is where steering lives.\n\n**Full-paper read, 2026-08-14** (closing the reading task that had been open in TODO; Parisi, Thain, Hallak, Tsai & Qian, FAccT '26, arXiv:2605.14097, DOI 10.1145/3805689.3812253). The abstract left two questions open and the answers cut in opposite directions.\n\n*How they measured consensus, and why their null result transfers less than it appears to.* The primary outcome is agreement between three participants' **numeric allocation vectors** (a Krippendorff-style α, reported as Δα). That is a different construct from anything this project cares about: it measures whether three people's numbers converged, not whether anyone understood why the others held their position. They also name a ceiling — post-discussion round-one median α was 91.1 with an interquartile range of 68.3 to 100 — so there was little room to improve. **Do not cite their consensus null as evidence about reasoning quality.** It is evidence that facilitation does not move a numeric-agreement metric that was already near its ceiling.\n\n*The exact form of the steering, and why it transfers completely.* Attributed in the paper's own qualitative analysis to the **summarization** agents specifically: facilitators restated participants' proposed splits, and restatement **normalizes outlier suggestions** in high-variance topics. That is paraphrase-flattening, named as a mechanism, occurring at the restatement step, and it is the mechanism the disagreement-preservation instrument was built to detect. Study 2's design sharpens it further, since it compared a summarization-centric facilitator against a principles-guided one against no facilitation, and the steering attaches to the summarizing role rather than to facilitation as such.\n\n**Consequence for the three-arm experiment below**: the control arm should not be \"no synthesis\" alone. The informative comparison is **restating synthesis** against **citing synthesis** — because their result says the danger lives in restatement that smooths outliers, and this build's answer is a synthesis that must point at the claims it rests on and declare what it dropped. That makes the prediction testable rather than merely differentiating: if citation-forced synthesis still normalizes outliers, the design answer fails on its own terms.\n\n**The cost argument.** A synthesis is the single largest reduction in traversal cost available (`lowering-the-cost.md`). Declining to synthesize is safe and charges every reader full price forever. Deliberus does not actually decline — it synthesizes at `/query` — so the choice is not whether to take the cost reduction but whether to take it inspectably.\n\n**The confession principle.** A subsystem with no way to report failure will report success. The synthesis path currently reports its provenance and cannot report its omissions, which is the definition of a channel that can only flatter.\n\nStated in one line: **a flattening synthesis is defined by what it dropped, so a synthesis that must enumerate its own drops has made its flattening measurable instead of invisible.**\n\n## 7. What the build changed about the plan\n\nFive departures, recorded because a plan that is never corrected by its own execution was not a real plan.\n\n**The accounting moved out of the LLM's reach entirely.** The plan wrote `omitted: {...}` as though it were another output field. Building it made the split obvious: deciding *which claims an answer rests on* is a semantic judgment only the model can make, and *what the budget dropped* is arithmetic the model should never be asked to perform on itself. So `cited_claim_ids` is on the response schema and everything else is in a separate pure module (`deliberus/synthesis_ledger.py`) with no pydantic, no graph, and no LLM. Two consequences, one intended and one not: an LLM is never invited to grade its own omissions, and — unplanned — the accounting became unit-testable in an environment that cannot import the project's own dependencies, so the logic is covered by 40 tests that actually ran rather than by static reading alone.\n\n**Phase 3's first item was wrong as written, and the correction is the build's main conceptual finding.** The plan said *\"run disagreement preservation against the synthesis, not only against extractions.\"* The instrument measures paraphrase-flattening: two claims that attack each other embedded as near-identical, meaning extraction homogenized them. **A synthesis does not flatten that way. It flattens by dropping one side of a conflict.** Pointing the embedding instrument at the synthesis would have produced a number that looked like coverage and measured nothing about selection. So Phase 3 shipped a new instrument instead — `conflict_coverage`, which walks the ATTACKS and QUALIFIES edges inside the retrieved subgraph and asks, for each, whether the answer cited both endpoints, one, or neither. It also splits one-sided conflicts by cause, which the plan did not anticipate: an endpoint the depth budget never showed is a Class-A failure and an endpoint the model saw and discarded is a Class-B failure, and conflating them would blame the model for a setting.\n\n**The plan's own Class-A insight nearly got implemented as a flattering bug.** The first version computed retrieved-minus-shown by claim id. A claim carrying no id then compared equal to every other id-less claim, so a set of them reported *zero* truncation. It surfaced because an existing test fixture happened to omit ids. The budget truncates by list *position*, so position is the correct identity, and the fix is one line. Worth recording as more than a bug: **an omissions ledger that can under-count is precisely the channel-that-can-only-flatter this whole build exists to eliminate**, and it had that property for its first twenty minutes.\n\n**Two absences of citation are not the same absence, and the plan treated them as one.** Class B is \"shown and not cited\", which is only measurable when citations were reported. When none come back, listing everything shown as unused would be an accusation manufactured from missing evidence. The ledger therefore reports `class_b_status: unreported` rather than a number. The same distinction recurs one level down: a shown claim carrying no id *could not* have been cited, so its absence from the citations proves nothing, and it is counted as `unaccountable_shown` rather than as unused.\n\n**Three fixes surfaced that were not in the plan at all**, all on this endpoint and all instances of the confession principle rather than of anything new.\n\n- `synthesize_query_response` swallowed its LLM exception and fell through to the deterministic fallback with no log. Every answer could silently degrade to a template while the endpoint returned 200 and the response looked well-formed. That is the exact shape of the Apr 2026 extraction outage — fifteen days, eight of ten users, one `except Exception` — and CLAUDE.md already forbids it in this codebase by name.\n- The fallback was indistinguishable from a real synthesis in the response body. A reader could not tell that the prose came from a deterministic template because the model was unreachable. The ledger now records `mode`, and in fallback mode the reader-facing note says so in plain words. **A subsystem with no way to report failure will report success**, and this one had been reporting success since it shipped.\n- **A failed graph lookup was reported to the reader as a thin corpus.** When `build_truth_graph_context` raised, the endpoint logged a warning and substituted a `has_data: False` context — which is the same shape as a query the graph genuinely has nothing for. The answer then said *\"Deliberus does not have enough on this yet.\"* That sentence is a claim about the corpus, and a failed lookup gives no licence to make it: the reader walks away believing the graph is emptier than it is, which is the flattering direction again. The context now carries `retrieval_failed`, the prompt instructs the model to say something went wrong on Deliberus's side and explicitly not to imply the material is absent, the deterministic prose says the same, and the ledger reports it. The failure note outranks the fallback note, because it is the one that would otherwise mislead the reader about the corpus rather than about a single answer.\n\nThe third is the most interesting of the three, because nothing was broken. The code caught its exception, logged it, and degraded gracefully — textbook defensive handling. The bug was entirely in what the *user* was told, which is a place error handling is rarely audited.\n\nOne design rule from outside this thread earned its keep immediately.\n\n## 8. The truncation was too heavy, and the reason is a conflation in § 5\n\n*Founder, on reading the shipped ledger: \"seems to me synthesis generation is way too heavily truncated?\" He is right, and the interesting part is that § 5 of this document — written the same morning — contains the mistake that kept it that way.*\n\nThere are two caps on this path and they had been treated as one.\n\n| | What it limits | Was | Now |\n|---|---|---|---|\n| **Input budget** (`DEPTH_DISPLAY_LIMITS`) | How many claims the model gets to read | 3+2 / 5+3 / 8+5 | 3+2 / 8+5 / 16+12 |\n| **Output budget** (`DEPTH_ANSWER_TARGET`) | How long the answer may be | flat 520 chars at every depth | 420 / 1200 / 2400 |\n\n**The output cap was the more damaging of the two, and it was invisible because it lived in a schema.** `QuerySynthesis.answer` carried `max_length=520` — roughly eighty words — at *every* depth, while the prompt in the same file promised that \"curious means short structured paragraphs\" and \"expert may include QBAF/evidence language.\" Neither fits in eighty words. So the depth ladder was a promise the schema could not keep, and a reader who asked for expert depth received a casual-length answer with expert vocabulary in it. This is the same failure as the frontier page's stated contract that nothing satisfied: **a limit that contradicts a stated intent is a doc lying in code form**, and it survived because a `max_length` in a model definition is not read as a product decision.\n\n**§ 5's watch-out then defended the input budget with the output budget's reasoning.** \"A casual answer with sixteen claims in it is not a casual answer\" is true, and it is an argument about answer *length* — which the 520-character cap already enforced independently. Showing the model more claims never made the reply longer. It only made the reply better informed. Tying the two together bought no brevity and cost real quality, and the argument sounded right because both numbers point in the same direction at casual depth.\n\nWhat survives from the watch-out: **casual stays at 3 + 2 deliberately.** It is the newcomer surface, a one-to-three-sentence answer genuinely cannot carry more, and cognitive lightness is the point. What does not survive: the same argument applied to expert, where the answer now has room for 2,400 characters and the model was reading five claims out of a materially larger retrieved set.\n\nTwo smaller repairs came with it. The length constants were duplicated as bare literals inside the models (`max_length=520` beside `QUERY_SYNTHESIS_ANSWER_MAX = 520`), which is the same single-source-of-truth failure the depth budget had — they now live above the models and are referenced. And the frontend rendered the answer as one text node, so a multi-paragraph reply would have arrived as a wall; it now splits on blank lines.\n\n**The new numbers are wise defaults, not measured optima**, and they are instrumented rather than asserted: the ledger records `answer.chars` against `answer.depth_target` and flags an overshoot beyond 25%. Overshoot is recorded and never trimmed — cutting prose mid-sentence to hit a number is worse for the reader than a slightly long answer, and a recorded overshoot lets the target be tuned from observation. What a live run answers that no reading can: whether the model actually respects a stated character target, and whether an expert answer built from twenty-eight claims is better or merely longer.\n\n The curiosity research established that a gap shown without a move offered produces anxiety-driven avoidance rather than curiosity, giving the rule *no gap displayed without a move offered*. The omissions note is a displayed gap, so it always names something the reader can do — asking for more depth at casual and curious, opening the related pages at expert. A test asserts the sentence never frames a ratio, because *\"one of six used\"* is a score wearing a disclosure's clothes.\n\n## 9. Opening synthesis to agents — artifacts rather than an endpoint\n\n*Founder, after the agent-surface audit: \"If synthesis is limited to claims with any meat on their bones and cached intelligently then maybe we don't need to have it be human only accessible?\" Yes — and pushing on those two gates moves the answer off the endpoint entirely. Approved for build 2026-08-13.*\n\n**Why `POST /query` is the wrong door to open.** A free-text query is an unbounded input, and unbounded input is what makes an open endpoint an LLM proxy, a prompt-injection surface, and an uncappable spend faucet. The meat gate cannot fix that, because *performing* the gate costs something: relevance retrieval hits Darwin's embedder, which is single-slot with no `--parallel` and serialises every consumer, so an anonymous flood would starve extraction without spending a cent. The existing `10/minute` limit is per-IP and therefore decoration against anything distributed. There is also a money-specific reason: the Gemini key is currently shared with brf-auto, a client-facing project, and unpaid API credit is a *designed-for* operating state here — an anonymous faucet drains a revenue project's balance.\n\n**But \"meat\" is a property of a subject, not of a query, and subjects are enumerable.** Measured against production: **23 sources**, 21 of them carrying 35–166 claims and every one carrying 2–4 contested concepts, plus **82 concepts of which 68 have `usage_count` ≥ 3**. That is **91 synthesis targets**, or ~273 across all three depths, which at Flash rates is roughly **$0.30–$0.60 for the entire corpus**. A batch the founder initiates, not a faucet a stranger turns.\n\nWhich is the actual answer to the question: **agents do not need authenticating if there is nothing for them to spend.**\n\n### The design\n\n**Synthesis artifacts on the surface that already exists.** A per-source and per-concept synthesis rendered into `/extraction/{id}.md`, `/concept/{term}.md` and their JSON twins. Meat is guaranteed by construction. The artifact sits directly above the full claim list on the same page, so the descent is a scroll rather than a second request, and an agent fetching all three depths sees the resolution ladder explicitly — which is what `ux-principles.md` P6 has been asking for.\n\n**Fingerprint invalidation is not optional.** \"Cached intelligently\" is doing more work than it looks. There is no synthesis cache today, and a naive one would make the honesty *worse*: a synthesis served against a graph that has since moved is the docs-telling-lies failure, at runtime, on the public surface. The cache key carries a **subgraph fingerprint** — the retrieved claim ids with their strengths, plus the depth — so an artifact self-invalidates when its own premises change. Incidentally this is the first artifact in the system that can go stale by construction, a small down-payment on the temporal rung the frontier records as entirely unbuilt.\n\n**`published` defaults to false.** Generation and publication are separate steps. This is what makes the build safe to ship before any review, and it implements the staging below structurally rather than by promise.\n\n**The meat gate is a parameter, not an architecture.** First cut: a source needs claims and at least one conflict edge; a concept needs `usage_count` ≥ 3. Synthesising a source with zero attack edges is a summary, and summaries are commodity — run 5's referee report had zero attacks and nothing to weigh. The batch reports what it skipped and why, so the threshold is tuned from observation.\n\n### One defect the design did not anticipate: a third omission class\n\nBuilding it surfaced a truncation earlier than either omission class. `expand_subgraph` caps expansion at **twenty seeds**, which is right for a live query where a handful of relevance-ranked claims is the point — and wrong for an artifact seeded from an entire source. A 166-claim source would have been synthesized from twelve percent of itself.\n\n**And nothing downstream could have reported it.** Class A is retrieved-minus-shown and Class B is shown-minus-cited; seed truncation happens before the prompt exists, so both are blind to it. The ledger built to make omission visible had a hole above it.\n\nFixed by making the cap a parameter (20 live, 60 for artifacts) and by putting `seeds_available` against `seeds_used` on the context, so the quantity is stated rather than silent. The general lesson is worth more than the fix: **an omissions ledger is only complete up to the earliest truncation it can see**, and a pipeline can always grow a new stage above it.\n\n### Deliberately not built\n\n**The `GET /query` read path.** The storage supports `kind='query'`, and the read endpoint is one small step away, but it is blocked on a genuine fork rather than on effort: **whose query becomes a public URL?** Human queries already live owner-scoped in `deliberation_drafts`, and auto-publishing them would turn a private first-take into a crawlable artifact. Promotion has to be an explicit owner action, which also means the surface would be empty at launch. Worth building when there is evidence agents want the cross-cutting case that per-source artifacts cannot cover.\n\n### The permanence objection was overstated, and the answer was already in the lifecycle model\n\n*Founder, on reading the objection below: \"I believe at least the specs for the truth graph contains lots about this and aren't there already caveats woven in to what's built?\" Correct on both counts, and checking it found three places where this build had under-applied the project's own doctrine plus one false claim about it.*\n\nThe spec does carry the answer. § Truth Graph As Read Layer specifies `lifecycle: raw, draft, candidate, canonical, superseded`, per-stage retrieval weights (implemented in `graph/maturity.py`), a `supersedes` lineage relation, and the principle that *\"meaning-bearing revisions should preserve lineage rather than silently overwrite history.\"* The reader-facing caveats also exist: the agent preamble already says presence is not endorsement and that the metadata describes the state of scrutiny rather than the truth of the claim.\n\nSo permanence is not an unanswered problem. **It is an answered problem this build had not yet applied**, in three specific ways:\n\n1. **The artifact had no lifecycle stage.** Only a `published` boolean — making it the single publishable object in the system with no lifecycle at all, in a project whose doctrine is one graph with staged maturity. Now `draft` on generation, `canonical` when a human publishes, `superseded` when the sweep finds drift, plus `revision` and `previous_fingerprint` for lineage. The vocabulary is the existing one on purpose: an artifact does not get a private taxonomy.\n\n2. **The reader was told what the reading *is* but not that it *moves*.** The preamble covered machine-written, depth, date, not-an-editorial-position, and material-listed. It said nothing about revisability. It now says the reading is a snapshot rather than a verdict, that it does not update itself, and — the line that does the real work — that **where the reading and the live claim list disagree, the claims win.** That single sentence is what stops a stable URL from functioning as a settled position, and it is a statement about authority rather than about confidence.\n\n3. **A claim in CLAUDE.md about this path was false, and I wrote it.** It said *\"the read path checks the fingerprint so a stale reading is never served as current.\"* It does not. `_published_artifact` fetched by key and returned. Worse, the claim described something that *should not* be built: recomputing a fingerprint per page view rebuilds the subgraph on every read and defeats the entire point of a pre-generated artifact.\n\nThe corrected design splits staleness in two, and says what each half misses. The request path runs a claim-count comparison against a number the page already holds — zero extra queries, catches claims appearing or disappearing, **blind to a strength moving**. The full fingerprint recheck lives in `generate_syntheses.py check`, a sweep that marks drift as `superseded` and deliberately does *not* unpublish, since taking a reading down without a human deciding to is its own silent action. Both facts reach the reader.\n\nTwo things remain genuinely open rather than solved. **Regeneration overwrites the earlier prose** — the revision counter and previous fingerprint survive, the words do not, which is a real gap against the spec's preserve-lineage principle and matters precisely because a citation made against revision 2 now resolves to revision 3. And the residual exposure is smaller than first stated but not zero: a labelled, dated, snapshot-flagged, citation-carrying reading that is *wrong* is still wrong at a stable URL. The review gate is the answer to that, and it is a human reading the prose, not a caveat.\n\n### The objection as first stated, kept because the correction is the content\n\n**A published synthesis is permanent, citable and crawlable prose about contested topics under the project's name.** Today `/query` output is transient and draft-scoped. Publish it and a subtly flattening reading of the death-penalty source becomes a stable URL that outlives any correction, because caches and archives make publication one-way. The preamble, the citations and the ledger make it defensible; they do not make it small. Hence the staging: **generate the 23 source syntheses at *curious* depth, read them, then publish.** Twenty-three is a reviewable number and that review is the best flattening test available — better than any instrument, because the founder knows these sources.\n\n**And the deeper one, which is the project's own threat model:** agents are the readers most likely to treat a synthesis as final and least likely to descend. If they consume syntheses and never fetch a cited claim, this has built the conclusions layer the project exists to complement. That is measurable — **a descent rate**: does agent traffic that fetches a synthesis subsequently fetch anything the synthesis cited? It would be the first outcome-ish measure in the system, and it holds the bet accountable instead of assuming it.\n\n---\n\n**See also**: [truth-graph-evidence-system.md](truth-graph-evidence-system.md) (the original design, including the `claim_refs` sketch and the \"inspectably connected\" intent) · [lowering-the-cost.md](lowering-the-cost.md) · [incentives-analysis.md](incentives-analysis.md) · [dogfood-run-4-agent-surface.md](dogfood-run-4-agent-surface.md) · `sources/chatgpt-research-thread-2026-08.md` (the inspectable-synthesis passage) · `sources/chatgpt-literature-dump-2026-08.md` (the N=879 result)\n"}