Free guide · sending online shoppers into a showroom
Build a find-a-dealer map you own
You have resellers, and a hesitant shopper who would buy today if they could put hands on the thing. Every off-the-shelf answer to that is a paid app at $10–70/month that keeps your dealer list in its own database. The native alternative is achievable, costs nothing to run, and has roughly a dozen silent failure modes — most of which produce a page that looks completely fine and is wrong.
This is the order of operations that avoids them. It starts somewhere unexpected, because on the build this was written from, the feature was not blocked by the map. It was blocked by the fact that nobody could say which addresses a customer could actually stand in.
The deliverable
Here it is, running
Not a screenshot and not a mock — the artifact this method produces, on a real map, with working search, region zoom, distance sorting and tier filters.
The worked demo
A fictional machine-tool builder’s partner network
One self-contained HTML file — the same stylesheet and runtime that shipped, with the data layer swapped. Twenty fictional partners in real cities. Open it, then save it and read it: the brand tokens, the map and the location array are all in the one file, which is the whole architecture in a form you can inspect.
Real dealers, real addresses, live on their storefront. Every figure in this guide was measured from this build. It runs on a hosted platform, so the page around the artifact carries that storefront’s own tag stack — the artifact makes no runtime request; the shop it sits in does.
Why the demo's businesses are inventedThe reference build’s roster is twenty real companies, and their agreement was to appear on their supplier’s dealer page — not to be republished on a software vendor’s marketing site. This guide lists written permission to list each business as a gate; shipping a demo that ignored it would make the page argue against itself. The fictional roster also demonstrates the claim that matters most here — that the data layer is one swappable array — better than borrowed data would.
The uncomfortable first finding
Stark Side Gear sells rooftop-tent mounting hardware through resellers. Two hours of work on map rendering would have been wasted, because the dealer list did not exist in any usable form.
separate lists each treated as “the dealer list”, disagreeing in both directions
4
names that appeared on more than one of them
6
confirmed demo locations missing from the spreadsheet everyone treated as authoritative
3 of 4
The deeper problem was categorical. The list that was easiest to export was a dropship channel: the retailer lists the product, the order routes to the supplier, and the product may never physically exist at the retailer’s address. A locator built from that list sends customers to touch something that is not there — worse than having no locator, because it burns the customer and the dealer at once.
The gate before anything elseDo not design the presentation until you can answer “which of these records describes a place a customer can stand in, with the product in it”. If the answer is “we would have to ask them”, that is the project, and the map is the easy part that comes after.
The outcome
What you end up with
Four things exist at the end that do not exist now, and none of them are rented.
One self-contained HTML file that is the actual locator. Brand tokens, an optional motif, an inline SVG map and one array of locations. No framework, no bundler, no API key, no tile server, and no runtime request to anyone. Paste it into a code block on almost any platform and it works.
A dealer roster you can defend line by line. Human-owned, with a last-verified date and provenance on every disputed field. It is the thing your locator, your sales team and your reseller agreements should all have been reading from, and most companies discover they never had one.
A pipeline that regenerates instead of drifting. Corrections go into the roster and flow downward. Nothing is hand-typed twice, so the live map and the list in your head cannot quietly disagree.
A read-only audit that runs after you stop looking. Almost every way a locator goes bad involves no code change at all — a record flipped to draft, an access toggle, a dealer who moved. This is the thing that tells you.
What it takesThe verification work is the cost, and it is a person’s time rather than a subscription — on the reference build station 7 took longer than the map, the schema and the deploy combined. You need somebody who owns the dealer relationships and can be interrupted with “did they move or did they add”. And you need permission from each business before you publish their address and route customers to it. The full list of limits →
Fit
Does this apply to you
If three of these five are true, it does.
Your product is one people want to see before they buy — bulky, mechanical, expensive, or fitted to something they already own.
You have more than one list of dealers, and you are not certain which one is right.
Some of your accounts have a showroom and some do not, and your current list does not distinguish them.
You are paying monthly for a locator, or you are about to, and the data would live in the vendor’s database.
Nobody owns keeping dealer contact details current. If a dealer moved last quarter, you would find out from a customer.
The method
The method
Eleven stations, each with a gate — the condition that has to be true before the next one starts. Open any station for what to run, what to ask, and what to tell an assistant. Station 7 is where the time actually goes, which is why it is the one already open.
1Roster of recordhuman-ownedA plain markdown table. One row per location, a last-verified date, and provenance on every disputed field. Not the spreadsheet, not the CRM.
↓ generate — never transcribe
2Seed datageneratedEmitted from the roster by a script, with refusal gates. Carries the projected pin coordinates and the projection version that produced them.
↓ upsert, keyed on the record’s own identity
3Platform recordsplatformWhere the store keeps structured content. Survives a theme swap, which is why the expensive layer lives here.
↓ rendered by
4PresentationdisposableThe section, the stylesheet, the map geometry. Version-controlled, re-pushable, and safe to throw away.
Four layers, one owned by a human. The direction of flow is one-way. Corrections go into layer 1 and regenerate downward. Editing layer 2 or 3 directly is how the roster and the live site drift apart — and the drift is invisible, because both look fine in isolation.
Decide
Before a line of code. Both of these are expensive to reverse.
01
Establish the tier vocabulary before collecting data
A locator makes a promise about what happens when somebody arrives. Different locations keep different promises.ownerowner
The instinct is one flag: "is this a dealer?" That collapses under contact with reality, because a pin is a promise that there is a door, and behind some of those doors there is nothing to touch.
The rule that generalises: a tier is defined by what a visitor can actually do at that address — not by contract type, revenue, or how you file them internally. Ask that question about your own network and you will usually land on three to five tiers. Write them down before collecting a single record, because the answer determines which questions you have to ask each location.
Whatever tiers you choose, resist collapsing the underlying facts into one status field. On the reference build the owner supplied, over the course of an hour, all four combinations of two flags: storefront and demo unit and stock; storefront and demo unit but no stock; storefront with neither; and no storefront at all. So those two are independent booleans, and a single status enum would have forced a lie on half the roster.
Worked example, not a template. The reference build sells bulky mechanical hardware that bolts to a vehicle, so is there one on the floor is the axis that matters. Yours will be a different axis. What transfers is the question, not the answer.
demoMarker Primary pinWhat it promises a visitor Walk in and put hands on a mounted unit
resellerMarker Secondary pinWhat it promises a visitor A real location that sells it — no demo guaranteed
pickupMarker Distinct markerWhat it promises a visitor The brand's own collection point. Not a showroom.
onlineMarker No pinWhat it promises a visitor No storefront. A pin is a promise there's a door.
Tier
Marker
What it promises a visitor
demo
Primary pin
Walk in and put hands on a mounted unit
reseller
Secondary pin
A real location that sells it — no demo guaranteed
pickup
Distinct marker
The brand's own collection point. Not a showroom.
online
No pin
No storefront. A pin is a promise there's a door.
Never infer a tier from sales volumeOn the reference build the single largest account by revenue — over a third of channel sales — has no storefront and no inventory. It correctly gets no pin. Revenue tells you who sells; a locator is about who can be visited, and those are different questions with different answers.
Your criteria
List the binary facts that change what you promise a visitor — each one its own boolean field, never a new tier. Two or three is typical. Ones that came up here or are obvious neighbours: has a unit on display · holds stock · installs on site · appointment only · trained staff · carries the full range · rents as well as sells.
Gate — all true before the next station
Three to five tiers, each implying a different action for the visitor
If two tiers would produce the same card, they are one tier
Every binary fact that varies within a tier is its own boolean field
One yes/no per flag, per location, answered by a human
What to tell an assistant
What it does
Interview the owner about what a visitor can do at each kind of location. Draft the tier list and the flag list from their answers, and stop there.
What it must ask you
What can a customer actually do at each kind of location — look, touch, buy, collect, book?
Which of these facts vary between two locations of the same tier?
For each location: one yes/no per flag. Nothing else resolves these.
What it must never do
Infer a tier from sales volume, contract type, or how the business files them internally.
Collapse two independent facts into one status enum because it reads more cleanly.
Give a pin to a location with no storefront. A pin is a promise there is a door.
Invent a tier the owner did not describe.
02
Research your platform's limits before choosing an architecture
A hosting platform imposes a hard ceiling somewhere you did not expect. The cheapest time to find it is before the architecture is set.researchresearch
The limits in the table are one platform’s. Every platform has an equivalent set. The transferable lesson is not the numbers — it is that the numbers exist, that at least one of them will decide your architecture, and that the one which bites is usually the one that fails silently.
The row cap deserves emphasis because it is the quiet one. Nothing errors when you exceed it. The page simply stops showing the rest of your data, and it renders perfectly while doing so. The same store had an existing section iterating 135 entries with no pagination wrapper; roughly 85 rows had been invisible for months and nobody had noticed.
The parent→children shape is the one that will not scale. A locator reads naturally as region → locations. On a platform with a row cap, resist modelling it that way: measured on the reference platform with a 135-child parent, the pagination escape hatch lifted a global collection from 50 to 135 rows, and did nothing at all for a parent’s reference list. Items past the cap there are not merely unrendered, they are unreachable — you cannot skip to them. So iterate the flat global collection and group in the template, even though grouping by parent is the more natural data model.
And never validate coverage with a reported count. The list’s own `.size` returns 135 while the loop yields 50, so a size-based check passes on a truncated render. Count what actually rendered.
One platform's numbers. Yours will differ; that a hard ceiling exists somewhere unexpected will not.
Template file sizeValue256 KBConsequence Map geometry cannot live in the section. It ships as a separate asset.
Records per loopValue50 by defaultConsequence Silent truncation. A pagination wrapper raises it to 250 per page.
Fields per record typeValue40Consequence Budget them. The reference build used 24.
JSON field writeValue128 KBConsequence Geometry cannot be stuffed into a field value either.
Record type descriptionValue255 charactersConsequence Creation fails outright. This one was undocumented in the guides we had.— found by hitting it
Limit
Value
Consequence
Template file size
256 KB
Map geometry cannot live in the section. It ships as a separate asset.
Records per loop
50 by default
Silent truncation. A pagination wrapper raises it to 250 per page.
Fields per record type
40
Budget them. The reference build used 24.
JSON field write
128 KB
Geometry cannot be stuffed into a field value either.
Record type description
255 characters
Creation fails outright. This one was undocumented in the guides we had.— found by hitting it
Measured, with a 135-child parent. The escape hatch works on the global collection and does nothing for a reference list hanging off a parent.
Skipping past the cap in a listRows returned0 — genuinely unreachable
The list's own reported sizeRows returned135
Access pattern
Rows returned
Global collection, plain loop
50
Global collection, paginated
135
Parent's child-reference list, plain
50
Parent's child-reference list, paginated
50
Skipping past the cap in a list
0 — genuinely unreachable
The list's own reported size
135
The last two rows are the finding: a size-based check reports the true count while the loop yields the truncated one, so it passes on a truncated render.
Roughly half of any locator's schema is the same everywhere:
business_name short_name (map label) tier
street_address city state / region
postal_code latitude longitude
map_x map_y (derived — see station 5)
phone email website_url
contact_url hours last_verified
card_note internal_note (never rendered)
Two fields earn their place whatever you sellA per-record last-verified date, because stale data is the number-one documented locator failure and it decays silently. And an internal note — who to call, why a record is disputed — which is genuinely useful operational memory and must never reach the storefront. Store it, then assert in the validator that it never renders.
Your criteria
Find your platform’s row cap and find out whether its escape hatch applies to every access pattern or only some. Those are two separate questions and the second one is the expensive one to get wrong. Then budget your fields: the open half of the schema is one boolean per flag from station 1, plus whatever a visitor to your category needs before driving.
Gate — all true before the next station
The row cap is known, and so is whether the escape hatch covers every access pattern
The pagination wrapper is in place on day one, while it is still a no-op
Coverage is checked by counting rendered output, never by a reported size
Field budget agreed, with a last-verified date and an internal note on every record
What to tell an assistant
What it does
Read the platform’s own documentation for size, iteration and field limits. Report each with the source you found it in, and flag which ones the proposed architecture would hit.
What it must ask you
Which platform is this shipping on, and which plan?
Is there an existing page on this store that iterates records? It may already be truncating.
What it must never do
Assume a limit from memory. Quote it from the platform’s own docs, or measure it.
Report a limit as found without saying whether you read it or hit it.
Use array filters on the platform’s record objects without checking they support field access — they frequently return nothing and never error.
Treat a reported count as evidence that every row rendered.
Build
One self-contained file, approved by a human, then generated downward.
03
Build the HTML file first, whatever your stack
One self-contained file, generated by a script, with real data. Not a mockup — the actual renderer.mechanicalmechanical
On any stack this is the fastest path. On a hosted template platform it is also the only sane one, because the alternative is a push and a cache wait per iteration — and the page you are staring at may not be the page you just shipped.
It is not a mockup. It is the actual renderer, the actual interaction code and the actual data, and on most stacks it is also the finished artifact. What it buys is iteration in seconds, design decisions made against real records, and cheap failures: the reference prototype went through more than twenty revisions with the owner, and three scaling bugs, a projection bug and a broken pan interaction were all found and fixed before any platform code existed.
Four layers inside the one file, each independently swappable — brand tokens, an optional motif, the map, and one array of locations. Nothing above depends on a framework, a bundler, an API key or a tile server, and there is no runtime request to anyone.
┌─ Brand layer CSS custom properties. Point them at your brand guide.
├─ Motif layer Optional decorative geometry.
├─ Map layer Inline SVG. Country outline + your regions.
└─ Data layer One JSON array of locations.
.your-component {
/* Lands on ~16px against a host whose root is 10.5px.
On a host with the usual 16px root, set this to 1rem. */
--base: 1.524rem;
}
.your-component__card-name { font-size: calc(var(--base) * 1.28); }
The unit did not mean what the stylesheet assumedThe artifact drops into somebody else's site, and that site sets the root font size. The reference theme sets 10.5px, so every rem written against the usual 16px assumption rendered at about two thirds of its intended size — the component's headings came out smaller than the host's paragraphs. Nothing errors, code review passes, and it surfaces as "the text feels small", which reads like a taste problem and gets answered with a taste fix. That works slightly, which is the worst outcome, because it confirms the wrong diagnosis. Measure the host's root before you write a single size.
Your criteria
The motif layer is the most brand-specific thing on the page, and it should be yours or nothing. The mechanism generalises even when the imagery does not: define it as coordinates in the real world, project it with the same projection as the map, render it under the data layer clipped to the landmass, and scale it down as the reader zooms in — texture should recede as data comes forward. If it competes with the pins for attention, delete it.
Gate — all true before the next station
It opens in a browser and works, with no build step and no dependencies
Every colour is a namespaced CSS custom property, not a hex in the markup
Density is carried by marker shape as well as colour, so it survives a monochrome print
Type is anchored to a base the component owns, never to the host’s root
The human approves it before any platform code is written
What to tell an assistant
What it does
Generate the standalone file from the roster with a script. Measure the host’s root font size first and set the component base from it.
What it must ask you
What are your brand’s accent, surface, text and line colours?
Is there a motif that comes from the product’s context, or should we ship without one?
Which host site will this be dropped into, so the root font size can be measured?
What it must never do
Write a size before measuring the host’s root. The unit may not mean what you think.
Use bare custom property names. Namespace every token and class, or you will collide with the host.
Signal density with colour alone.
Proceed to platform work before the human has approved the artifact.
04
Decide where each thing lives, on durability not convenience
Hand-edited theme code can be lost on a template update. The records you gathered by asking humans are the part that must survive.owner + developerowner + developer
Ask one question per artifact: what happens to this on a platform or theme update? On the reference platform the answer is that code edits carry forward only if they do not conflict, there is no merge tool, and the standing advice is to keep your own copy.
The expensive part — the records you gathered by asking humans — is the part that is already safe, because structured records are stored at the shop level and are theme-independent. The presentation layer is code you can re-push. That asymmetry is the whole reason for putting the roster in records rather than in the template.
There is usually a fully update-proof option, and it usually costs more than it looks: on the reference platform it is an app extension, which needs a developer account and the standard app architecture with a hosted backend, because the no-backend template does not support it.
Your criteria
Is this long-lived infrastructure, or a feature you are shipping now? If the store already carries several custom sections, the durability question is store-wide and should not be answered for one page in isolation.
Gate — all true before the next station
Every artifact is placed by what survives an update, not by what is quickest
The records survive a theme or template swap
Anything that does not survive is version-controlled and re-pushable from a script
What to tell an assistant
What it does
List every artifact the build produces and state, for each, whether it survives a platform update and what regenerates it if not.
What it must ask you
Is this page long-lived infrastructure or a feature we are shipping now?
Does the store already have custom sections whose durability was decided some other way?
What it must never do
Put the human-gathered records anywhere that a template update can remove.
Recommend the fully durable option without stating what it actually costs to stand up.
05
Pin the projection in exactly one place
Stored pin coordinates are only correct relative to the geometry they were computed against. That coupling is invisible and total.mechanicalmechanical
If the map is a static SVG rather than a tile service, geographic coordinates have to become pixel coordinates. Most template languages cannot do that arithmetic, so it happens once, at seed time, and the result is stored per record.
Which creates the coupling: change the projection scale when regenerating the map, and every pin drifts off its region — with no error, no warning, and a page that looks entirely plausible. The mitigation is one module exporting the constants and a version string, imported by the geometry builder, the coordinate projector and the verifier. The verifier fails if any record’s stamp does not match the asset’s.
Never hand-edit a stored coordinateIf a pin is in the wrong place, the address or the latitude is wrong. Fix that and regenerate. A hand-corrected pixel value is correct until the next regeneration silently overwrites it, or worse, survives one and disagrees with every other record.
Gate — all true before the next station
Scale, translate and a version string are written down in exactly one file
The geometry builder and the coordinate projector both import it
The verifier fails when a record’s projection stamp does not match the asset’s
No coordinate anywhere was typed by a person
What to tell an assistant
What it does
Create the single projection module, then import it everywhere a coordinate is produced or checked. Add the version stamp to every generated record.
What it must ask you
Which projection and viewBox does the approved artifact use?
What it must never do
Hardcode a scale or translate anywhere except the projection module.
Hand-edit a stored pixel coordinate for any reason.
Regenerate geometry without re-projecting every record against it.
06
Generate the seed data, never transcribe it
A wrong phone number is worse than no phone number, because the customer concludes the whole page is stale.mechanicalmechanical
The approved artifact already contains the records — street addresses, phone numbers, email addresses. Retyping those into a seed file is where a transposed digit enters, and it enters silently.
So the extractor reads the approved artifact and emits the seed module, with refusal gates: a malformed email, coordinates outside the country’s envelope, an unknown tier, a duplicate name, or a record still flagged placeholder all abort the run rather than emitting a partial roster.
Then cross-check. Every projected coordinate was compared against the artifact’s own rendered output: 20 of 20 exact. That check is cheap, and it is the difference between believing the pipeline is faithful and knowing it.
Gate — all true before the next station
Zero hand-typed coordinates, addresses or phone numbers
The extractor refuses the whole run rather than emitting a partial roster
Every projected coordinate matches the approved artifact’s rendering
What to tell an assistant
What it does
Write the extractor. Validate the whole set, then emit. Report the coordinate cross-check as a count, not as a claim.
What it must ask you
Which artifact is the approved one? Anything not approved is not a source.
What it must never do
Transcribe a record by hand, however few there are.
Emit a partial roster when one record fails validation.
Report the cross-check as passing without stating how many matched out of how many.
Verify
Where the time actually goes, and the only part you cannot delegate.
07
Verify the contact data — this is the actual project
It cost more time than the map, the schema and the deploy combined. The map is the easy part that comes after.the constraint — a human, permanentlythe constraint — a human, permanently
The feature was not blocked by the map. It was blocked by the data. On the reference build 4 separate lists each claimed to be "the dealer list", and only 6 names appeared on more than one. Three of the four confirmed demo locations were absent from the spreadsheet everyone treated as authoritative.
The deeper problem was categorical. A dropship relationship — the retailer lists the product, the order routes to the supplier — means the product may never physically exist at the retailer’s address. A locator built from that list sends customers to touch something that is not there, which is worse than having no locator, because it burns the customer and the dealer at once.
Addresses drift in five distinct ways, all observed on twenty real businesses. A site contradicts itself, and the policy or contact-information page set once in settings is the most reliably stale field on the platform. The search engine’s own record is stale too, so first-party-versus-search is not a tiebreak. A directory points at the warehouse rather than the showroom, and customers arrive at a loading dock. Expansion looks identical to relocation — always ask "is this a move or an addition". And ownership changes lag everywhere: one business was acquired, its own site never mentioned it, its social page title already carried the new city, and trade press had the deal date.
Email is where automation gets confidently wrong. Search snippets produced two wrong addresses on the reference build — one pointing at a sibling brand’s near-identical domain, one a generic address that exists nowhere on the company’s site. Both would have bounced. Only ever take an address off a page you actually rendered. And check MX records, not A records: one business publishes mail on a domain that serves no website at all and delivers fine, while the domain that does serve their site has no MX record. An automated liveness check got both backwards.
Ranked by how often each was right when sources disagreed — which is the only ranking that means anything, since they agree on the easy records.
1Source The human who owns the relationshipWhat it was like in practice Overruled web research on five records and was right every time. The internet lags relationships someone manages directly.
2Source The company's own site — read several pagesWhat it was like in practice Their pages contradict each other. The policy page set once in settings is the most reliably stale field on the platform.
3Source The company's own DATED social postsWhat it was like in practice "Come see our new store at X" beats any static page. The date is the whole value.
4Source Government recordsWhat it was like in practice A city sign permit dated a relocation to the month. Nobody pulls a wall-sign permit for a building they are leaving.
5Source Search-engine mirrorsWhat it was like in practice Best available proxy when the map service blocks automated fetch — and it was stale too.
6Source Generic directoriesWhat it was like in practice Frequently a previous address. One listed a warehouse rather than the showroom.
7Source Search-result snippetsWhat it was like in practice Actively produced wrong data twice, including an address at a near-identical sibling domain.
Rank
Source
What it was like in practice
1
The human who owns the relationship
Overruled web research on five records and was right every time. The internet lags relationships someone manages directly.
2
The company's own site — read several pages
Their pages contradict each other. The policy page set once in settings is the most reliably stale field on the platform.
3
The company's own DATED social posts
"Come see our new store at X" beats any static page. The date is the whole value.
4
Government records
A city sign permit dated a relocation to the month. Nobody pulls a wall-sign permit for a building they are leaving.
5
Search-engine mirrors
Best available proxy when the map service blocks automated fetch — and it was stale too.
6
Generic directories
Frequently a previous address. One listed a warehouse rather than the showroom.
7
Search-result snippets
Actively produced wrong data twice, including an address at a near-identical sibling domain.
Ranked by how often each was right when sources disagreed:
1 the human who owns the relationship overruled research 5/5 times
2 the company's own site, several pages their pages contradict each other
3 their own DATED social posts dates are the whole value
4 government records a sign permit dated a move to the month
5 search-engine mirrors best available proxy — and stale too
6 generic directories frequently a previous address
7 search-result snippets produced wrong data twice
Park what you do not understand — never delete it quietlyA QA pass cut an unexplained fragment from an hours field — five characters nobody could read — and flagged it to ask about. It meant pickup lockers: you can collect an order well outside the shop’s staffed hours, which for a customer on a remote island is one of the most useful facts on the page. A fragment you cannot parse is usually compressed knowledge, not noise. Cutting it and raising it in the same breath cost one round trip and recovered a genuine selling point; cutting it silently would have lost it permanently, and there is no error message for a fact that stopped existing.
Your criteria
When a record carries a fact that does not fit any field you defined, that is a signal your schema is short a field — not that the fact should be crammed into the nearest one. And pick your staleness window from how fast your sector actually moves: twelve months is a reasonable default, six is defensible.
Gate — all true before the next station
Every location traceable to a source you would defend out loud
Nothing sourced only from a search-result snippet
Every conflict resolved by the human who owns the relationship
"Did they move or did they add" asked explicitly, per conflict
Written permission to list each business, which doubles as an accuracy check
What to tell an assistant
What it does
Research each location against the hierarchy, render the pages you cite, and report conflicts with their sources ranked. Hand every conflict to the human rather than resolving it.
What it must ask you
Which of these records describes a place a customer can stand in, with the product in it?
For each conflict: is this a move or an addition?
Have they agreed to be listed? Publishing an address and routing customers there should be opt-in.
What it must never do
Construct an email address from a pattern. If a business does not publish one, do not invent it.
Take an address from a search snippet rather than a page you rendered.
Infer that a business is trading from a live-looking search result.
Conflate website liveness with email liveness. Check MX, not A.
Quietly delete a fragment you cannot parse. Flag it and ask.
Return lower-confidence data without saying that a fraction of verification was manual.
08
Write a validator for the things that fail silently
Ordinary bugs announce themselves. Each of these produces a page a reviewer would sign off on.mechanicalmechanical
A missing pagination wrapper truncates at the row cap and renders perfectly. An array filter over record objects matches nothing and never errors. A boolean read without its value accessor is truthy for the field object regardless of what is stored, so every record reads "in stock". A missing storefront-access grant renders zero rows while the admin looks healthy. Records left in draft are skipped entirely, so a successful seed produces an empty page. And an internal field leaks to the storefront with nothing to stop it.
One of them takes the whole page down at once. The map payload is JSON inside a script element, and most template `json` filters escape quotes and backslashes but not forward slashes. A value containing the literal sequence that closes a script tag ends the block early; the browser parses the rest as HTML, the payload never reaches the map, and every pin disappears — not just that record’s. No linter catches it, because the template is correct. It only fires when the data happens to contain the sequence, which is why it survives every review and then appears eighteen months later when somebody imports a record from a bad CSV.
Two defences, and use both: reject angle brackets at the seed gate, since no real business name, street address or phone number contains one; and strip markup before serialising, as a second line of defence for anything already stored or edited later in an admin UI.
Test the validator, not just the codeA validator that has never failed is not evidence of anything. Make deliberately broken copies and confirm each is caught. On the reference build that took twenty minutes and 2 of the 4 broken copies PASSED — which is how the script-termination hole was found, and how a missing boolean assertion was found. Both were real defects in code that had already been reviewed. One warning from doing it: the first mutation deleted a phrase from the file's own explanatory comment rather than the actual tag, and reported a pass that meant nothing. Assert that your mutation changed what you think it changed before trusting its result.
Your criteria
If your records legitimately contain markup — a rich-text description, say — you cannot strip it. Serialize into a data attribute with HTML escaping instead and read it with getAttribute. Attribute context has no early-termination problem.
Gate — all true before the next station
Every silent failure above has an assertion
The validator fails on a deliberately broken copy of the template
Each mutation is confirmed to have changed what it claims to have changed
What to tell an assistant
What it does
Write the build-time validator, then mutation-test it. Report which mutants survived — a surviving mutant is a finding, not a footnote.
What it must ask you
Do any records legitimately contain markup? That changes how the payload must be serialized.
What it must never do
Report a validator as working without running it against a broken copy.
Trust a mutation you have not confirmed landed.
Assert on the template source instead of the rendered output.
09
Render the template locally, before you push anything
The last station added, and the one that would have saved the most time if it had been the first.mechanicalmechanical
The default loop for a hosted template language is push, wait, look, guess — and with a page cache serving a stale render for half an hour afterwards, the page you are staring at may not be the page you just shipped. You end up debugging a ghost. The fix is to render the same template file on your laptop, against the same data, in about a second.
Three properties make it worth building rather than eyeballing. It fails loudly on anything unshimmed — turn on strict filters, so an unknown platform filter throws instead of quietly rendering nothing, which would look like a pass. It can simulate the platform’s limits, turning an invisible production ceiling into a failing assertion on a laptop. And it can lie about scale: at twenty records the pagination wrapper is decoration, so a stress flag that clones the roster makes it load-bearing and removing it fails locally instead of in eighteen months. Test at the size you will be, not the size you are.
Assert on the rendered output, not the template source. Source linting checks the spelling you thought of; output assertions check the result however it was spelled.
Assert on OUTPUT:
card count == record count (at --stress size, not just today's)
the JSON payload parses
the structured data parses
every coordinate lands inside the viewBox
no internal-only field text appears anywhere in the HTML
every mailto: resolves to an address that exists in the data
boolean-driven copy appears exactly as often as the boolean is true
Your harness is not the engine that shipsEvery local emulation diverges from production somewhere; the only question is whether you find out on purpose. The reference platform’s template engine does not process backslash escapes inside string literals and the JavaScript implementation used by the harness does — so one fix was working in one engine and a silent no-op in the other, and the test output could not tell you which, because a failure looks identical to a fix that did not apply. Prefer constructs that behave identically in both engines, lint for the divergence itself, and keep a written list of what the harness cannot tell you.
Gate — all true before the next station
The template renders locally against the fixture, and against LIVE records before any deploy
The harness throws on an unshimmed platform filter rather than rendering nothing
Assertions pass at stress size, not just at today’s record count
A written list exists of what the harness cannot tell you
What to tell an assistant
What it does
Build the local render harness with strict filters on. Run every assertion against the fixture, then against records pulled live from the store.
What it must ask you
Are records edited by non-developers? If so, rendering against live data is recurring, not a pre-deploy step.
What it must never do
Report a pass from a harness that silently ignores an unknown filter.
Test only against the fixture file. A harness that only sees your fixture tests your fixture.
Assume the local engine and the shipping engine agree on string handling.
Debug the code you just changed before confirming the thing you are testing with still works.
Ship and keep it true
Gates on the way out, and one check that keeps running after you stop looking.
10
Small-scale test before batch
Dry run, commit, one record, render, then the rest — with a gate at every step.mechanicalmechanical
Records are independent of the template, so seed before you deploy: the first deploy then renders a full page instead of one lonely record, and a layout problem that only appears at twenty rows shows up on the first look rather than the second.
Make seeding idempotent and a failed batch costs nothing. Derive each record’s key from its own identity — a slug of the business name — rather than letting the platform mint one. Writing becomes upsert, so re-running updates in place instead of creating duplicates. That single property is what makes it safe to fix one bad record and re-run the whole batch, which is what you will actually want to do, repeatedly.
Validate the whole batch before writing any of it, not per-record as you go. A record that fails on write leaves the roster half-seeded, and now you are reasoning about a partially-applied change. And make the dry run work without credentials — a dry run writes nothing, so requiring a token puts the most valuable part of the script, the pre-flight validation, behind a secret. The reference build got this wrong initially: the validation gate could not run on a machine that had not unlocked the password manager, which is exactly the machine a second person reviews from.
Definition script dry run. Inspect the payload. Commit nothing.
Commit the definition. Then read it back and confirm public read access and publishable status before seeding anything.
Seed one record. Read it back from the API and confirm it is active, not draft.
Render locally against that one live record. This answers most of what a deploy would have told you, at no cost and with no cache.
Batch the rest. Re-run the verifier. Render locally again, against all of them, from the live store.
Deploy to an unpublished environment. Confirm a pin lands correctly.
Verify a scoped deploy against its source, not your working copyWhen you deploy part of a project into an existing site, the check that matters is whether anything else moved — and the tooling's "success" is not evidence. The intuitive check, diffing the deployed target against your working copy, is the wrong comparison: a working copy carries unrelated in-progress work, so a diff cannot separate "the deploy was correctly scoped" from "my whole working copy just went up". Diff the target against the thing it was cloned from. On the reference build that was 0 differing files, with the extras being precisely the ones deployed. That is a provable statement.
Your criteria
Expect the admin UI’s pickers to be scoped to what is currently published, not to the draft environment you are working in — so the thing you just built is simply not in the list and it looks like it failed to deploy. The underlying value is usually just a string on the record, and the API will set it regardless. Before working around it, look for accidental evidence: something in your system is often already doing the risky thing quietly, and has been for long enough to count as a result.
Gate — all true before the next station
Every write read back from the API, never inferred from the rendered page
The dry run works on a machine with no credentials
The whole batch validates before a single record is written
Seeding is idempotent on a key derived from the record’s own identity
A scoped deploy diffed against its source, not against your working copy
What to tell an assistant
What it does
Run each step in order and report the read-back for every write. Stop at the first gate that does not pass.
What it must ask you
Which environment is the unpublished one, and what was it cloned from?
What it must never do
Trust a rendered page as evidence of a successful write. Caches serve stale renders for a long time.
Write any record before the whole batch has validated.
Let the platform mint a record key when the record has its own identity.
Require credentials for a dry run.
11
The audit that keeps running after you stop looking
Almost every way this page goes bad after launch involves no code change at all.recurringrecurring
Station 8’s validator is a build step: it runs when you change the code. This is a different artifact with a different job. Someone flips a record to draft in the admin and it vanishes from the page with no error raised anywhere. Someone edits a record in the admin instead of the source of truth, and the next deploy silently reverts their work. Storefront access gets downgraded and the entire page empties while the admin looks perfectly healthy. A dealer moves, closes or gets acquired and nobody tells you.
Write it read-only. It must be safe to run against production at any time, by anyone, without thinking about it. The moment an audit can write, running it becomes a decision — and a check you have to decide about is a check you skip.
Separate blocking from advisory. A record in draft is a failure; a location last verified fourteen months ago is a warning. Collapsing those into one severity means either the audit blocks constantly or it stops meaning anything.
Read-only, and it asserts:
every record published, not draft the default failure mode of API writes
public read access still granted empties the whole page, silently
every required field populated a blank name renders a nameless card
derived values agree with their source catches hand-edits and reprojections
store matches the repo, field by field catches admin edits and pending deploys
no two records on the same pixel one pin hides another
tier and flags not contradictory a demo tier with no demo unit is a typo
every contact route well-formed a malformed link is invisible until clicked
nothing older than your window the station 7 problem, made visible
record count against the row cap warn early; the failure past it is silent
A checker that cries wolf stops being a checkerThe first live run reported one error and one warning and both were wrong, with the same root cause: three scripts each keeping their own idea of what a field is. The audit assumed every key on a source record was a stored field, and reported a missing field that was never meant to exist. Blank optional values are omitted on write rather than stored as empty strings, so the store holds null where the source holds an empty string — read naively, that is drift on every optional field every dealer left blank. Neither was a real defect, and that is exactly what makes it dangerous: an audit that reports noise on a clean system teaches you to skim its output, and then it is still running but no longer doing anything.
Your criteria
Find your platform’s coercions before you trust a diff. A decimal field given 321 comes back as 321.0, so eighteen of twenty records round-tripped byte-identically and the two that landed on whole numbers looked like drift. Compare numbers numerically, never as text. Common ones: decimals gaining trailing zeros, booleans stored as a string or an integer, dates normalised to UTC, and text silently trimmed.
Gate — all true before the next station
The audit is read-only and safe for anyone to run against production
Blocking and advisory findings are separate severities
The field list, the required list and the empty-value rule live in ONE module every script imports
It compares what a write WOULD produce against what is stored, not raw record against raw record
It runs clean, so a non-clean run means something
What to tell an assistant
What it does
Write the read-only audit against the shared field module. Run it, then fix every false positive before anyone is asked to read its output.
What it must ask you
What is the staleness window for this sector?
Who receives this when it fails, and have they agreed to read it?
What it must never do
Give the audit any ability to write.
Ship it while it still reports a false positive on a clean system.
Compare a stored value to a source value as text.
Collapse "broken right now" and "somebody should look" into one severity.
Evidence
Why the gates exist
Every rule in this method came from something breaking — and the thing they have in common is that each one produced a page a reviewer would have signed off on. Read cold, “wrap the loop on day one” sounds like fussiness. On the same store it had already hidden 85 rows for months.
Eighty-five rows had been invisible for months, on a page that rendered perfectly
An existing section on the same store iterated 135 records with no pagination wrapper. The platform returns 50 and stops. Nothing errors, nothing warns, and the page looks complete — so roughly 85 rows had simply not existed for anyone visiting, and nobody had noticed.
The trap underneath it is worse than the cap. The list’s own reported size returns the true count while the loop yields the truncated one, so any check written against a count passes on a truncated render. Coverage has to be measured on what rendered.
The rule this produced — Wrap the loop on day one, at twenty records, while it is still a no-op — nobody remembers at record 51. And count rendered output, never a reported size. Station 2
One record’s name could take every pin on the page down
The map payload is JSON inside a script element, and the platform’s serializer escapes quotes and backslashes but not forward slashes. A value containing the sequence that closes a script tag ends the block early: the browser parses the rest of the JSON as HTML, the payload never reaches the map, and every marker disappears — not just that record’s.
No linter catches it, because the template is correct. It fires only when the data happens to contain the sequence, which is why it survives every review and then surfaces eighteen months later when somebody adds a record from a bad CSV import. It is not platform-specific either: the same hole exists in React with a serialized payload set as raw HTML.
The rule this produced — Reject angle brackets at the seed gate, and strip markup before serialising. Use both — one covers what you write, the other covers what somebody edits in an admin UI later. Station 8
Two of four deliberately broken copies passed the validator
Twenty minutes of mutation testing on a validator that had already been reviewed: delete the pagination wrapper, leak an internal field into a card, drop a boolean accessor, remove an escape. 2 of 4 passed. Both were real defects, and one of them was the script-termination hole above.
The first attempt at the mutation was itself wrong — it deleted the phrase from the file’s own explanatory comment rather than the actual tag, and reported a pass that meant nothing. A test harness can be wrong in exactly the same way the code can.
The rule this produced — A validator that has never failed is not evidence of anything. Break it on purpose — and assert that your mutation changed what you think it changed before trusting the result. Station 8
Four more, less dramatic and just as real
Four lists, and they disagreed in both directions
The CRM's record type had no address, phone or coordinate fields at all. The sales-channel export had revenue and no addresses, because that channel is dropship and never needs one. The signed-agreements folder was accurate and stale. The rest was in the owner's head. 4 lists, 6 names in common — and three of the four confirmed demo locations were missing from the spreadsheet everyone treated as authoritative.
The address that is not a place
One "dealer" published exactly one address anywhere: a registered-agent mail drop shared by several company-formation services. Nobody works there, and a pin would have landed on a mailbox service in a state the business has no presence in. Watch for suite numbers in buildings hosting many unrelated companies, incorporation-friendly states for a business that operates elsewhere, and virtual-office language on the building’s own listing.
Website liveness and email liveness are unrelated facts
One business publishes mail on a domain that serves no website at all — and it has a valid MX record, so mail delivers fine. An automated "is this domain alive" check called it dead and would have stripped a working address. The reverse was also true: the domain that does serve their website has no MX record, so mail there would bounce. That is precisely why they publish the other one.
Check your instruments before you doubt the aircraft
The map stopped reframing after a search. The code was new, so the code was the suspect — twenty minutes of reading it and reproducing the arithmetic by hand confirmed it computed exactly the right answer, which only deepened the confusion. What broke the deadlock was testing something unrelated: a zoom verified working earlier the same day also did nothing. Two independent features cannot regress simultaneously.
The environment was lying. Frame callbacks do not fire in a page the browser is not compositing — a backgrounded tab, an offscreen iframe, some automation contexts — so every animated transition silently never ran. There is a real fix hiding in that story: anything that reaches its destination only by animating should have a way to arrive without one, because a visitor with a backgrounded tab hits the same stale state for real.
What all of them reduce toEvery one of these renders a page a reviewer would sign off on. So the method is not "be careful" — it is assert on what actually rendered, and break your own checks on purpose. A count is not coverage, a success message is not a deploy, and a check that has never failed has told you nothing.
Design
Three decisions that came from the data
Not styling choices. Each of these changed what the page promises, and each came out of looking at the actual roster.
A pin is a promise
Locations with no storefront get no marker, or a deliberately different one — a dashed ring rather than a teardrop, and no directions link. The largest account on the reference build is online-only and correctly has nothing to drive to.
Separate "can I see it" from "can I buy it"
A customer who drives two hours expecting to buy and can only look is a worse outcome than one who knew going in. Cards say either "in stock — walk out with it today" or "demo only — they’ll order yours". This is the number-one documented locator complaint, and it costs one line of copy to prevent.
The weakest tier became the most useful feature
Locations with a storefront but no demo unit are the frustrating middle. Putting a prefilled "request an in-store demo" message on those cards — addressed to the dealer, sent from the customer’s own mail client, copied to the brand — converts that weakness into demand routing. A local buyer stating purchase intent is the argument for stocking, and it lands far harder from a customer than from the supplier.
Naming the manufacturer inside that message matters more than it looks, because it gets forwarded. "Can you stock the mount" is unactionable; naming the product and the maker is a purchase order waiting to happen. The five that generalise: request a demo at this location (to them, copied to you) · a general enquiry · nothing near me (to you) · arrange a pickup · become a dealer.
Deploying it
The same eleven stations, a different tax
Stations 1 through 11 produce the same artifact whatever you are on. This is where they diverge — and being honest about it means saying that most of the hard parts are one platform's tax, not the method's.
Hosted template platform
Records live in
Native structured records, shop-level
Ceiling
Template size cap and a row cap on every loop
Render locally
Possible — needs a shimmed engine
Preview
An unpublished theme, with a cache lag on every check
Next.js on Vercel
Records live in
JSON in the repo, a database, or a headless CMS
Ceiling
None that matters here
Render locally
Native — the dev server IS the shipping renderer
Preview
A deployment per branch, no cache lag
WordPress · Webflow · Squarespace
Records live in
Custom post type or CMS collection
Ceiling
Usually none; check the size a code block may be
Render locally
Hard — usually needs a local install
Preview
Staging, if you have one
Most of the hard parts of this method are hosted-platform tax. On a framework stack everything inlines, the projection can happen at render time so the station-5 coupling largely disappears, and the local-render station is free because the dev server is the shipping renderer. What does not change: the tier vocabulary, the independent booleans, the last-verified discipline, never constructing an email address, the pin-is-a-promise rule, and every finding in station 7. Those are about the data and the people, not the runtime. Neither does the script-termination hole — that is a property of putting JSON inside a script element, and it arrives in React with exactly the same shape and exactly the same fix.
Path A — a hosted storefront platform
Pick this when: Your store is on Shopify, BigCommerce or similar, a non-developer has to be able to fix a phone number, and the records must survive a theme swap. This is the path the reference build took.
Records live in: Native structured records at the shop level (metaobjects), edited in the admin.
01Create the record type, dry run firstInspect the payload before committing anything. Then read the definition back and confirm public storefront read access and publishable status — not from the script’s own output, from the API.
02Seed ONE record, read it backConfirm it is active, not draft. API writes default to draft on most platforms and the template skips drafts entirely, so a successful seed produces an empty page.
03Render the template locally against that one live recordThis answers most of what a deploy would tell you, at no cost and with no cache. Needs a shimmed template engine with strict filters on, so an unknown platform filter throws instead of rendering nothing.
04Batch the rest, then re-render against live dataSeed before you deploy: the first deploy then renders a full page, and a layout problem that only shows at twenty rows appears on the first look.
05Deploy scoped to an unpublished themeDiff the target against the theme it was cloned from — not against your working copy, which carries unrelated in-progress work and cannot separate "correctly scoped" from "everything went up".
06Publish, point the page at the template, link it from navigationThe admin’s template picker only lists what is currently published, so the value usually has to be set through the API. A page nothing links to gets no traffic.
What bites on this path
Wrap the loop in pagination on day one, while it is a no-op at twenty records. Nobody remembers at record 51, nothing errors, and the page simply stops showing the rest of your data.
Verify against the API, never the rendered page. A page cache serves a stale render for roughly half an hour after a push or a write, and query-string cache-busting does not defeat it.
Path B — your own site or app
Pick this when: You control the HTML — a framework app, a static site, or a CMS with a custom-code block. Meaningfully simpler, and worth being honest that most of path A’s difficulty is platform tax rather than the method.
Records live in: A JSON file in the repo, a database, or a headless CMS. Decide who owns edits FIRST.
01Take the artifact as-isOne self-contained HTML file: brand tokens, optional motif, inline SVG map, one array of locations. No framework, no bundler, no key, no tile server. The demo linked above is exactly this file.
02Point the brand tokens at your ownOne block of CSS custom properties at the top. Namespace every one of them, or a host page declaring a bare --accent will overwrite yours and the failure will look like a bug in an unrelated component.
03Set the type base to a value you ownMeasure the host’s root font size before writing a single size. If the component owns its own base, it lands correctly whatever the host set; if it inherits, every number in your stylesheet means something other than what you wrote.
04Swap the data layerReplace the array. Project the coordinates with the same projection the geometry was built with — a build script, or at render time, but never by hand.
05Serve itDrop it in your static directory, or render the same markup from a component. If your platform serves files from a directory, check whether that directory is actually public — plenty of stacks gate it.
06Decide how a record gets correctedRecords in the repo mean a dealer address change is a pull request. That is fine for a developer-owned page and wrong for a marketing-owned one. Revalidate on a webhook, read from a database, or point at a CMS.
What bites on this path
The script-termination hole is not platform-specific. Putting JSON inside a script element has the same hole in React, reached through a raw-HTML prop, with the same fix.
Most of the silent failures in station 8 — the draft trap, the row cap, the storefront-access toggle — simply do not exist here, so the validator gets shorter. Keep the output assertions: card count, payload parses, coordinates inside the viewBox, no internal fields in the HTML. Those catch data problems, and data problems are stack-agnostic.
The demo is this path, running → One file, served from a static directory, with its own literal in the route allowlist because nothing in that directory is public by default.
The runbook
Rebuilding this from scratch
Everything above is why. This is what, in order — written after the reference locator shipped, so the order reflects what actually happened rather than what was planned.
Six inputs to settle before writing any code
Missing any of them produces work you throw away. The first is the one that actually blocks — and if it is unanswerable today, that is the project.
1The roster of places
Which of your records describe a room a customer can stand in, with product in it?
gate · You can name every location and say where the answer came from. Not "the spreadsheet says so."
2Tier vocabulary
What distinctions actually change what a visitor should do?
gate · Each tier implies a different action. If two tiers get the same card, they are one tier.
3Independent flags
Which facts vary within a tier — stock, install, appointment-only?
gate · Flags are booleans on the record, never new tiers.
4Brand tokens
Accent, surface, text, line, and the density shades
gate · They exist as CSS custom properties, namespaced, not hex codes in the markup.
5Motif — or none
The decorative geometry, if any
gate · It comes from the product’s context. If you cannot say why it belongs, ship without it.
6Platform limits
Row cap? Escape hatch? Does the escape hatch apply to every access pattern?
gate · Two separate questions. Answer both before choosing an architecture.
Then fifteen steps, each ending in a gate
#
Do
Gate
1
Write the roster of record — human-owned, with provenance
Every row traceable to a source you would defend
2
Verify contact data against the hierarchy
Nothing sourced only from a search snippet
3
Build the standalone HTML file
Opens in a browser and works. No build step, no dependencies
4
Get the artifact approved on look and behaviour
Approved before any platform work
5
Pin the projection constants in exactly one file
Nothing hardcodes a scale
6
Generate the seed data from the roster
Zero hand-typed coordinates
7
Decide where each layer lives, on durability
Records survive a template swap
8
Write the build-time validator
It fails on a deliberately broken copy
9
Build the local render harness; assert on output
Card count equals record count, at stress size
10
Mutation-test both
At least one mutant survives and teaches you something
11
Dry run → commit → ONE record → render locally
Every write read back from the API
12
Batch the rest; re-run the audit against LIVE data
Zero drift, zero false positives
13
Deploy scoped to an unpublished environment
Diff target against its source: only your files differ
14
Deploy to production, then link it from navigation
A page nothing links to gets no traffic
15
Stand the read-only audit up as a recurring job
It runs clean, and someone will notice when it does not
Do not start by reading a shipped template. It is the output of every decision above and it will teach you the what while hiding all the why — which is the reverse of useful when you are about to make those decisions again for a different catalogue.
Limits
What this is, and what it is not
The full version, since you have read this far. This is a method, an artifact you copy, and a set of scripts. There is nothing to install and no dashboard. The dealer relationships stay yours, which is the point and also the work.
What it is
A method, in eleven stations, each with a gate you have to pass before the next one.
One self-contained HTML file as the artifact — brand tokens, an optional motif, an inline SVG map, and one array of locations.
A four-layer separation, so corrections go into a human-owned roster and regenerate downward.
A field guide to contact-data drift, written from twenty real businesses.
A worked reference build with its own defects included, and a runbook that reproduces it.
What it is not
Not a radius search. Twenty locations fit on one screen. Search-by-postcode needs a postcode-to-coordinate table shipped as an asset — the public-domain gazetteer covers it, but it was not needed here and is not in the artifact.
Not unlimited. Past roughly 250 records the pagination escape hatch runs out and the architecture changes to client-side fetching. That is a "reconsider in years" threshold for a brand with twenty locations, not a flaw to pre-solve.
The map needs JavaScript. The geometry is too large to inline under the template size cap. The list renders server-side and works without JS; the map does not, and a failure is shown rather than hidden.
Not a data-quality service. It tells you your dealer data is wrong and gives you the hierarchy for fixing it. Somebody still has to make the calls, and station 7 is permanently a person.
Not free of manual work. Directories and map services return 403 to automated fetches or block scraping outright. A meaningful fraction of verification is manual, and any tool that says otherwise is returning lower-confidence data quietly.
No authoritative precedent exists. Despite native structured records being the obvious fit, we found no published engineering write-up of a records-backed locator at scale. The commercial apps sidestep the size cap by keeping data in their own backend.
What an assistant is never allowed to decide
Never
What it may do instead
Decide whether a location gets a pin
Report what the sources say about a storefront, and ask the owner
Construct an email address from a pattern
Link the contact page, and record that no address is published
Resolve a conflict between two addresses
Present both with their sources ranked, and ask "move or addition?"
Publish a business without its agreement
Ask — the reply doubles as an accuracy check
Hand-edit a derived coordinate
Fix the address or the latitude behind it and regenerate
Declare a deploy verified from a success message
Diff the target against its source and report the file counts
The general ruleAn assistant may gather, render, project, validate and report. It may never decide what a location promises a visitor, and it may never invent a way to contact one. Every one of those is a claim the business makes to a customer who is about to get in a car.
The working kit
Everything above is yours to use — the method, the gates, and the machine-readable version for your assistant. The kit is the part you cannot get by reading. Files that run, and the template for the part that is actually hard.
.mdThe roster of recordLayer 1 as a template, with a worked row showing what "a source you would defend" looks like written down.
.mjsThe two single-source modulesProjection constants and field rules — the two places where "written down exactly once" is load-bearing.
.mjsThe read-only auditSafe to run against production by anyone. Blocking and advisory kept apart, and coercions handled so it does not cry wolf.
▦The workbook and prompt packOne prompt file per station, and every gate tracked in a file that survives a refresh and moves between machines.