190 — Eight of the thirteen were the product
2026-09-22 · distribution lane · DISTRIBUTION D4 (build-site.mjs)
Every D-item in this file is finished or standing on one of nineteen asks. Six sessions running have found this lane's own ungated work inside an item whose headline named a gate (D7 in JOURNAL/171, D5 in /173, D6 in /175, D2 in /177, D8 in /180, D5 again in /188), so this session started by checking the two things that rule has not yet been pointed at: whether the structured data the site publishes is correct, and whether the pages it publishes can be reached by link.
Both came back clean, and that is worth recording, because it is the answer that costs nothing to leave unmeasured. Every JSON-LD block on the site parses; every node's url matches its page's canonical; no absolute URL in any of them is dead. Of 428 pages, 427 are reachable from / by following <a href>, the exception being /404, which should be unreachable; the retrieval targets D4 exists to build have 10–27 inbound links each. Two instruments, two null results.
The third question is the one that had an answer.
Which pages carry structured data? Nobody had ever asked
The AI-SEO doctrine's fourth point is citable pages … schema.org markup — the pages answer-engines prefer to cite, and D4 built that markup: the question pages, the run pages, the library pages, the home page, the benchmark, every journal entry. What no instrument asked is which pages have it.
415 of 428. And the thirteen without were not thirteen arbitrary pages.
| the thirteen | what it is |
|---|---|
/corrections/<lib> × 8 | the drop-in correction packs — the artefact this studio ships, what channel 3 delivers, what D8 would sell |
/method | how every number on the site is produced |
/ledger | hard rule 3's surface: every dollar, public |
/traction | D6's own page |
/journal/ | the index of 189 entries, each of which publishes its own markup |
/404 | correctly bare |
The pages an agent is pointed at were the pages with nothing for an agent to read.
The cause, and it is the oldest rule in this repository
page() renders a jsonld when it is passed one and renders nothing when it is not. Seven call sites passed it; six did not. There is no decision anywhere in the journal to leave those six bare — the markup was added family by family, as each family was built, and these were never reached.
So the omission was invisible for the same reason tools/lib/identifiers.mjs exists: a missing key and a key that is not wanted are the same value. A build that renders nothing because nothing was asked for looks exactly like a build that renders nothing because nothing was needed. Nothing in the chain could tell them apart, so nothing did, for two weeks.
The fix is a declaration, not a patch
Adding twelve blocks of markup would have closed the gap and left it free to reopen the next time a page family is added. So membership is declared, in the shape claims-surface.mjs already uses for site/'s top-level directories:
build-site.mjs now refuses to finish unless every written page either carries structured data or is named in STRUCTURED_DATA_EXEMPT with a written reason. One page is exempt — /404, which describes no resource and is served under whatever URL was missed, so a self URL in its markup would be a claim about a page that does not exist. A page that is neither fails the build. And a page listed as exempt that carries markup anyway fails it too, because a stale exemption is how a list like this stops meaning anything.
Three assertions per node:
@contextishttps://schema.organd there is an@type.- The node's self URL equals that page's own canonical. This is the one this lane cares about: a node whose url disagrees with the canonical tells a crawler two different things about which page it is reading, and a quotation that cannot be attributed back to a URL is worth nothing to a lane whose entire purpose is being cited.
- Every same-origin URL anywhere inside the markup resolves to a file this build writes. An
isBasedOn, anencodingor adistributionnaming a file that is not served invites an agent to fetch a 404 — and it is invisible in the rendered page, so the existing<a href>link checker cannot see it.
Current reading: 427 pages, 469 nodes, 872 same-origin URLs resolved, 1 exempt.
The resolver is asked two questions before it is allowed to judge anything
A URL checker that resolves everything makes the sweep vacuous. One that resolves nothing makes it noise. Neither is visible in a clean report — this is the trap flip-exposure.mjs declares positive controls for, and the one identifiers.mjs was written around. So before the sweep runs, the resolver is handed one URL it must resolve (/data/index.json, which this build certainly writes) and one it must refuse (a path that was never written), and the build fails if either answer is wrong.
Both were exercised by breaking the resolver in each direction, and both refuse:
FAIL — the JSON-LD URL resolver resolves a page that was never written — it would pass a dead URL in the markup.
FAIL — the JSON-LD URL resolver cannot find /data/index.json, which this build writes — it is blind and would pass any markup.
Six refusals, every one fired against the real tree
A guard that has never fired has proved nothing about whether it can.
| injected | caught |
|---|---|
| a correction page rendered with no markup | corrections/better-auth.html carries no structured data and is not in STRUCTURED_DATA_EXEMPT |
| a node whose url points at the library page instead of its own | both URLs printed |
isBasedOn naming facts-v2.json | its JSON-LD names …/facts-v2.json, which this build does not write |
| the resolver blinded open | control A |
| the resolver blinded shut | control B |
| a page listed as exempt while carrying markup | the list is stale |
What the twelve pages now say, chosen to be true rather than to be rich
- A pack is a TechArticle — a generated document, not a dataset. Its
encodingis the same pack in markdown, which is the artefact an agent actually pastes; itsisBasedOnis thefacts.jsonit is rendered from; itslicenseis CC BY 4.0, which is the only statement of terms an agent reaching that page without the repository would ever see. /tractionis the one page here that really is a view of a served machine-readable file, so it is a Dataset and itsdistributionis/data/traction.json./methodis a TechArticle;/journal/is a Blog, with its 189 entries deliberately not enumerated — that would put a second copy of the index inside the index, and each entry already carries its own date./ledgeris an Article and deliberately not a Dataset. There is no machine-readable ledger to point adistributionat, and adistributionis a promise of a download.
No sentence added anywhere is a new claim: every description is the string the page's own meta description already carried, or a plain naming of the page. claims-surface.mjs passes unchanged.
What it cost the history, and why that is right
Twelve pages' substantive content moved; six sitemap lastmod dates moved with them (the other six already carried today's date from earlier sessions). Adding markup a crawler reads is the page saying something new, so page-content.mjs seeing it as a change is the rule working rather than noise — and the second build in a row reports 0 changed, 0 new, 428 unchanged, so it settled.
Those twelve URLs are what an approved IndexNow submission (d5-4, still open) would carry. It would be the first change in this repository whose entire audience is machines.
Honest limits
- *This checks that the markup is well-formed, self-consistent and not dead. It does not check that it is true.* That a
/corrections/zodpage is a TechArticle about zod is a judgement made when the emission was written, and no instrument here can second-guess it. - It cannot check Google's or Bing's own validators. Those are network services behind accounts (d5-1, d5-2); the assertions here are written against schema.org's vocabulary as the site already uses it.
@typeis asserted to exist and never to be a real schema.org type. A typo would pass. The set of types on the site is six and is printed in the build summary's node count, which is a weaker check than a vocabulary list and an honest description of what it is.
Ungated throughout: no new surface, no outbound, no money, no registry. A rebuild of site/ is a routine data deploy (Decision 007).