Structure · generated surfaces
A pattern from the gf.cx specimen book
Three pages that happened to agree
A page type you have more than one of — three vehicle pages, six dashboards, every product page in a catalogue — has a structure. The question is whether that structure exists anywhere other than the files themselves. If it doesn't, the copies are being kept in sync by whoever last read all of them, and they are already drifting.
"is it part of a vehicle-pattern now?" … "yes publish the pattern and have the generators read from it"
The problem
The gf.cx vehicle pages had a shape. Contractors, then the service log, then what's specific to this vehicle, then cost of ownership, documents, receipts, Q&A, notes. One of the three was called "the target-state" in conversation and the other two were brought to match it by hand.
That arrangement has a specific failure mode, and it isn't that someone forgets. It's that nothing can tell you it happened. Over a single day of edits to these three pages, all of the following were true at once, and every one was found by a human noticing rather than by a check:
.svc-block > summaryexisted in three different versions — one corrected, one still using the originaldisplay:flexthat tears a two-word heading in half, and one half-fixed variant in between.- The page-level expand-all button had been authored inside a radar-detector accessory card, two accordions deep — so the one control that unlocks the page became unreachable the moment its own ancestors closed. Its typography was settled separately, in its own specimen.
- A status panel promised "Upcoming appointments" over content whose newest date was seven weeks in the past. A static page has no clock.
- One page had its receipts section in a different place from the other two, and had had it there the whole time.
The last one is the honest indictment. Two pages were restructured to match the third; the third was never checked against the result. Nobody could have noticed without opening all three and reading them in order, which is exactly the work the pattern was supposed to remove.
The pattern
Write the structure down as data, put it where it can be read, and give it two consumers: the generators that build the pages, and an audit that fails when a page stops matching.
vehicle-page-structure/spec.json ← authored once, published
├── defaults details_open_on_load, toggle-all placement
├── type_scale section 26px · nested record 20px · caption 11.5px
├── order the canonical section sequence, with slots
└── guards which tool checks what
The spec is a file in the pattern's own directory, so
patterns.gf.cx/vehicle-page-structure/spec.json
serves it to anyone — human or agent — who wants the structure without a
checkout. The generators read it from disk, never over the
network. That is the same rule the favicon control plane follows: the manifest is
authored in the repo, read locally, and published for legibility. A
generator that needs the internet to lay out a page is a generator that breaks on
a plane.
Relative order, not absolute
The obvious spec is a list of sections in order, and it is the wrong one. Real instances differ: one vehicle has no Q&A section, only one has classic registration, only one has installed accessories. A spec that demanded an exact list would be wrong about every page it described, and a check that is wrong every time gets switched off within a week.
So the spec ranks sections and the audit checks relative order: the sections a page actually has must appear in the canonical sequence. Anything vehicle-specific goes in a declared slot — one after the service log, one after documents — which says "your own content belongs here" without pretending to know what it is.
{ "key": "vehicle-detail", "slot": true,
"aliases": ["installed accessories", "acquisition gallery", "current condition"],
"note": "what this particular vehicle has that the others do not" }
Matching a page's heading to a spec key is substring-against-aliases, not
equality, because real headings carry counts, dates and vendors — Service
invoice #248282 — 22 Jun 2026, Flemington BMW. One trap worth naming:
longest alias wins. "Harvest service log" contains "service log",
so a first-match scan files it under the wrong section and then reports the right
one as missing. The first run of this audit did exactly that.
Prove the guard on the bugs it was built after
A new check that reports "all clean" on its first run has told you nothing. It passes on a conforming page and on a page it cannot parse, and those look identical from the outside.
The test is free if the defects are in version control. Run the audit against the pages as they were before each fix:
$ git show 041177231:pa/vehicles/bmw-2013.html > /tmp/before.html
$ pa_vehicle_spec.py /tmp/before.html
⚠ 8 section(s) ship open, spec says all closed — Seeking contractors, …
⚠ toggle-all is nested 2 deep inside "Uniden R4 In-car radar / laser
detector · windshield-mount" — unreachable once that section is closed
Every defect fixed by hand, reproduced from history, naming the exact card
the button was buried in. That is the run that earns the green on the current
pages. The same discipline caught the companion date-audit shipping a false
green — it stripped \xa0 while the pages write a literal
entity, so it parsed zero dates and passed everything.
Reading it from the generators
Publishing the spec is half of it. If the generators still hard-code the type scale, the spec is documentation, and documentation drifts from code silently — which is the original problem wearing a hat.
from pa_vehicle_spec import load_spec
_SPEC = load_spec()
SECTION_LABEL_PX = _SPEC["type_scale"]["section_label_px"]
OPEN_ON_LOAD = _SPEC["defaults"]["details_open_on_load"]
load_spec() raises rather than falling back to built-in
defaults. A silent fallback would reintroduce exactly the drift the file exists
to stop, and would do it invisibly.
Two checks make the wiring real. First, regenerate and confirm the output is byte-identical — that proves the spec describes what the pages already are, rather than what you remember them being. Second, and easy to skip: change a spec value and confirm the output moves. Byte-identical output is also what you get from a spec nothing reads.
$ PA_VEHICLE_SPEC=/tmp/spec-with-caption-99.json regenerate
font-size: 99px
$ regenerate # real spec
font-size: 11.5px # …and byte-identical to baseline
What it caught on day one
Writing the spec found a live drift immediately: the LR4's receipts section sat after the harvest log, where the other two pages had it before Q&A. It had been there through every prior fix.
It also found a rotted anchor nobody could have seen. The receipts generator
inserted its block before the first <h2 class="qa-section__label">
— an element that no longer exists on any of the three pages, because Q&A
became a <details>. The fallback would have dropped the block
at the end of the page. It never showed, because the region markers always
existed and the replace branch always won. The cold-insert path had been broken
for as long as it had been unnecessary. It now anchors on the spec: the first
section canonically after receipts.
Changing the order is now a one-line edit
The first real change arrived the next day: the service log should open the page, above "Seeking contractors". It is the vehicle's primary record, and it carries the expand-all control — which a reader wants before scrolling, not after a section they may not care about.
Before the spec, that was an edit to three files and a hope. Now the order lives in one place, the pages are brought to match, and the audit is what says whether they got there. Run it against the spec before the swap and it names both pages and the direction of the fault:
⚠ bmw-2013.html: "Seeking contractors" is out of canonical order
— spec puts it before "Service log + Expand all", the page has it after
⚠ ford-f250.html: … the page has it after
✓ LR4.html conforms
LR4 passes because it has no contractors section at all — which is the case that the relative-order rule exists for, and the reason an absolute list would have been wrong here. Same run after the spec bump: three greens. A guard that only ever prints green tells you nothing; this one had already told the truth about the pages a minute earlier.
Where it applies
- Any page type with more than one instance that a generator writes into — the more instances, the sooner hand-sync fails.
- Especially where regions are generated. A section reorder
has to move the whole marked region, because the region usually owns a
<style>and a<script>as well as markup. The spec is the right place to say so. - Skip for a one-off page. A spec describing a single instance is a second copy of that instance, and now you have two.
Reusable elements
- The published spec — authored on disk in the pattern's directory, served at a stable URL, read locally by every consumer.
- Ranked order with slots — canonical sequence plus named openings for per-instance content, so the check is right about instances that legitimately differ.
- Longest-alias matching — headings carry live data, so map them by substring, longest first.
- Length-preserving noise stripping — blank out comments and
<style>/<script>with spaces rather than deleting them, so offsets stay valid in the original document. These pages mention<details>inside a CSS comment; deleting shifts every position after it. - Warn, don't fail — findings print on every regeneration;
--strict-structureturns them into an exit code. A run that refused to write would block the very regeneration that fixes the drift.
Reference
- Origin
- home.gf.cx vehicle pages, 2026-08-12 — after a day of hand-syncing three pages, Dan asked whether the structure was "part of a vehicle-pattern now". It wasn't.
- Spec
spec.json— v1.2.0, the machine-readable companion to this page.- Specimen
- The control that inherited the wrong voice — the A/B/C that set the toggle-all's type, with ink gaps measured live in the browser.
- Reference impl
~/bin/pa_vehicle_spec.py(loader + audit); read byvehicle_receipts_render.pyandpa_vehicle_mileage_chart_inject.py. Live at home.gf.cx/vehicles/ford-f250.- Failure it prevents
- Instances of a repeated page type drifting apart in ways only a human reading all of them in order could detect — and therefore not detected.
- Related patterns
- The plate that outgrows its column — one value, published once, consumed by everything downstream. Built-with provenance footer — the same model applied to page provenance.
- Build cost
- One JSON file, one ~200-line module, three import lines. No runtime cost — nothing ships to the browser.