How throughline was used · a worked example · 15 September 2026

An Essay on a Throughline

The technical account

Five graphs in one repository, six sources composed by the rendering, 153 stamped links, a forty-line renderer with its own graph, and three tool releases in one evening.

Two pagesThis page is for a reader who wants the machinery. The account for a general reader tells what happened, who did what and what it cost.
5graphs: two shaping passes, an argument, a rendering and a renderer
157items across the five, all ratified by the end of the evening
6sources composed by the rendering: the argument, two private style graphs, three public axes
153links, every one stamped with its target's fingerprint
3tool releases the same evening, each from a defect the work found

The tool, and the contract

Throughline is a Git-native requirements tool: one small YAML file per item, a permanent UID, typed links, and a validator that fails the build on an orphan, a dangling link or an unaccepted item. tl-compose lets one graph borrow another's items by tag or by path. The first example's technical page sets out the working contract in full; it held here unchanged. The AI authors every item as proposed; a human ratifies; every commit names an item; every document is generated and gated; a claim about the world is a link to a source that was read.

proposedborn from the AI, counts for nothing
ratifieda human's name and a fingerprint of the words
stalethe words moved after the signature; re-ratify or revert

Five graphs, and what they compose

the essay's five graphs, and what they compose composes by path composes by path expresses ×38, cites ×10 satisfies ×38, relates ×1 satisfies ×17 satisfies ×18 satisfies ×9 satisfies ×22 renders its document argument shape · 26 why these registers argument · 51 claims, evidence, objections blog shape · 27 why these registers blog rendering · 36 paragraphs, front matter renderer · 16 tools/prose, own tests tone of voice private · v2019-09 house conventions private · v2019-09 purpose: persuade public · v2026-07 audience: general public · v2026-07 medium: web public · v2026-07 the essay's five graphs, and what they compose
The five graphs and what they compose. Solid boxes are graphs in the repository; dashed boxes are sources composed by tag, two private and three from the public catalogue. Arrows are composition and the links that cross between graphs, with counts. Generated from the graphs' own configuration and dumps.

Everything lives in one repository under one idd/ directory, one graph per subdirectory. Two shaping graphs record why the other graphs have the registers they have. The argument holds what the essay claims. The rendering holds the paragraphs and composes the argument by path, two of the company's own style graphs by tag, and three public writing axes from the catalogue by tag. The renderer is the small program that turns the rendering into a Word file, and has a graph of its own. Every link that crosses a graph boundary is stamped, so a change on either side of the seam is visible on the other.

Shaping, twice

shape-argument: registers and the links between them derives_from ×2 derives_from ×1 derives_from ×6 derives_from ×5 derives_from ×6 derives_from ×1 derives_from ×5 CON · 1 constraint DEC · 7 decision DOM · 1 domain NEED · 6 need NG · 6 non goal SRC · 5 source shape-argument: registers and the links between them
The shaping graph for the argument: sources read, a domain reading, the needs the work has of its graph, one decision per register created, and one non-goal per register considered and rejected.
shape-blog: registers and the links between them derives_from ×1 derives_from ×1 derives_from ×5 derives_from ×8 derives_from ×6 derives_from ×1 derives_from ×5 CON · 1 constraint DEC · 5 decision DOM · 1 domain NEED · 6 need NG · 6 non goal SRC · 8 source shape-blog: registers and the links between them
The shaping graph for the rendering: the same registers, one pass later, once the argument existed to be rendered.

Nothing was written into a working graph until a discovery pass had recorded why it would be shaped as it was. That pass is itself a small graph with five registers: the sources read, a one-paragraph reading of the domain, the needs the work has of its graph, one decision per register created, and one non-goal per register considered and rejected. It ran twice because the work turned out to be two things.

The argument's pass rejected three registers with reasons recorded: tests, because a claim is verified by evidence, not by a procedure; requirements, because nothing here is to be built; and paragraphs, because a paragraph's wording depends on voice, reader and channel while the claim does not. That last rejection is the design decision the whole example rests on: paragraphs belong one layer down, in a rendering that composes the argument.

The argument graph

argument: registers and the links between them assumes ×3 derives_from ×32 supported_by ×18 derives_from ×6 answers ×6 ASM · 3 assumption CLM · 23 claim EVD · 15 evidence INT · 1 intent NG · 5 non goal OBJ · 4 objection argument: registers and the links between them
The argument graph: its six registers and the links between them, roots on the right. Every claim derives from the intent or another claim; empirical claims are supported by evidence; objections are answered.
INT · intent1 thesis, the root the argument serves
CLM · claim23 claims, each typed by attribute: empirical, definitional, inferential or normative
EVD · evidence15 evidence items, each a root: citation, URL, the day it was read, a verbatim quote
ASM · assumption3 assumptions the argument rests on and a rendering may choose to state
OBJ · objection4 objections in the sceptical reader's voice, a delivery root: one nothing answers is an error
NG · non_goal5 things the essay deliberately does not argue

