ArtStroy logo
ArtStroy qa · ai · engineering
AI Coding · September 22, 2026 · 8 min read

Your Import List Is Not Proof That You Reused Anything

One module imported four others and executed zero lines from one of them. What it costs to assemble nine independently built parts, measured instead of claimed.

Nine machine parts laid out on a bench, three of them still wrapped

The final stage of a course is supposed to assemble everything built along the way. Mine was going to be exactly that: a support agent importing the mature pieces from nine earlier stages.

Reading the code killed the thesis before I wrote a line of it. Stage 6 — the deployed platform, built four stages earlier — already opened like this:

from stages.s01_agent_loop.loop import run_agent
from stages.s02_rag.documents import PUBLIC
from stages.s03_router.graph import run_graph
from stages.s05_memory.decision import Decision, Situation, decide

Four stages already imported. “The capstone imports the mature parts” would have described something that happened four stages ago.

But the same block contains the real thesis, and it took a measurement to see. From stage 2 that file imports one namePUBLIC, an access-level constant that travels onward as an argument. Search, embeddings, the access filter, everything stage 2 exists for, never run.

“Imports” is not the same as “uses.” And the import list hides the difference perfectly.

Working code: stages/s10_capstone/ at tag stage-10. 32 checks, 16 of them on failure modes. Runs offline, no API key.

Executed lines, not import lines

If the import list is not proof, something else has to be. The instrument already existed: the previous stage measured how many lines of a framework’s package execute on a request, to price the scaffolding you do not write. Point it at your own modules instead:

s01:   30      s05:   51
s02:   25      s06:   29
s03:   30      s08:    8

Six stages execute. Three do not, deliberately, and sit in a separate list with a reason each: MCP tools need a running server, voice needs gigabytes of weights — and the third one is the finding I did not expect.

The instrument was measuring itself

Stage 9 — the framework comparison, and the source of the line counter — sat among the assembled parts and reported a non-zero number. Exactly one line. Modest, plausible, and easy to read as “it participates a little”.

Then I ran the measurement over empty work:

assemble.measure(lambda: None)   # {'s01': 0, ..., 's09': 1}

Nothing happened, and stage 9 still reported one. That line is sys.settrace(previous) — the counter switching tracing back off inside its own finally. The instrument was measuring itself and reporting the result as work.

“Measures” is not the same as “uses,” and the distance between them is exactly the distance between “imports” and “uses”.

It moved to the not-wired list with that sentence as its reason. The general lesson is cheaper than the specific one: run your measurement over no work and see what it reports. One line, and it falsifies a whole class of instrument.

The price of assembly, in one unit

Nine modules designed independently do not join for free. The joining code is the cost, and the cost is two numbers:

stage lines executed:   173
adapter lines:          12 of 16 written  (7%)

The first version of this measurement was wrong in a way I find genuinely embarrassing, because it is the mistake the whole stage is about. Adapters were counted statically, by parsing the code. Stage lines were counted dynamically, by execution. The numerator said is in the code; the denominator said runs. Printed side by side they looked comparable, and they were not — the same substitution the stage exists to expose, inside the stage’s own instrument. An independent reviewer found it; I did not.

Both are executed lines now, and the gap that remains is informative rather than an error: 16 written, 12 executed. The missing four belong to the adapter that builds the search index, which runs once at startup and never on a request. It costs code and costs nothing per request, and only a measurement in one unit can say so.

There is a limit past which this stops being assembly: if the joining code weighs as much as the parts, you are not assembling, you are rewriting, and the claim that the parts were mature is no longer testable. A fifth is the line I drew, and there is a check on it.

The rule that is hardest to keep

During assembly you will always find a part that one small edit would make more convenient. The edit is cheaper than an adapter, cleaner to look at, and genuinely improves that part.

It is forbidden anyway. A part you had to change disproves “the parts were mature”, and the change reaches that stage’s lesson, its checks, its tag and its published article. So every mismatch goes into an adapter, where it becomes a number — and the desire to edit the part goes into a report, naming the stage.

Which is why the report matters more than the service. Mine holds seven entries. Three of them:

  • Two different classes named Answer, in stages 2 and 6, carrying different things. Neither stage was wrong on its own; the error appears the moment they stand next to each other. The assembled service calls its own reply Reply for precisely this reason.
  • Stage 5 requires a Situation it does not fill in. Its checklist takes the properties pre-classified — “a human classifies” — so stage 6 wrote a private _looks_like to do it. The capstone refused to write a third classifier of the same thing and imported the private name of another stage instead. That smells, which is why it is in the report rather than quietly in the code.
  • Found documents reached the model with no fence. Stage 2 wraps retrieved text in explicit data markers, with an instruction saying the block is material rather than instructions, and it checks for them. The capstone glued the document to the question with a blank line — reopening the gap stage 2 had closed, in the one place where all the parts finally stand together.

An empty “what assembly revealed” section would be the most suspicious possible outcome, so there is a check that the section is non-empty and that its entries name stages.

Justifications that are parsed, not read

Every architectural decision in the assembled service cites a source stage. That is not a bibliography: a small parser reads the document and asserts that the stage exists and that the named decision record exists.

The reason is specific. Twice in this repository a plausible sentence pointed nowhere and aged silently — a message citing a decision that stage had never made, and a table contradicting the measurement block in its own file. Both were found by review, never by their author, because nobody executes prose.

The parser had the same disease. A row it could not read — three columns, an escaped pipe, missing spaces — simply vanished from the parse, taking the dangling citation inside it along. A skipped row was indistinguishable from an absent one. An unreadable row is now a defect rather than silence, and the second table, which also names stages, is checked by the same code.

The limit is stated out loud: the check knows the source exists, not that it contains the decision. The second needs understanding; the first already catches every error that has actually occurred here.

The second deploy cost no adapter at all

The assembled service has no HTTP layer of its own. It takes the application from stage 6 and substitutes itself into it.

That worked for a reason worth naming. Reply is deliberately not called Answer — and it satisfies the stage 6 application’s contract completely: ok, text, trace_id, branch, kind. The two stages agree on shape, not name, which is why the substitution costs zero adapters.

One field was missing. retry_after, which the HTTP layer puts in a header. It surfaced not in design review but on the first request, as an AttributeError — and that is the honest way to find out what a contract really requires.

What this deliberately does not prove

  • Executed lines describe this request and this thread. The limit is inherited with the instrument.
  • Three stages are not wired. Named with reasons, not forgotten — and zero for a declared part reddens the suite by name.
  • Six scenarios are not coverage. They show assembly; the case set lives in the evaluation stage.
  • “An adapter never decides” is checked by shape. It catches if and the one-line conditional; something that decides via a dictionary would pass.
  • The live deploy stays NOT EVALUATED. It needs a real machine, and green there would be green for the unverified.

Numbers

6 parts execute, 3 deliberately not wired
173 stage lines executed against 12 adapter lines
32 checks, 16 of them on failure modes
12 mutation exercises, five of which are holes the mutation pass found itself

The assembled service, the measurement, the parsed justification document and the 12 mutation exercises are in stage 10 of the course repository, pinned to the tag. The most useful file is not the service — it is ARCHITECTURE.md, and specifically the section listing what went badly.