Spec a no-backend site-planning studio POC on free, keyless map data

Turn a site-planning idea into a POC spec for a static map app, where constraints with provenance drive a rules-based layout and every free data source has a fallback.

Prompt advanced Tested Opus 5.5 · Nov 2026May be dated

The prompt

Paste this into a Claude chat with web search on, before any code exists. Expect a long answer; if it stops early, ask it to continue from the last section. Save the answer as docs/SPEC.md and build to it.

Fill in the blanks4 placeholders · empty ones stay as {{…}}

Example

Input:

  • domain: campground layout: tent and caravan pitches, an amenity block, internal tracks and one entrance
  • users: a small holiday-park operator's planners, on laptops, screening candidate fields
  • jurisdiction: Spain (Andalusia)
  • brief: none

Output (trimmed to three of the ten envelope rows and their register rows, four of the eight rules in the generator's header comment and three of the twenty acceptance tests):

## 4. Constraints as data
…
| Field | Unit | Generic default | Spain (Andalusia) preset | Read by |
|---|---|---|---|---|
| campingAreaMax_share | % of field | 70 | 75 (AND-02 · verified) | G7 cap; C2 camping-area check |
| amenityMaxHeight_m | m | 6 | 6 (AND-08 · verified) | massing.js height clamp; C6 |
| boundarySetback_m | m | 6 | 5 (AND-10 · check) | G1 inset and feature buffers; G8 |
…
- D26 = Decreto 26/2018, de 23 de enero, de ordenación de los campamentos de turismo (BOJA nº 27, 07/02/2018), URL `https://cdn.juntadeandalucia.es/boja/2018/27/BOJA18-027-00030-1906-01_00129362.pdf`.
…
| id | field | name | value | status | source.document | source.clause | source.url | source.quote | checked_on | reviewed |
|---|---|---|---|---|---|---|---|---|---|---|
| AND-02 | campingAreaMax_share | Camping area cap | 0.75 | verified | D26 | Art. 28.1 | D26 URL | "La superficie de acampada no podrá exceder del setenta y cinco por ciento de la superficie total del camping." | 2026-11-06 | null |
| AND-08 | amenityMaxHeight_m | Communal buildings: ground floor only, max height | 6 | verified | D26 | Art. 15.1.a | D26 URL | "Contarán tan sólo con planta baja y una altura máxima de 6 metros" | 2026-11-06 | null |
| AND-10 | boundarySetback_m | Setback from the field boundary. D26 sets 5 m only for fixed accommodation units; confirm the setback for pitches and the amenity block with the Ayuntamiento (PGOU, suelo rústico) | 5 | check | D26 | Art. 30.4 | D26 URL | "una distancia mínima de tres metros entre aquellos y de cinco metros al perímetro del camping" | 2026-11-06 | null |
…
## 5. Generator

```js
/**
 * …
 * Pure: no fetch, no DOM, no Date, no Math.random (prng.js mulberry32(seed) only). Same input -> identical output.
 * …
 * Layout rules:
 * …
 * 2. Access: entrance E = entranceOverride, else the boundary point closest to a features.roads access road (primary…track), else the midpoint of the longest boundary edge; an entrance track envelope.entranceWidth_m wide runs from E, perpendicular to its edge, to the first internal track.
 * …
 * 4. Placement order: street sides are filled in order of their start's distance from E, each from the collector outward: the amenity plot (useTable.amenity) first, then caravan pitches up to the mix.caravanShare quota, then tent pitches; leftover slots stay open space.
 * …
 * 6. Terrain (terrain.grid): slot slope ≤ useTable.caravan.maxSlope_pct takes any use; ≤ useTable.tent.maxSlope_pct takes a tent pitch only; steeper stays open space; the amenity plot needs ≤ useTable.amenity.maxSlope_pct; terrain null = flat.
 * 7. Caps: stop adding pitches before pitch area exceeds envelope.campingAreaMax_share × field area or open space falls below envelope.openSpaceMin_share × field area; tracks + amenity over envelope.commonAndTracksMax_share are reported as overshoot, never trimmed; edits (slot id → caravan|tent|open) apply last and are never undone by a cap.
 * …
 */
```
…
## 12. Acceptance tests
…
| # | Test | How | Pass when |
|---|---|---|---|
| T3 | Overpass blocked | Route `/overpass-api\.de/` aborted | Entrance at the midpoint of the longest edge, `entrance.source` = "fallback"; note shown; C7 shows "not checked"; all six complete |
| T9 | Over-limit design | Set `commonAndTracksMax_share` to 5 %; set height 9 m on the ES-AN preset | Station 4 and R5 show "… m² over (x % vs 5 %)" in red; height reads 6 m with a "clamped (AND-08)" badge; F6 and checks use 6 m |
| T12 | Constraint change marks stale | Generate; toggle one pitch; change `boundarySetback_m` | Banner "Rules changed … regenerate to apply"; layers and edit unchanged until Generate; after Generate the edit is re-applied |

Each Andalusia preset value cites its register row and status: AND-02 and AND-08 are verified with the article and a quote of the decree text that holds the number, and AND-10 has a quote but stays check, naming the Ayuntamiento, because the decree's 5 m covers only fixed accommodation units. The tests expect the access rule's longest-edge fallback with Overpass aborted, the overshoot in red with the height clamped to AND-08, and the pitch edit kept after a constraint change.

Why it works

  • Constraints are data with provenance. The envelope's numbers drive the generator and the checks, and each preset value cites the register row it came from. Three statuses keep a prototype honest: verified needs a document, a clause and a quote of the text, anything less shows as check or assumed, and the proposal prints verified rows as cited, not reviewed, until a person ticks them. The row carries its field, citation and date checked from the start, so a later research agent fills the same rows: adding AI changes who fills them, not the register's shape or the generator.
  • The rules live in the generator's header comment. Each rule names the field it reads, so a reviewer can check the layout against one list, and any field that no rule or check reads gets cut. A seeded, deterministic generator that stores its fetched data as inputs and works in a local metric frame gives repeatable output you can test, and avoids distorted geometry from doing distances in degrees. Clamping where values are read and marking the layout stale, never rewriting it, means a constraint change can't silently corrupt the user's edits or the totals.
  • Every free service has a timeout, a fallback and a swap. Keyless public data gets a working map app in a day, but those services rate-limit, hang and go down. Named fallbacks (flat terrain, a default access edge, a generic preset) keep a demo moving, layers added on style.load still appear while basemap tiles hang, and a small interface such as sampler(lng, lat) lets production change provider without touching callers.
  • Thin across all six stations, with a brainstorm that names seams. Going from site to proposal end to end proves the workflow, which is what a POC is for. Bigger ideas go in a list where each names the file or interface it would plug into, so they stay out of v0.1 but have a place to land.

Variations

  • Other domains. A logistics yard, a business park or an urban masterplan: change domain only. The stations rename and the rules change; the architecture doesn't.
  • Build it next. In Claude Code, with the spec saved as docs/SPEC.md: "Build section 10's POC cut station by station. Write the section 12 tests as Playwright tests that abort each external host in turn, and run them after each station."
  • Switch on the research agent. Use section 11 as the brief for a web-search model call, made from a small server-side function that holds the API key, that returns register rows as JSON; show each row with an accept button.
  • Generalised from a masterplanning prototype.