Two schema rules did the work a reviewer would normally do. A coverage rule at error severity says an empirical claim with no supported_by link to evidence fails the check; that is what found three facts the four human drafts had carried, because a claim could not be typed empirical without a link and a link could not be made without reading. And objections are declared delivery roots, so an objection with no answers link into it is reported unserved; that turned the persuade axis's rule about acknowledging the strongest objection into structure, and gave the fifth draft one new paragraph and two new sentences.

Where the evidence supported less than the draft claimed, the claim was retyped rather than the evidence stretched: "each was feared at the time" became inferential and hangs on an assumption; "does not think, assess or choose" now rests on a stated definition of thinking.

The rendering graph

blog: which registers link to which INT · 1 intent SEC · 6 section argument (source) 48 links in audience (source) 9 links in conventions (source) 17 links in medium (source) 22 links in purpose (source) 18 links in voice (source) 39 links in EL · 6 element derives_from ×6 expresses ×5 satisfies ×5 satisfies ×4 satisfies ×2 satisfies ×5 INT · 1 intent satisfies ×6 satisfies ×4 relates ×1 satisfies ×3 PAR · 23 paragraph derives_from ×1 derives_from ×22 cites ×10 expresses ×33 satisfies ×9 satisfies ×6 satisfies ×8 satisfies ×16 satisfies ×30 SEC · 6 section derives_from ×6 satisfies ×6 blog: which registers link to which · rows link to columns
Scrolls sideways.The rendering graph, as a matrix because its 19 kinds of link would not fit as arrows: rows are its registers, columns what they link to, including the six sources. Every paragraph expresses claims, cites evidence and satisfies borrowed rules.
INT · intent1 intent carrying the word budget, the reader and the channel
SEC · section6 sections, rendered as headings
PAR · paragraph23 paragraphs whose text is the publishable paragraph
EL · element6 elements: title, subtitle, teaser, byline, sign-off, and the social post

Those are the counts on the evening. The essay has since been through a rework: it stands at four sections and twenty-eight paragraphs, with a fifth register of thirteen version records, each naming a commit and the sha256 of the file at it. The measurements further down this page are of that later document.

The rendering composes six graphs. The argument, by path. The company's tone-of-voice and house-conventions graphs, private, pinned at a tag minted that evening for the edition of the style guide they encode. And three public, vendor-neutral axes from the catalogue: writing to persuade, writing for a general reader, and writing for the web.

Each paragraph carries three kinds of link. expresses to the claims it carries; cites to the evidence it names in its text; satisfies to the borrowed rules it applies. A coverage rule makes a paragraph that expresses nothing an error, and the title and teaser elements must express claims too, which is how the fourth draft's teaser was found to contradict the essay's first section. The house rules caught an italic sign-off, "a assessment" and an unexpanded abbreviation the same way: a satisfies link that could not honestly be made until the text changed.

Two knowing deviations were recorded as relates links on the rendering's intent with the reason, never as satisfies: the sentence-length rule, which the essay's twelve-word average does not meet by design, and the word budget, 900 in the intent against 1,169 rendered. The ratifier accepts or overturns them; the graph does not pretend.

The renderer

prose: registers and the links between them derives_from ×7 verifies ×7 INT · 1 intent NG · 1 non goal REQ · 7 requirement TEST · 7 test prose: registers and the links between them
The renderer's own graph: seven requirements, each verified by one test, and a non-goal that says it never rewrites an item's words.

tl docs renders a document from item markers, and it renders each item as a block: a header with its UID and status, the text as a quotation, its links, its attributes. That is the audit view, and it is exactly what a reviewer wants. It is not an essay. Nor can the rendering graph embed a paragraph from the argument graph, since a document renders only the graph's own items. Both facts fed the design: paragraphs live in the rendering, and a small renderer of the project's own turns the generated document into the file a reader opens.

That renderer is forty lines of parsing and a Word writer. It strips the block chrome, renders section items as headings and elements by slot in the shape the author's working document already had, resolves each cites link to the URL on the evidence item so the published essay carries live citations, and writes the Word file in the house format with every zip timestamp fixed so that two runs give the same bytes. It has its own graph, with a test for every requirement in it. The fifth draft is the graph, rendered.

Two runs giving the same bytes is a weak claim, and the first version of this page made a weaker one: that regenerating after ratification produced a file whose hash had not changed. That could not have come out any other way. The renderer reads an item's text, title, order, form and slot; ratification writes ratified_by and ratified_fingerprint, and the renderer reads neither. Measuring it properly, on 17 September, meant proving that by removal rather than by argument: strip all 216 signature lines from the 108 item files that carry them, regenerate, and get the same document back. The claim worth making runs the other way. One character changed in one paragraph's prose moves the copy and exactly one of the twenty-seven parts of the Word file, and leaves the other twenty-six alone; the same holds for every one of the twenty-eight paragraphs, taken one at a time. Both halves are written down as assertions in a test, which goes red if either stops being true. Nothing runs it on a schedule yet: the repository has no CI.

