A page that reads an artifact must be able to reach it at render time
Affects: next.config.mjs, lib/engineering-evidence.ts, scripts/check-evidence-tracing.mjs, components/engineering/request-flow-figure.tsx, app/developers/page.tsx, lib/engineering-practice.ts
Decision
The set of files the evidence pages read off disk is declared once, as EVIDENCE_ARTIFACTS, traced into the bundle of every route that can render at runtime, and checked on every build. A route that reads an artifact is either force-static — so the read provably happens during the build — or it appears in outputFileTracingIncludes with the complete list. There is no third option, and check:evidence-tracing is what makes that true rather than aspirational.
What produced it
D-088 put the full architecture render into the body of /security, /developers and /engineering. It shipped in 622fd88, passed eighteen checks, deployed, and two of the three pages served it. /security served everything else on the page and no figure at all.
Six probes were needed to establish why, and five of them ruled out the obvious answers. It was not a stale CDN copy: the response carried x-vercel-cache: MISS and age: 0, so the origin had genuinely rendered it. It was not a missing component: a probe for the older <ArchitectureDiagram /> returned ad:1 on the same fetch that returned png:0. It was not an unshipped edit: git show HEAD -- app/security/page.tsx had <RequestFlowFigure> at line 452, in the same commit that put it on the two pages where it worked. It was not a partial deploy: all three routes changed in that one commit.
What separated the pages was one line neither the review nor the audit had any reason to look at. /engineering and /developers are prerendered. app/security/page.tsx carries export const revalidate = 60, because it shows a live uptime reading.
Why an ISR route breaks a build-time read
Next.js traces each server route's imports and ships only the traced files into that route's serverless bundle. public/ is never traced — those bytes are uploaded to the CDN, which is a different machine from the one that renders the page. For a route that is prerendered once, this costs nothing: the filesystem read already happened during the build, and its result is baked into the HTML.
An ISR route re-renders in the lambda. There the read throws, and getArchitectureRender() does what it was written to do — returns null rather than taking the page down with it. The figure disappears. Everything else on the page renders perfectly. Nothing logs, nothing errors, the build is green and the deployment is healthy.
So /security served the figure correctly for exactly one revalidation window, and then stopped, and the only evidence that anything was wrong was a byte count in a fetch.
Why the guard walks the import graph
app/security/page.tsx does not import lib/engineering-evidence.ts. It imports a component that imports it. A check that grepped route files for the module name would have passed on the one route that was broken, which is the worst possible outcome for a check — it would have converted an invisible failure into an invisible failure with a green tick beside it. So check:evidence-tracing resolves @/ and relative specifiers and walks the graph, and it treats dynamic import() the same as a static one, because a lazily imported server component reads the same files at the same moment.
The check found a second route on its first run. /developers was prerendered only by inference — nothing declared it. Any future headers() or cookies() call anywhere in that tree would have silently converted it to a dynamic route and taken the coverage section with it, in exactly the way /security lost the figure. It now declares force-static, so that change fails the build instead.
Alternative rejected: read the artifacts through the CDN instead of the disk
/architecture.png and /coverage.json are both published and both fetchable, so a server component could fetch them over HTTP and work identically in a lambda and a build. It was rejected for the reason the module's own header gives: a page that fetches its evidence at runtime is showing whatever is currently deployed at that URL, not what was committed alongside the code that renders it. A build-time read of the repository is the only version of this where the picture and the page are provably the same generation. The bundle gets 1.4 MB larger on one route; the claim stays checkable.
Alternative rejected: make /security fully static
It would have fixed the figure in one line, and it would have done it by turning off the live uptime reading — trading a true statement about availability for a picture. The uptime number is the more perishable of the two and the more useful.
The second change: the absence is now loud
RequestFlowFigure returned null when it could not read the render, which is how a whole exhibit left the page without leaving a mark. It now renders a short block saying the build could not read the diagram, calling that a defect in our deployment rather than a statement about the architecture, and linking both published files. This is the same posture /developers already takes when the coverage run is unreadable.
A failure that renders as nothing is indistinguishable from a decision not to publish. On a site whose argument is that its claims are checkable, that is the one failure mode worth spending markup on.
What would make this wrong
If Next.js begins tracing public/ into route bundles by default, the config block becomes redundant and the check starts failing on stale entries — which is the correct signal, and the decision to revisit rather than a reason to delete the check. If the traced artifacts grow to the point that they push a route over the serverless bundle size limit, the answer is to narrow the globs per route rather than to drop the tracing, and the check will name which route is affected.