The SpecAssay field guide: a tour of SpecAssay, the trace-manifest, and Loupe, one screenshot at a time. Every capture is a live render of a real HomesFlow trace-manifest: samples/homesflow.trace-manifest.json for sections 1 to 5, and samples/HomesFlow.refused.trace-manifest.json, which the Gate refused, for section 6.
Why bother
Everyone can ship plausible code now. The hard part is proving it does what you meant: which intent it served, what build filled that intent, and what proof showed it was done. When that answer takes longer than a coffee, the Golden Thread, the chain from each requirement to the code and the test that answer for it, is already lost. Requirements tools like Jira and HP ALM tried to hold the map in a second system that always lagged the repo. This stack keeps the map in the repo and checks it on every push; the checker is called the Gate. That check belongs in CI, not only on the nice developer's laptop: a cowboy without SpecAssay installed can still push, and the CI Gate is the property line.
Pull any feature and show its intent → build → proof chain before the coffee cools. That is the whole pitch. Loupe is the glass. SpecAssay is the assay office. The trace-manifest is the hallmarked record you keep.
- SpecAssay: a small overlay on stock Spec Kit. Every intent gets a durable ID; the build marks which one it serves; acceptance criteria need proof. Silent gaps stop the line. Then the Gate strikes what it found into a file.
- trace-manifest: that file (default
trace-manifest.json). Living documentation wound from the repo, not typed into a lagging side tool. Theformatvalue belongs to no single tool, so any emitter can write one and Loupe can read it. - Loupe: the magnifying glass. Does not re-scan. Only reads the trace-manifest. Follow the Golden Thread; spot the fray.
Spec Kit stays Spec Kit. No second career in requirements tooling. The interface is already a little fun; the docs should not scare you out of trying it.
For the workflow story see reading-a-manifest.md; for terms see ../presets/specassay/GLOSSARY.md.
Don't panic about the IDs
Eighty-two durable IDs in the sample can look like a lot. Most of them live in the PRD and the specs, the ordinary places intent already gets written down. You do not paste an ID onto every line of code. The ID prefixes are the ordinary requirement types wearing name tags: US- user story, FR- functional requirement, NFR- non-functional, AC- acceptance criterion. If you've written requirements, you already know these; "intent" is just the word for what they all are before the build serves them.
Leave a mark when you touch the work. If the code serves a named intent, one short comment: @covers …. If a task is still open against an intent, name it on the task: Carries:. Those marks are greppable; SpecAssay watches them; you do not memorize the grid.
What you are looking at in the loupe is living documentation: the record is wound from the repo itself (names, comments, tasks, tests). When the work drifts, the Golden Thread shows it. That is the point.
Where these pictures came from
Sections 1 to 5 are a real render of HomesFlow's live trace-manifest, the same file shipped as ../samples/homesflow.trace-manifest.json. No hand-edited JSON. HomesFlow is a small real app used as public evidence of the practice; the count of IDs is "a product with a PRD," not "a compliance mountain." They are taken by a script rather than by hand, so any of them can be taken again from one command and none of them can quietly stop matching the file it was shot from.
Section 6 is a render of ../samples/HomesFlow.refused.trace-manifest.json, which the released Gate refused for real. It was made on purpose, from a scratch copy of HomesFlow with one test renamed so that its acceptance criterion lost the test that named it, and the note beside it gives the commit, the Gate version, the row, and the Gate's verdict lines word for word. The refusal is honest; only the break was staged, and now the break is a file anyone can read rather than a scratch tree somebody remembers.
These three were the exception on this page until today: shot by hand on 2026-09-10 from a copy that was deleted afterwards, impossible to take again, and left behind when the viewer's own words changed. They are not an exception now. Every capture here comes from one command over a manifest that lives in a repository.
1. The top bar: did the Gate accept this manifest?
Before reading any row, the top bar answers the only global question:
- Gate accepted this manifest. The trace-manifest contains zero hidden unfinished work. Not zero unfinished work; zero hidden unfinished work. Loupe reads a file rather than a pull request, so the banner states the Gate's own verdict on that file and nothing about merging.
- The stat tiles read left to right: Rows (all durable IDs), then Backlog → Debt → Proven (waiting, excused, proven), with GAP last. Each tile is a filter button.
- The
GAPtile only lights up red when GAPs exist. Here it reads 0, which is why the Gate accepted it. - Load Manifest… loads any other
*.trace-manifest.jsonfile. The loupe never re-scans a repo; it only reads what the Gate emitted.
2. The map: every requirement in its area
The same rows the list holds, grouped into the eighteen areas HomesFlow's IDs are named for: A11Y, ADMIN, AUTH, EDIT, and so on. Each group shows its row count and one badge per status present in it, so ADMIN reads 1 BACKLOG · 1 TRACKED DEBT · 1 PROVEN while A11Y reads 4 PROVEN and nothing else.
Four statuses do all the work on this page, and they are the same four everywhere: on these badges, in the All statuses filter beside the search box, and in the tiles along the top bar.
- Backlog: minted and waiting. Planning-altitude stories and features without their own
@coversor test, plus anything anointed into backlog (section 4). Not a defect. - Tracked debt: not done, and the team said so on an open task. Honest amber.
- Proven: a named test exists. The list's
Own proofscolumn counts them. - GAP: an acceptance criterion with neither a test nor an admitted debt. Usually absent, as it is here. When a group carries one, the Gate has refused the manifest.
By Area beside the filter rolls the groups up a level; Scan with its − and + sets how much of each statement a card shows.
3. End to end: one requirement, three legs
The pane on the right opens on the whole field: one strand per requirement, eighty-two of them, each drawn top to bottom in the colors above. Click any row in the list and that pane walks the one row instead, and ‹ Back to field returns you to all of them.
A walked row has three legs, and the names are the three questions:
- Requirement: the durable ID and its statement, plus a ▸ file:line toggle (e.g.
HomesFlow.prd.md:262) that expands to the actual PRD or registry source around the line where the ID was minted. Its status pill sits under the statement, with any open task that carries it. - Code: every
@coversmark found in source, each labeled with the file basename and line (e.g.HomeDetailView.swift:3), the full path below, and an "also covers: …" line naming the other IDs that share the same annotation. Each expands to the real code around it. - Test: named tests that encode the ID (for ACs), labeled by test name with the path below, expandable the same way.
The third tab, Descent, does the same walk for every row at once, for reading a whole manifest rather than one thread of it.
@covers in one breath. A one-line mark, // @covers AC-GUEST-01, FR-HOME-03, naming which statements of intent this code serves. Leave it when you touch the file. The Gate reads it; SpecAssay never invents one for you. Loupe lists those hits under Code. The task-side twin is **Carries**: on an open checkbox. Missing or made-up IDs fail closed, so the habit stays light: mark what you meant, and the check keeps you honest.
Every claim in the walk can be opened to the file and line that backs it, the requirement included. The braided line down the left side is the Golden Thread itself. Its two states matter more than any color: solid means the Thread holds; frayed means it is broken.
A proven requirement, test and all
AC-GUEST-01 end to end: the statement with its registry line (HomesFlow.prd.md:262), six @covers marks each labeled by file and line, and a named test test_AC_GUEST_01_guest_fields_only at GuestTests.swift:10. Green nodes, solid braid. This is what "proven" means: a named artifact you can open, not "the tests passed once."
Note this proven row also carries an Open debt (Carries:) block, an XCUITest still open on T064. Proven with additional open work is a normal, honest state.
Tracked debt: amber, but nothing is hidden
AC-HOME-10 is not done, and nobody is pretending otherwise.
- The top of the Thread is amber: the requirement is real and owed.
- Right under it, an open task admits the gap (
T024e: still needs a snapshot or UI test). That admission is what makes this debt instead of a lie. - The middle is green: code already claims this ID. The bottom is blue, and the Test leg says why in words: "No test; tracked as open debt (see above)." The braid between them stays solid.
Amber or blue without fray means incomplete but excused: amber for owed debt, blue for not-yet. Someone wrote it down, so the Golden Thread stays intact.
4. Anointed backlog: minted on purpose, carried by a TODO
Minting an ID is a promise, and that is a feature, not a trap. Name an intent only when you mean it; if nothing claims it yet, write one open TODO that carries it (anointed backlog, usually in specs/backlog/tasks.md). That is enough ceremony that fat-finger drift still fails, while an honest "build this soon" stays clean.
Here US-SHARE-01 shows exactly that state: a minted-ahead Owner story (a read-only procedure-share link), status BACKLOG, the registry line it was minted on, and an Open debt (Carries:) block naming the open TODO T901. Zero code, zero tests, and the Gate still accepts the manifest. Drop the TODO without picking up the work and the next Gate run refuses: registry, specs, and tasks must all name the same IDs. The Thread will not carry an unclaimed promise.
Filter tile Backlog reads 5: four planning-altitude stories plus the anointed US-SHARE-01.
5. Backlog altitude: waiting, not broken
Stories and features (US-…, FR-…, NFR-…) are covered through their child ACs, not by stapling @covers US-… onto a file. When nothing in source carries their ID, they ride as backlog: blue nodes, solid braid, and both lower legs say so in words, the Code leg "No own @covers (backlog altitude)" and the Test leg "No test of its own; at backlog, so not a silent gap." Silent-gap refusal is AC-only.
US-EDIT-01 is the picture directly above, and the one thing it does not have is the thing section 4 was about: no Open debt (Carries:) block, because a planning-altitude story waiting on its own ACs needs no TODO to excuse it. That block is the whole visible difference between these two states.
6. The Gate refuses: what frays and what does not
The same viewer over a manifest the Gate refused. Three things change at once:
- The banner goes red: Gate refused this manifest · 1 refusal.
- The GAP tile goes hot: 1. That row is counted there now instead of under Proven.
- In the field on the right, one strand of the eighty-two ends in red instead of running to the bottom.
The other tiles do not line up with section 1's, and that is not the refusal's doing: this manifest was captured two months later, from a HomesFlow that had moved on, so its green baseline was already different. Only the GAP tile and the row behind it are what the staged break did. The note beside the file carries both sets of numbers.
That hot GAP tile is the assay office's whole reason for being. A silent gap is gilt: work gilded to gleam like solid gold, with no proof underneath. The assay strips the gilding; the thread frays. (It carries the other reading too: the guilt of unfinished work nobody admitted.)
The refused row itself
AC-GUEST-05 promises that a Guest who tries to mark a step complete is refused and the attempt is logged. One test answered for it. In the scratch copy that test was renamed, from test_AC_GUEST_05_guest_cannot_update_step to test_guest_cannot_update_step, and nothing else was touched: it still exists, still compiles, still passes. Only its name stopped carrying the ID.
That is the whole lesson of this section. Nobody deleted a test and nobody wrote a bug. A promise quietly ended up with nothing answering for it, and the row still looks busy: the Code leg carries both its @covers marks, ProcedureRepository.swift:6 and GuestTests.swift:4. The braid frays between Code and Test, and the Test leg says why in plain words: "No test, and no declared debt: a silent gap."
It lands at GAP rather than tracked debt because its carrying tasks are ticked. No test and nobody admitting one is owed is exactly what a silent gap is.
Proven rows stay proven in a refused run
AC-A11Y-01 in that same refused manifest: two named tests, so its Test leg stays green and its braid stays solid. A refusal elsewhere does not un-prove this row's evidence, and the Gate's verdict is about the manifest, not about every row in it.
Rules of thumb
| You see | It means |
|---|---|
| Solid braid, green nodes | Golden Thread holds; a named test exists |
| Solid braid, amber node | Owed — tracked debt, admitted on an open task |
| Solid braid, blue node | Not yet — backlog altitude, or an honest-missing step |
| Open debt (Carries:) block | The exact open task that excuses (or carries) the incompleteness |
| ▸ file:line / ▸ test name | Expand to the actual source behind the claim |
| Frayed braid | Golden Thread broken: silent gap, or the manifest was refused as a whole |
| Requirement, Code, Test | The three legs of a walked row, in that order, top to bottom |
| Red banner, hot GAP tile | At least one AC has neither a test nor admitted debt |
One sentence version: green is proven, amber is owed debt, blue is not-yet, fray is a broken Golden Thread.
You do not have to start out totally getting all of this from docs. These samples are someone else's project; they can only mean so much. Your own work and your team's work, the stuff you've been watching and building; that's another thing entirely... Put the Loupe on it and watch it light up.
← Back to SpecAssay