Taking that measurement found five faults in the renderer that no gate could see. A paragraph set to rejected was still published, byte for byte, with the composed check, the document check and the render all green, because the only status the renderer filtered was deleted; it now refuses rejected, deferred and suspect, in both graphs and through the evidence a citation resolves to. Swapping two sections' order attributes changed nothing, because the order check filtered to paragraphs. Under a C locale with UTF-8 mode off the renderer stopped with a decode error and wrote nothing, which is the default state of a minimal container image. Six of the eight zip header fields were left to the platform rather than set. And the determinism test compared two renders to each other and to no recorded value, so a build whose only deflate is zlib-ng satisfied every assertion in it while producing a different file. Repacking the same twenty-seven parts on Fedora 42, whose zlib reports itself as 1.3.1.zlib-ng, gives 610,009 bytes rather than 602,437, twenty-five of the twenty-seven compressed differently, and every one of them decompressing to identical bytes; repacking them under zlib 1.3 reproduces the published file exactly. What is recorded and checked now is a manifest of the document, the sha256 of the copy and of all twenty-seven parts uncompressed. The hash of the Word file itself is a property of the deflate build that packed it, and is no longer published as though it travelled.

$ python3 tools/prose/render.py --repo . --out output wrote output/essay.md and output/essay.docx (1258 words including front matter) every claim in the argument is expressed $ sha256sum output/essay.docx # before and after ratifying all 157 items ef38838b7ffb73d6 output/essay.docx ef38838b7ffb73d6 output/essay.docx

The challenge pass

Before hand-off, the challenge pass ran over all five graphs: the deterministic checks by script, the semantic ones by reading items held together. It found ten paragraphs that named evidence in their text with no edge, each of which got a cites link. It found one claim that restated two others, which was retired and its paragraphs pointed at the originals. And it found all 130 links into the composed sources unstamped, so none could detect a moved source; all were stamped, which is what makes an amended claim or a changed house rule mark its paragraphs suspect.

Reading the paragraphs against the rules they cited found two more things a script could not: one paragraph cited the active-voice rule and contained a passive, and every paragraph cited the sentence-length rule the essay does not meet. The first was reworded; the second became the recorded deviation above.

Ratification, and three tool releases

Ratifying 157 items exposed three defects in the toolchain, and each became a release before the evening ended.

  • The ratification cockpit failed on every graph that composed a source. A private field it read from the composition library had been renamed a release earlier, and the cockpit's own tests had stubbed the seam rather than exercised it. The fix was one line; the real change was a test that composes a real source unstubbed, and a build step that runs the installed cockpit over the tool's own composed graph before it ships.
  • Stamping 153 links took about fifteen minutes, because every command re-validated six sources over the network and each link was one process. Measured, not guessed: three seconds a call, of which 1.3 s was the network and 1.7 s was start-up. An issue was filed with the numbers and two asks, a revalidation window and batch mutations.
  • tl ratify took one UID, and the hand-over command that passed two did not run. The author asked whether it should take several. It now does, in the core tool and the composition layer, each item shown and confirmed on its own.

The author then declined to press the button 157 times, delegated the signatures in writing, and the AI ran tl ratify on his instruction with the delegation recorded in every commit message. The ratified_by field names the human; the commit names the delegation. Whether that is a ratification or a formality is the question this example leaves open.

The gate

tl-compose -C blog/idd/blog check --strict tl-compose -C blog/idd/blog docs --check python3 tools/prose/render.py --repo . --out output

Three commands, on every commit: the composed rendering graph is sound with nothing unaccepted in it, its generated document matches the graph, and the essay renders with every claim expressed.

What the tooling taught

Paragraphs are presentation. The sorting rule, that an item belongs to a layer only if nothing below it could make it false, put the paragraphs one graph down from the claims, and that is what let one argument carry a blog, a teaser and a title without drifting.
Make evidence a coverage rule, at error severity. One supported_by rule found three errors that four human drafts had carried, before any paragraph existed.
Objections are delivery roots. Declaring them so turned a style guide's advice into a check that fails when an objection goes unanswered.
An audit view is not a document. tl docs is right for a reviewer and wrong for a reader. The gap is a small renderer with its own graph, not a hand edit of the output.
A deviation is a relates link, never a satisfies. A rule the text knowingly breaks is recorded with a reason; the ratifier decides.
Stamp every link into a source. Unstamped, a moved house rule or an amended claim is invisible to the paragraphs that depend on it.
A figure that could only have one value is not evidence. Regenerating after sign-off was always going to give the same bytes, because the renderer never reads a signature. The measurement worth publishing is the one that could have gone the other way, and the way to prove a blindness is to remove the thing and see nothing move.
Measure the slow thing. Three seconds a call, 1.3 of them network, was worth an issue; "it feels slow" was not.
Check --help before handing over a batch. The command that failed in the author's hands was the tool's limit, not his; the tool changed.