✍ Write a book, start to finish¶
Status: built, and a tutorial. Written 2026-08-18. Updated
2026-09-15, renamed from BOOKS.md and given the walkthrough framing its
five sister pages use. All four pieces of the track exist -- the outline,
the generation unit, the three registries and the assembly skill.
Nothing here is a plan.
Written for someone drafting a document larger than a chapter with this pipeline. Assumed: nothing about this pipeline, though a book is the one genre where doing a single chapter first (a textbook chapter is the usual one) will save you a false start. AGENTS.md has the drafting layer's contract and ARCHITECTURE.md the four layers, but you do not need either to follow the steps below.
Sister tutorials, for the chapters this book is made of: a survey, a thesis chapter, a textbook chapter, a tutorial, a deep-research report.
A book is not a longer draft. It is a signed outline, one authored
chapter per {#id}, an acceptance record per chapter, three derived
registries, and an assembly step -- with two human sign-offs, one
before any prose is generated and one before the book is called
finished. Those two gates are the whole point: everything between them
is mechanical, and neither can be automated away.
The walkthrough is the first half of this document, in the order you run it, with a real book's output at each step. The second half is the reasoning: each step links to the argument behind it, so a decision is stated once and read where you need it.
🧭 Table of contents¶
- The constraint everything here answers
- The steps, at a glance
- Before you start
- Step 1: write the outline
- Step 2: sign the outline off
- Step 3: hand each chapter to its author
- Step 4: accept the unit
- Step 5: build and read the registries
- Step 6: assemble the book
- Step 7: build the PDF
- Step 7b: the one review aid that reads a book
- Step 8: the second sign-off
- What one real book looked like
- Retrofitting a book drafted before this track
- Why an id is required on every heading
- Why sign-off is a sibling file
- Why sign-off is recorded per chapter
- Why a chapter is the authored document
- Structure and artifact are different units
- What
status's exit code is, and is not - What the input digest covers, and what it must not
- Why
registry checkexits 0, when the twostatuscommands do not - What the registries cannot see
- Why a registry excerpt is not hashed into a unit's contract
🔑 The constraint everything here answers¶
A book does not fit in a context window, and generation quality degrades long before the limit.
So cross-chapter consistency cannot live in a model's memory of an earlier call. It has to live in explicit on-disk artefacts, injected as relevant excerpts into each unit's generation, and checked deterministically afterwards. That is the same two-plane discipline as the rest of the project: the artefacts are written by deterministic passes, never by an LLM writing to the corpus plane.
Two human sign-offs, and no more: the outline and the finished book. No automated check verifies that an argument is good -- only that it is grounded, consistent and complete. Everything between the two sign-offs is mechanical.
🔭 The steps, at a glance¶
| Step | Command | Who runs it |
|---|---|---|
| 1 | spec init, then edit spec.md |
you |
| 2 | spec sign |
you, and only you |
| 3 | spec seed, then a genre skill writes the prose |
you, then skill |
| 4 | spec align, then unit accept |
you, per unit |
| 5 | registry build, registry check |
you or the assembler |
| 6 | the book-assembler skill composes book.tex |
skill |
| 7 | pdflatex, bibtex, pdflatex x2 |
you or the assembler |
| 8 | read it | you, and only you |
Steps 3 and 4 repeat per chapter. Steps 5 to 7 are what
.claude/skills/book-assembler/ does in one run.
A real spec partway through this ladder is committed:
the sample project holds a signed two-chapter outline
(spec.md),
its sign-off record
(signoff.md),
and one accepted unit
(units/ch-staleness.json)
whose authored chapter sits at
content/drafts/twin-basics/ch-staleness.md
-- with the second
chapter honestly unwritten, because a spec is allowed to be ahead of
its book and the registries say so rather than pretending otherwise.
🔧 Before you start¶
You need a synced corpus, because every unit is grounded in it:
1 | |
Every command on this page also works as
python -m chitragupta.<layer> ...--python -m chitragupta.draft spec initis the same aschitragupta draft spec init. Use whichever your install gives you; the console script is used throughout below.
A book lives in one directory under content/drafts/, one file per
chapter, named for that chapter's own {#id}. Nothing needs to exist
there yet -- the outline comes first, and the prose is written into it
afterwards:
1 2 3 4 5 6 7 8 9 10 | |
content/drafts/twins/ holds only chapters a person wrote.
Everything the assembly produces is output and lands under
content/rendered/twins/, which is where a chapter's own render goes
anyway -- so the fragments arrive there without being told to, and
book.tex is composed beside them. Nothing you author is ever mixed in
with something regenerated, in either direction.
content/specs/ mirrors the book's own directory under content/drafts/
-- the same rule content/dossiers/, content/rendered/ and
content/review/ follow, read one level up. Those mirror a single
draft, so they carry the draft's parent directory; a book is a
directory of drafts, so its own path carries over. Everything under
content/ is gitignored: it is your data, not the pipeline's.
▶ Step 1: write the outline¶
1 | |
That writes a skeleton. Edit it into the book you mean to write -- planned top-down, generated bottom-up. Four heading levels, and no more:
| Markdown | Is | Generates |
|---|---|---|
# Title |
the book | -- |
## Part {#part-i} |
a part | -- |
### Chapter {#ch-1} |
a chapter | one authored document -- and what unit accept records |
#### Section {#sec-1} |
a section | one heading inside it -- structure, never a file |
Nothing sits below a section: a level deeper would describe structure the outline has no business owning. Text beneath a heading is that heading's brief -- what it must establish, and what it leaves to another. Text before the first heading belongs to nothing and is never handed to a generator; it is the preamble for whoever opens the file.
A chapter is one authored document, and its sections are the headings
inside it. The outline stops at the sections of a chapter; the
sub-headings an author writes underneath are theirs, not the spec's.
Why the chapter and not the section
-- it is what lets spec align mean anything.
Every part, chapter and section needs an explicit {#id}, and a heading
without one is refused rather than guessed at --
why.
An id also becomes a filename: unit accept writes
content/specs/<book>/units/<id>.json. It is held to the same rule a
citekey is, therefore -- no / \ : * ? " < > |, no control character,
not . or .., and not a name Windows reserves -- and an id that
breaks it is named as a parse problem rather than quietly rewritten into
one that does not.
A worked spec.md, short enough to read whole and showing all four
levels, the briefs, and the preamble that belongs to nobody -- the file
is at examples/dossiers/book/spec.md:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | |
Four things that example is showing:
- The brief is the text under a heading, in your own words. It says what the chapter must establish and what it must leave to another, which is what stops two chapters writing the same section.
- Ids are stable names, not numbers.
ch-fidelity, notch-3. A chapter that moves keeps its id, and every\cref{ch-fidelity}in every other chapter still resolves. - Nothing sits below
####. A level deeper would be describing structure the outline has no business owning. - Cross-references are declared in the brief ("answers the question
sec-modelraised"), which is what makes step 5's registry check able to tell you when one dangles.
Check what you wrote:
1 | |
1 2 3 4 | |
spec show, spec sign and spec status all refuse a spec that does
not parse, and print every problem rather than the first: someone
fixing an outline wants the whole list, not one round trip per missing
id.
▶ Step 2: sign the outline off¶
The first of the two human gates. Nothing generates prose from an unsigned outline, and no command can do this for you:
1 2 | |
1 2 3 | |
sign records a twelve-hex digest of spec.md in a sibling file --
why a sibling -- so status can tell
three states apart:
| State | Exit | Means |
|---|---|---|
signed off at digest x |
0 | the approved outline is the one on disk |
| not signed off | 1 | nobody has approved this outline yet |
| changed since sign-off | 1 | approved at one digest, now another |
That non-zero exit is not a new gate -- what it is.
It records one digest per chapter as well, and status names the
chapters that actually moved:
1 2 3 4 | |
Why per chapter -- it is what keeps a revision to one chapter from freezing the other fourteen.
▶ Step 3: hand each chapter to its author¶
The spec owns the book's structure; the genre skills own its content. This is the handover:
1 | |
1 2 3 4 5 | |
For each chapter the outline describes at section level, it writes that
chapter's section names into its dossier's outline.md as bare ##
headings and stops. Every brief:, claim: and queries: line beneath
them is left for whoever drafts the chapter -- a brief the spec invented
would be the book track writing content.
It refuses an unsigned outline, because seeding from a structure
nobody approved puts unsettled section names in front of an author as
though they were decided. It never rewrites a heading, so it is safe
to re-run once the spec grows a section; --dry-run says what it would
add. The write itself is done by dossier init, so the package that owns
content/dossiers/ stays the one creating files in it.
A freshly seeded outline has headings and no briefs, so
dossier outline <draft> --check will report each as "neither a brief:
nor a claim: block". That is the correct reading: nobody has filled it in
yet.
Filling it in is the chapter author's job, and it is the same
outline.md every other genre uses. Per section: brief: (steering,
never printed), claim: (your prose, grounded or reported back),
queries: (run verbatim). Each chapter also gets its own scope.md --
the reader, what the chapter covers, what it defers to another chapter,
and the glossary.
That glossary is where a book is won or lost. Chapter authors working in
parallel will define the same term three ways unless each chapter's
scope.md pins it, and step 5's terminology registry is what catches
them when they do.
Whichever genre a chapter is, its tutorial has the field-by-field walkthrough and a complete filled-in example:
| Chapter is | Follow |
|---|---|
| teaching material, worked examples, exercises | WRITE-A-TEXTBOOK-CHAPTER.md |
| a hands-on lab the reader follows at a keyboard | WRITE-A-TUTORIAL.md |
| a literature map or state-of-the-art chapter | WRITE-A-SURVEY.md |
| an RQ-driven argument | WRITE-A-THESIS-CHAPTER.md |
| a multi-perspective, contradiction-mapped report | WRITE-A-DEEP-RESEARCH-REPORT.md |
A book may mix them -- a textbook whose third chapter is a survey and whose fifth is a lab is a normal shape, and the unit record does not care which genre wrote a chapter.
▶ Step 3b: generate one unit¶
Ask for the contract, which is what the unit is generated from:
1 2 | |
1 2 3 4 5 6 7 8 9 | |
The contract is explicit in both directions:
| In | Out |
|---|---|
| the spec slice (title, brief, the part and chapter above it) | the unit's prose at content/drafts/<book>/<unit-id>.md |
the sources it is grounded in, given as --source <citekey> |
the citekeys it actually cites, recorded |
| registry excerpts, injected at generation time | the claims the register picks up |
--source is repeatable and is part of the input digest, so grounding a
unit in a different set of papers is a different unit to generate --
what else the digest covers.
Registry excerpts are handed to the generator but deliberately left out
of that digest --
why.
Then write the unit. This is the one step this track does not own: a
genre skill drafts it (thesis-chapter-writer for a .tex fragment,
another genre for Markdown), grounded in the sources, and saves it as
content/drafts/twins/sec-1.md. From step 5 on you can hand that skill
what the rest of the book already settled:
1 | |
A part or a chapter has no contract, and asking for one is refused rather than answered with an empty contract: those levels name no prose of their own.
▶ Step 4: accept the unit¶
Before accepting, check that what was written still matches what you approved:
1 | |
1 2 3 4 5 6 | |
It compares the sections your outline declares for a chapter against the
## headings that chapter's author actually wrote, and reports four
things: a declared section not authored, an authored section not
declared, a renamed heading, and sections all present but out of
order. A reworded heading is one finding rather than two -- "you renamed
this" is what happened. Numbering is ignored, so 3.1 The model half and
The model half are the same section: a genre skill numbers what it
writes and the outline does not.
align reads and refuses nothing, and exits non-zero on a finding the
way spec status does. It is silent on a chapter the outline describes
only at chapter level -- see
why a chapter is the authored document
for the scoping rule and what it is protecting against.
1 | |
1 2 3 | |
accept writes the record only after the project's one gate passes on
the draft. It refuses five ways, each for a stated reason:
- This unit's chapter is not signed off -- there is nothing to accept a unit against until a human has approved the structure. Asked of the unit's own chapter, not of the whole book, so revising chapter 7 does not stop you accepting a unit in chapter 3 (why).
- The unit's chapter no longer matches the outline -- acceptance
records that a human approved this prose against that outline, and a
chapter whose headings have drifted makes the record say something
untrue.
spec alignlists what moved. A chapter nobody has written yet is deliberately not a refusal: a book is drafted unit by unit, and holding the first unit until the whole chapter exists would make it impossible to accept. - There is no draft -- generate the unit from its contract first.
- A
--sourceis not in the ledger.--sourcenames the papers this unit claims to be grounded in, and the acceptance record keeps that claim permanently, so it is checked against the ledger before anything is written. A record asserting grounding in a citekey no real parse ever produced is exactly the fabricated reference CLAUDE.md's one rule exists to prevent. Refused with the argument-shaped faults above rather than beside the gate's findings, where it would read as a defect in the prose. - The citation gate refuses the draft.
acceptruns the project's one gate rather than re-implementing or replacing it -- it reads the draft once and hands that string tocitation_gate.check_text, reporting throughcitation_gate.report, which is the same printerchitragupta draft gateitself uses, so the two read identically. Reading once is the point:acceptused to gate the path and then re-read the file to hash and record it, so a write landing between the two calls produced a permanent record for prose the gate had never seen. A unit nobody may cite from is not a unit a book may assemble from, and this is the existing gate doing its existing job -- not a second one.
The record holds the input digest the prose was generated against, the
sources, what it cites, and a digest of the prose itself. It carries
no timestamp, so accepting an unchanged unit twice produces
byte-identical files and a diff of content/specs/ is a diff of what was
accepted.
Repeat steps 3 and 4 per unit. unit status is the board, and it names
what the dossier says about the same prose beside what this layer
recorded:
1 2 | |
Two records of the same text exist and they answer different questions:
accept's output_digest is "has this changed since a human accepted
it", the dossier's fingerprint is "has it changed since the sidecars were
reconciled". Different commands refresh them -- unit accept and
dossier stamp -- so they can disagree, and neither report used to
mention the other. no dossier, not stamped, agrees, disagrees and
stamped, no draft are the five answers. Reported, never enforced:
this layer does not judge a dossier.
1 | |
| State | Means |
|---|---|
unwritten |
no draft on disk |
drafted |
prose exists, nobody accepted it (also what an unreadable record reads as) |
accepted |
the record matches both the current contract and the prose |
stale: inputs changed |
the outline moved under an accepted unit |
stale: draft changed since accepted |
the prose moved after acceptance |
It re-derives all three digests rather than trusting them, and exits 0
only when every unit is accepted and current -- the same standing as
spec status.
▶ Step 5: build and read the registries¶
1 2 | |
1 2 3 4 5 6 | |
Three registries, written under content/specs/<book>/registries/ by a
deterministic pass over accepted units only:
| Registry | Written from | Flags |
|---|---|---|
| terminology and notation | - **Term** -- definition bullets |
a term defined in more than one unit |
| claims | every sentence that cites something | the same claim made in more than one unit |
| cross-references | [text](#id) and \ref{id}/\cref{id} |
a reference no unit or outline entry defines |
Three properties hold for all of them:
- Built from accepted units only, and the count is printed, naming the ones it could not see. A registry over half a book is not the same claim as one over all of it.
- Nothing here is written by an LLM. They are a deterministic reading
of accepted prose, which is the whole reason they can be trusted --
the same standing
chitragupta/ledger.pyhas as a reading of a real bib file. - The conventions are borrowed, not invented. The definition bullet is
the dossier glossary's, the sentence splitter is the provenance aid's,
and everything from a
## Referencesheading onward is cut the waychitragupta/acronyms.pycuts it -- measured there against the real 15-chapter book, because a rendered reference list is nothing but citation-bearing lines and would otherwise fill the claim register with bibliography.
A cross-reference is never spelled @id. That is a citekey position:
a section id reaching it would put something the ledger has never seen
where only a real bibliography entry may go. tests/test_registry.py
pins that the citation gate reads neither supported reference syntax as a
citekey.
check exits 0 however much it finds --
why --
and there are things it structurally cannot see --
which.
▶ Step 6: assemble the book¶
Ask for the book and .claude/skills/book-assembler/ runs steps 5 to 7:
it confirms both status commands, prints every registry finding, and
only then composes. Everything it assembles has already passed every gate
per unit, so assembly is deterministic composition plus a human sign-off
-- there is no enforcement machinery here to write.
Conventions as data, not code. The whole composition is one table, and the ids carry through unchanged:
| Outline | LaTeX | Label |
|---|---|---|
# Title |
\title{...} |
-- |
## Part {#part-i} |
\part{...} |
\label{part-i} |
### Chapter {#ch-1} |
\chapter{...} |
\label{ch-1} |
#### Section {#sec-1} |
\input{sec-1.tex} |
the unit's own \label{sec-1} |
That the {#id} becomes the LaTeX label unchanged is what makes the
cross-references registry check verified actually resolve in the built
PDF -- the outline, the registry and the document all name the same
thing.
The bibliography is one list at the end of the book. Each unit is converted with
1 | |
and --fragment is the whole difference: no preamble, the unit's own #
heading becomes a \chapter, and code blocks are left unhighlighted
because Shaded/Highlighting exist only in the standalone template.
It also defers its citations: the fragment emits \citep{...}
instead of a resolved [1], and carries no reference list of its own.
book.tex then holds \bibliographystyle{IEEEtran} and
\bibliography{bibliography}, and one bibtex pass numbers every
citation in the whole document at once.
Why the resolution has to move, not just the list. Citeproc assigns
numbers in the same pass that builds the list. Resolve per unit and every
chapter restarts at [1], so chapter 1's [2] and chapter 2's [2] are
different papers; collect those into one back-of-book list and half the
markers point at the wrong entry, in a book that compiles cleanly.
Deferred, a source cited in two chapters carries one number in both
and the sequence runs continuously -- measured both ways on a real
two-chapter build.
The citekey aliasing still applies, and now on both sides: a key
containing -- (@lim_state---art_2020) would otherwise reach LaTeX
truncated and render as [?], so the render rewrites it to
lim_state-x2d-x2d-art_2020 in the fragment and in the .bib it
copies beside book.tex. Never hand-edit either.
A standalone render -- the same unit without --fragment -- is
unchanged: citeproc, the vendored IEEE style, and its own numbered
reference list, exactly as every other genre skill produces one. The two
shapes exist because a chapter read alone wants its sources at the end of
the chapter, and the same chapter in a book wants them at the end of the
book.
🔠 Brace-protect acronyms in your .bib titles¶
This is the one thing a book needs from your bibliography that a
single draft does not. Two IEEE implementations now format your
references: assets/csl/ieee.csl for a standalone render, and
IEEEtran.bst for the assembled book. They agree -- byte for byte, on
every entry measured -- provided acronyms in a title are wrapped in
braces:
1 | |
Unbraced, bibtex lowercases them and the book's bibliography reads "a
survey of iot and ai digital twins". Citeproc preserves them, so the same
entry is correct in a standalone render and wrong in the book. Braced,
both produce the same line.
This is ordinary BibTeX practice -- braces mean "do not change this
case" -- and it was simply never load-bearing here before, because
nothing in this pipeline ran bibtex. It is a property of your own
export, so fix it in your reference manager rather than in
papers/bibliography.bib, which a re-export overwrites. Nothing in the
pipeline rewrites your titles: guessing which capitalised word is an
acronym and which is a proper noun is exactly the kind of silent
alteration to a human's bibliographic data this project does not make.
One more consequence for the book itself: margin=80pt -- about 28mm.
The book class's own margins are 94pt inner and 143pt outer (measured),
generous enough to run a 15-chapter book to 546 pages; a third of that
was tried and read too tight for print, so the setting is that doubled.
book.md is written beside book.tex, in
content/rendered/<book>/: the same structure in Markdown, hyperlinking
the chapter files alongside it, for anyone who is not building LaTeX.
Where the registry proposal's "blocking" actually lives. The skill must run
registry check and print every finding, in full, before composing --
which is the guaranteed invocation ARCHITECTURE.md permits, in place of
the conformance it does not. tests/test_skill_book_assembly.py pins
that, so a hand edit dropping either half fails the suite.
▶ Step 7: build the PDF¶
A book is built directly, from its own directory -- the \input paths
are relative to it:
1 2 3 4 5 | |
Four passes, and the bibtex one is not optional: the first
pdflatex records which keys the document cites, bibtex turns those
into book.bbl, the third pulls the bibliography in, and the fourth
resolves \cref, the table of contents and the citation numbers now that
the entries exist. Skip bibtex and every citation renders [?] while
pdflatex still exits 0.
Then read the log before believing the PDF. pdflatex exits 0 on a
book that renders [?] where a reference should be -- natbib reports a
dropped citation as a warning, not an error:
1 2 | |
Anything but [] means a citekey never reached the bibliography. This
check became load-bearing when the bibliography moved to the end of the
book -- before that, citeproc had resolved every citation already and
there was nothing for the warning to report. Python
rather than grep -c deliberately: on the host this was first run,
grep -c over that log printed nothing at all, and a check that silently
reports nothing is worse than no check.
If your units number their own sections (## 1.0 Before you start),
put \setcounter{secnumdepth}{-2} in content/specs/twins/preamble.tex,
or LaTeX numbers them a second time -- "1.1 1.0 Before you start", and
worse further in.
A chapter title that carries its own number is a different case, and
this is the wrong lever for it. A unit headed # Chapter 1: Why Anyone
Pays used to open Chapter 1 / Chapter 1: Why Anyone Pays, with the
table of contents reading 1 Chapter 1: Why Anyone Pays to match --
and secnumdepth{-2} fixed that by taking away every section and table
number in the book, which is a document-level price for a chapter-level
problem. Step 6's draft render --fragment now drops the prefix from
the \chapter{} it emits instead, so the number comes from the book
class alone and everything else keeps its numbering. Your .md is not
touched: it still titles that unit's own standalone pdf, and every unit
stays accepted. If you already added secnumdepth{-2} for a
duplicated chapter number, take it out -- it is now costing you the
section and table numbers for a clash that no longer happens.
A unit you drafted as .tex is the exception: step 6 does not convert
it, so the prefix reaches the book as you wrote it. chitragupta draft
style reports that one as chitragupta.ChapterSelfNumbered; delete the
prefix from the \chapter{} and let the book supply the number.
That file is the supported way to override anything the assembled
book.tex sets. It is optional and most books have none; if it exists,
book-assembler copies it beside book.tex and \inputs it as the last
line of the generated preamble, so it wins over the defaults above it.
Do not hand-edit book.tex to do this. That file is regenerated
every time the book is assembled, so an edit to it is lost at the next
run, while preamble.tex is yours and survives.
The default book.tex sets \setcounter{secnumdepth}{2} -- the book
class's own default, restated so a book states its numbering rather than
inheriting it silently -- and \setcounter{tocdepth}{1}, which stops the
table of contents at the section. Which numbering a book shows is a
composition decision and belongs to the book; renumbering your headings
does not, and is draft-reviser's call.
▶ Step 7b: the one review aid that reads a book¶
Every other aid reads a draft; this one reads the assembly, and asks a question only a book can be asked -- does the assembled document still carry every citekey its accepted units stand on?
1 | |
Advisory, exits 0 whatever it finds, blocks nothing. It reports two things, and they mean opposite problems:
| Finding | What happened | Usually means |
|---|---|---|
dropped |
an accepted unit the assembly never \inputs, named with every citekey the book then holds nowhere else |
a chapter left out of book.tex by accident -- the most expensive assembly mistake there is, and invisible in a PDF that compiles |
appeared |
a citekey in a file the assembly includes that no unit owns | a title page, appendix or preamble file citing something -- fine if deliberate, a leak if not |
It resolves the assembly's \input chain rather than grepping
book.tex for citekeys, and that is not a detail: citeproc resolves each
unit's citations inside that unit, so the assembly's own text states no
citekey at all. An aid that read the text would report every source in a
correct book as lost.
Two things it refuses, both exit 1: a path in no book (there is no expected set to compare against), and a path that is itself one of the book's units -- pointed at a unit it would report every other unit's citekeys as dropped, which is a confident and wholly wrong report.
Per-chapter aids still apply, and are worth running before assembly rather than after:
1 | |
Its findings are the chapter author's, in the same [unattended] /
[surfaced] shape each genre tutorial describes.
▶ Step 8: the second sign-off¶
The assembler presents what it composed -- the unit count, what the registries could not read, every finding, and what the gate and the two review aids said -- and stops there. It does not say the book is finished, and neither does anything else in this pipeline.
Nothing here has read the argument. The checks establish that a book is grounded, consistent and complete; none of them establishes that it is any good. That judgement is the second human gate, and it is yours.
Worth running before you circulate it, per unit rather than over
book.tex (which holds no prose):
1 2 | |
Both are review aids: they exit 0 whatever they find, and neither may block. The scan sees verbatim and near-verbatim reuse only -- genuine restatement is only detected where the embedding tier can run, so a clean scan is not a clean bill of health (PLAGIARISM.md).
📝 What one real book looked like¶
The first book assembled by this track, so the numbers are measured rather than illustrative -- a 15-chapter textbook, 22,155 lines of Markdown, re-measured on 2026-08-19 after the chapters were revised:
| outline | 3 parts, 15 chapters, 15 units |
| citations, gate-verified | 864 across the 15 units, 194 distinct citekeys |
| terminology registry | 15 definitions |
| claim register | 388 claims |
| cross-reference graph | 0 edges -- the chapters refer to each other in English, not as links |
registry check |
1 finding: one claim made in two chapters |
| the book | 430 pages, 1.7 MB, 0 undefined citations |
Two things that build found, both now fixed in the skill: the Markdown
conversion step named the wrong command, and three citekeys containing
--- were being silently truncated -- 10 citations that would have
rendered as [?] in a finished book.
🛠 Retrofitting a book drafted before this track¶
A book whose chapters already exist can be brought under the track without rewriting a word. The outline is derived, not invented:
- Take the parts and their chapter numbering from whatever table of contents the book already has.
- Take each chapter's title from that chapter's own
#heading -- what the prose actually says, not the table of contents' paraphrase. - Make each unit id the chapter's filename stem, so
unit acceptfinds the prose where it already lives.
One chapter is then one unit: a #### section whose id is the filename,
under a ### chapter entry that carries the same title. The chapter
heading stays inside the unit file, so book.tex emits \part and
\input and lets the fragment's own \chapter{} supply the title --
emitting one here as well would print every title twice.
Say in the spec that the outline was retrofitted, and from what. A sign-off records a person's decision; one recorded on an outline nobody has read is a record of the wrong thing.
💡 Why an id is required on every heading¶
Every part, chapter and section carries an explicit {#some-id}, and a
heading without one is a parse problem rather than something the parser
guesses at.
A derived id -- slugified from the heading text, say -- changes the moment someone rewords the heading, and every unit already written against the old spelling silently becomes an orphan. At chapter scale a person notices; across 300 pages nobody does. The same ids are what the cross-reference graph resolves against, so they have to outlive an edit to the words around them.
💡 Why sign-off is a sibling file¶
spec sign records a twelve-hex digest of spec.md -- the same shape as
the dossier's corpus fingerprint, and for the same reason: enough to
answer "is this the same document?", short enough to sit on one line.
It goes in signoff.md rather than into spec.md itself because writing
the digest into the file would change the file it just measured, and no
later read could ever match. The digest covers spec.md alone.
signoff.md carries no timestamp, the same rule the review layer's
reports follow: two sign-offs of an unchanged outline produce
byte-identical files, so "did this change?" is a diff. When it was
approved is not a question any check asks; what was approved is.
💡 Why sign-off is recorded per chapter¶
signoff.md records the whole-file digest and one digest per chapter,
each taken over that chapter's ### heading and everything under it.
The whole-file digest alone cannot answer the question accept asks.
Measured on a fifteen-chapter book: editing a single character in
one chapter's brief moved the file's digest, so every one of the 672
sections that outline declared, across all fifteen chapters, reported
signed_off: False, and accept refused across the entire book while one
chapter sat half-revised. A book is revised chapter by chapter over weeks;
a book-wide answer makes every in-flight revision a book-wide freeze.
The escape -- re-run spec sign -- was worse than the freeze. It
re-approves all fifteen chapters at once, so the record could no longer
tell "a human read this" from "a human re-approved it as collateral while
fixing something else." This document already states the principle for
the retrofit case, and it applies here unchanged: a sign-off recorded on
an outline nobody has read is a record of the wrong thing.
Keyed by chapter id, not title, for the reason ids exist at all: a reworded heading must not orphan the record of its own approval.
Nothing migrates an old signoff.md. A file with no chapter lines --
every book signed before this existed, including the retrofitted one --
falls back to the whole-book digest, which is the previous behaviour
exactly. Writing chapter digests into it on a human's behalf would be
this project inventing an approval, which is the one thing a record of a
person's decision may never do. Re-run spec sign to opt in.
💡 Why a chapter is the authored document¶
A chapter is one file; its sections are the headings inside it. An earlier revision of this document said one file per section; that has since been changed.
The change is what makes spec align mean anything. Under one file
per section, every file is a section by construction -- there is no way
for what was written to disagree with what was approved, so there is
nothing to check. Only when a chapter is a single document can its
headings drift from the outline a human signed.
It also matches the only book that exists. digital-twins-for-software-
engineers is one file per chapter, achieved by
retrofitting each
chapter as a single #### section carrying the filename. That was a
workaround for the old rule; this makes the real shape the declared one.
Alignment is scoped to chapters the outline describes at section level -- two or more declared sections, or one whose title differs from the chapter's own. Anything else reports "described at chapter level; nothing to align", with no finding.
That scoping is not a convenience. Measured on the real book: its
retrofitted outline declares one section per chapter while its author
wrote about forty headings under each -- 4 declared sections against 161
authored headings across the first four chapters. Without the rule,
align would put roughly 225 findings on a book that is not wrong, only
described at chapter granularity, and a check like that is the first thing
anyone turns off.
💡 Structure and artifact are different units¶
spec.md declares structure: headings a human approves before any
prose exists, and what spec align checks a draft against. unit accept
records an artifact: a file the citation gate runs on, whose whole
text is hashed into output_digest and whose citekeys are extracted. A
heading has no independent existence on disk.
Before a chapter became the authored document those coincided, because a book was one file per section. They cannot now, and the asymmetry is load-bearing: alignment only has content while the outline is finer than the file. Collapse them in either direction -- declare only chapters, or go back to one file per section -- and "did you write what you said you would?" becomes a question with no possible answer.
So unit contract|accept|status resolve against acceptance units: a
chapter the outline describes at section level is one; a chapter
described only at chapter level leaves its single section as one, which
is every retrofitted book and why they keep working unchanged.
A third sense of the word lives in chitragupta/review/_units.py -- the
scale the multi-source rule binds at, which is genre-dependent
(paragraph for a thesis chapter, section for a textbook one). That one is
about evidence; this one is about artifacts. They are not the same
question and are not expected to agree.
✅ What status's exit code is, and is not¶
spec status and unit status exit non-zero on an outline nobody has
signed or a unit nobody has accepted. That is not a new gate, and the
distinction matters enough to state rather than leave to a reader.
ARCHITECTURE.md's "Layer 4" draws the line by what a
check is measured against: the citation gate is measured against the
ledger, which is ground truth, while a check measured against a recorded
preference reports and never blocks, however mechanical its answer. These
two are measured against neither. They read back a record of a person's
decision -- did a human approve this outline, accept this unit? -- and
report it. They judge no draft's content, refuse no write, and block no
draft: chitragupta draft gate remains the only gate in this project,
and .claude/hooks/citation_gate_hook.py remains the only automatic
refusal.
What the exit code buys is that a skill can ask the question without parsing prose. What it does with the answer -- stop and ask you to approve the outline first -- is the human gate itself, not a machine outranking anybody.
🔒 What the input digest covers, and what it must not¶
input_digest is what makes an unchanged unit free to re-run. It covers
the spec slice, the sorted set of sources, and the registry excerpts
field -- and deliberately nothing else:
- Not the unit's own prose. A digest that moved when the output moved could never answer the question it exists for, which is "does this unit need regenerating?".
- Not the sign-off state, and not the draft's path. Neither is an input to the writing; folding either in would make a unit look stale for a reason that changes nothing about what should be written.
- Not the order the sources were given in. The set is sorted and de-duplicated first, so a unit grounded in the same papers hashes the same however the caller listed them.
Each part is labelled before hashing (sources:, registries:), so a
citekey and a registry line cannot collide into the same text.
💡 Why registry check exits 0, when the two status commands do not¶
spec status and unit status exit non-zero. registry check never
does, however much it finds -- and the difference is not inconsistency.
ARCHITECTURE.md's "Layer 4" is explicit that a check measured against a recorded preference "reports and never blocks, whichever layer it lives in", and that what may be enforced is invocation rather than conformance: "a harness may guarantee that it runs and that its findings are seen, never that they were obeyed."
The two status commands report whether a human decided something.
This one reports a machine's reading of prose: which term it thinks
was defined where, which sentences it thinks match, which reference it
thinks dangles. That is judgement however mechanical the arithmetic, so
it is evidence and never a verdict. There is no flag that changes this,
deliberately: DEVELOPER-AGENTS.md bars promoting a new check into a gate
outright rather than leaving it to an argument about how precise the
check is.
What the registry proposal calls a "blocking global check" is therefore delivered as guaranteed invocation in step 6, ahead of the human sign-off in step 8. That is a stronger reading of the requirement than an exit code would be, not a weaker one -- an exit code can be ignored by a caller; a sign-off cannot be given by one.
🚫 What the registries cannot see¶
Contradiction. The registry proposal asks for "duplicate and contradicting claims across chapters flagged". Duplicates are decidable and are flagged; two chapters asserting opposite things are not, and nothing here pretends otherwise. Naming what a check cannot see is this project's house style (PLAGIARISM.md does the same for the tier that needs an optional stack), and the final human sign-off is what covers the rest.
Two smaller limits, for the same reason: a definition that does not use the bullet shape is not registered, and "used consistently" is checked only in the sense that a term is defined once -- no attempt is made to decide whether a later paragraph used it the way the definition meant.
💡 Why a registry excerpt is not hashed into a unit's contract¶
registry excerpt <book> <unit-id> prints what a unit's generation
should be told about the rest of the book: the terminology the other
accepted units settled, and the ids it may point at. A unit is never told
to conform to itself.
That excerpt is deliberately not part of the unit's input digest, and
the reason is the cascade. A registry grows with every acceptance, so
hashing it in would mark every later unit stale each time an earlier one
was accepted -- which destroys exactly the property the contract exists
for, that an unchanged unit costs nothing to re-run. Instead the excerpt
is injected at generation time, and inconsistency is caught afterwards by
registry check over the whole book. registries stays in the
contract's shape, empty and labelled, so a caller that does want to pin
one has somewhere to put it.