Skip to content

Facts, decisions and the contract

hiiu splits what it can read from a repo, what it has to be told, and what it compiles from the two, and keeps the three in separate documents.

Three documents sit under everything hiiu does, and the split between them is the reason the pipeline can be automated at all.

Facts are what the repo says

hiiu analyze reads a directory and reports what it is: the framework, the router, the routes, the API handlers. Nothing in that document is a judgement. It touches no network, runs no model, and two runs of one input are byte-identical, so it can be cached against a revision and trusted without a staleness check.

The layer's contract is that it never holds an opinion. That is a stronger constraint than it sounds, and it is enforced by one deliberate absence: the analyzer never picks an app. The path you give it is the app, even inside a monorepo with exactly one candidate. Which app to build is a decision, and analysis is the layer whose whole job is not to make one.

The workspace readers exist only to make the refusal actionable. They take the union of every workspace declaration they can find, deliberately without a precedence rule, because ranking package.json workspaces against a pnpm-workspace.yaml is only meaningful if the answer selects something. Nothing branches on that list. Over-listing costs a glance; under-listing costs the answer.

The IR is what the facts compile to

hiiu ir turns those facts into the app's declared API surface as a single document: every endpoint hiiu believes the app can call.

Two sources feed it. The router's own claims, and any MSW handlers the repo already has, which state templates of their own and arrive with msw-handler provenance. Where both name one template they merge.

An endpoint is identified by a path template, not by a URL. That distinction is what stops the discovery loop running forever: if /invoices/inv_9f2c1b were treated as a literal path, every rebuild would seed different ids, every crawl would find fresh gaps, and the loop would never go dry. The recogniser that decides a segment is an id and the generator that mints ids are one contract, and neither changes alone.

A repo with no declared API surface compiles to zero endpoints and exits 0. That is a true answer, not an error.

Decisions are what analysis cannot compute

Some things are not in the repository at any depth. Which app in the monorepo. Who the persona is. Which routes matter enough that a denied request on one should fail the build.

hiiu decisions proposes answers, and that verb is load-bearing: a proposal is not a confirmation. Given --decisions <file> it does the opposite job and checks an existing document against the repo, exiting 3 and naming the contradiction if the two disagree. That is how a decisions file that was right last quarter fails loudly when the repo moves under it, rather than quietly building the wrong app.

Why the split matters

Because it is what lets the automated parts be automated. Facts regenerate from the repo on every build with no human in the loop, and cost nothing to recompute. Decisions are asked once, written down, and checked from then on. The maintenance agent can redo the first freely, and knows it must never silently redo the second.

Read next: the interception overlay, which is what the contract is compiled into.