The company that makes Wherefore, as a corpus that compiles. Its first asset is the site, and every asset after it goes through the same process: it is generated from facts said once, and judged on every push by the engine release it pins.
The engine lives in wherefore-build/wherefore and is published on PyPI as
wherefore-build. This repository holds what the company says and shows, and
moves at the company's pace: an engine release reaches the site only when this
repository moves its pin (requirements-engine.txt). The company's
decisions - why the site and the product are as they are - are kept in its
private repository, not here, so that none of them goes public with the site.
| path | what | kind |
|---|---|---|
facts/company.json |
the source: every fact the company states about itself, once | authored |
brand/tokens.json |
the brand: every colour, typeface and radius, once, each with its role and reason | authored |
facts/pendulum.json |
the case study's counts: a dated snapshot of the pendulum's own repository, which is private, counted once at the commit it names | captured, stamped inside |
facts/roadmap.json |
the engine's roadmap, byte for byte the roadmap.json beside its code; What's next is built from it, and wherefore check upstream fails the deploy when the engine's file has moved |
captured |
cases/pendulum/model.html |
the pendulum's interactive 3D model, the file https://chaoticpendulums.com/ embeds, copied byte for byte from the pendulum's repository at the commit case.model.taken names |
captured |
cases/pendulum/plate.html |
the pendulum's mounting plate alone: its public model opened on one part, the same bytes after one line, copied byte for byte at the commit case.part.taken names |
captured |
brand/fonts/ |
the three typefaces and their licences (SIL OFL 1.1), unchanged | authored |
brand/icon/icon-1024.png, brand/shoot_icon.py |
the icon as a PNG, for accounts that take only PNG, and the script that shoots it from site/brand/icon.svg |
captured, stamped |
gen/brand.py |
the brand pack's generator: the stylesheet, the fonts and the brand page | authored |
gen/site.py |
the site's generator: layout, filled from the facts and the pinned engine | authored |
gen/page.css |
the site's layout; every colour and face in it is a var(--…) from the brand |
authored |
gen/kit.py |
what the generators share: the sources, the draft banner, contrast | authored |
site/index.html |
the home page | generated |
site/features/, site/changelog/ |
everything it does and what's next, and every release | generated |
site/map/ |
the site's own map, written by wherefore graph run as a generator: every source, the scripts that read it, and every file they make, from a real run, in the brand's own colours (--theme site/brand/map.css) |
generated |
site/_headers |
what Cloudflare Pages serves with every file | generated |
site/install.sh |
the one-line installer wherefore.sh serves, from the pinned release (gen/install.py) |
generated |
site/brand.css, site/fonts/ |
the brand as custom properties, and the fonts it serves itself | generated |
site/brand/ |
the brand pack a person reads: every token, every contrast, the type, the icon, the licences | generated |
checks/self_contained.py |
the site loads nothing from elsewhere, and every link on it goes somewhere | authored |
checks/one_palette.py |
no colour or typeface is said anywhere but the brand, and no page a visitor receives carries a colour that is not the brand's, unless it is named as another's with the reason | authored |
checks/contrast.py |
every text colour reaches 4.5:1 on every ground it is set on | authored |
checks/brand_reasons.py |
every colour, typeface and radius says what it is for and why, so does the icon, and the brand says its idea | authored |
checks/install_line.py |
no install line on the site names the bare wherefore - another project's package; ours is wherefore-build |
authored |
checks/install_script.py |
the installer installs the pinned release with uv, pipx or pip, and refuses an old Python and an empty machine in words | authored |
checks/controls_press.py |
every control on the site does something a visitor sees, pressed cold on a phone | authored |
checks/case_study.py |
the snapshot says where and when it was counted, and each count on the page moves the page | authored |
checks/questions_turn.py |
the questions on "Why Wherefore" turn by themselves, stop on the chip a thumb taps, and hold still for a visitor who asks for less motion | authored |
checks/fits_phone.py |
every page fits a phone's width, moving or still and with "how this page is built" open, and no icon is bigger than the words it follows | authored |
checks/model_turns.py |
the pendulum's model stands beside the case study's heading and its plate beside the counts; each draws, and turns under a thumb; only their frames may run a script | authored |
checks/*_proof.py |
each check, and the gate, shown to fail | authored |
wherefore.json |
what this repository is to the engine | authored |
requirements-engine.txt |
the engine release this is built and judged by | authored |
requirements-browser.txt |
the browser library the press check drives | authored |
wrangler.jsonc |
the hosting: Cloudflare serves site/ as committed |
authored |
.github/workflows/pin.yml |
a release reaches the site by a pull request the release opens: the engine sends engine-released, this moves the pin, rebuilds, and opens a draft whose checks say what the site still has to describe |
authored |
Never edit site/ by hand. The gate fails a page that is not what its sources
make.
brand/tokens.json holds every colour, typeface and radius, with what each is
for and why it is what it is. gen/brand.py writes them into site/brand.css
as custom properties, copies the fonts the site serves itself, and builds the
brand pack at site/brand/.
- Nothing else says a colour or a face.
checks/one_palette.pyfails a#hex, anrgb()or a typeface written into a stylesheet or a generator. Change the accent in one place and every page follows when it is rebuilt. - Every text colour stays readable.
checks/contrast.pycomputes each paircontrast.pairslists and fails one under its minimum; the light scheme lists its own pairs, because on paper the accent is a fill under text, never text. The brand page prints the same numbers. - One brand, two grounds.
lightin the tokens is the same brand on paper: every role keeps its name, the neutrals invert, the accent and the caution keep their values as a highlighter, and only fail is deepened to read as text. The map is drawn on it (site/brand/map.css); the app inherits it. - Every decision carries its reason. Each colour, typeface and radius says
what it is for and why, and
ideasays what the whole look follows.checks/brand_reasons.pyfails a token without its reason. A colour that is another one in a different role (on-accentis the ground's black) sayssame_asinstead of copying a value. - The icon is shot from the brand, and held to it.
iconin the tokens is the letter, its colours and its place.gen/brand.pywritessite/brand/icon.svgwith its face embedded, so it draws the same anywhere. The PNG that profiles need takes a browser to draw, so it is captured, not generated: when the icon or a colour it uses changes, the gate fails until it is shot again -python3 brand/shoot_icon.py, regenerate, thenwherefore gate --restamp icon.
The home page says why before what, then shows it working.
- The headline and the try. Your product changes; your site, docs and deck
don't. Under it, the two lines that run a real
wherefore scanon your site - no setup, no account - beside what one printed on an example company, from the pinned release's ownreleases.json. - The picture. Your code branches, is tested and merges back; everything else forks and never comes back; with Wherefore, everything else gets code's shape.
- The demo. One fact (an example company renaming Workspaces to Projects) and the six assets that state it - the feature page, the demo video's captions, the deck, the docs, the app's button and a help article. Three steps: today, the rename, and the fix for the one asset written by hand. The page says it is an illustration, and names the real commands behind it.
- How it works, in three steps: scan, build, gate - and under it, what else it catches: three of the release's features in a line each, and the way to all of them.
- The case study: Chaotic Pendulums, the small hardware company the founder
started to test the idea - its counts, with one of its parts beside them
(
cases/pendulum/plate.html, the mounting plate, machined and anodised, turning in a frame a visitor can take hold of), and what it builds and what holds it, behind one press. - The rule. Agents already run your code's pipeline; Wherefore lets them run the rest.
- Why Wherefore: the word means why, and five tabs ask an asset of the example company "Wherefore art thou?" and show what it was built from, and why.
- Getting started: the install line and the release's own commands.
Features (site/features/) lists everything the pinned release does, each
with the release it arrived in, and what's next - the engine's own roadmap,
from the copy this site holds (facts/roadmap.json), each item marked next or
later. The site owns no roadmap: the engine's sits beside its code, and the copy
is held to it. The build stops if an item is one the pinned release already
ships: an item leaves the roadmap in the change that ships it.
How this page is built. A switch at the bottom of the screen shows, above
every part of the page, the facts it was built from and the checks that hold
it, with a key at the top. The key links the map (site/map/): the whole
chain, every source, the scripts that read it and every file they make, drawn
by the pinned engine from a real run of every generator - wherefore graph
is itself a generator of this site, which the gate runs like any command and
which wherefore graph credits rather than runs. It is not a description typed beside the page:
gen/site.py renders each part through a view of the facts that records every
fact it reads and every command it asks the engine for, and that record is what
the part shows.
Every line is in facts/company.json. A number appears in the facts once: a
line that mentions it names it as {before}, {after}, {total} and the
rest, and the generator fills it in. An unknown name stops the build.
No script runs on the site's own pages. The switch, the demo's steps and the tabs are
checkboxes, radio buttons and their labels, and gen/page.css shows what the checked one
selects; the case study's list opens as a <details>. checks/controls_press.py loads every page in a real browser at 390
by 844 with touch, presses each control on a fresh load, and fails one that
cannot be pressed, is smaller than 44 by 44, presses nothing, or shows nothing
new on the screen.
The one page that runs a script is the pendulum's own model, beside the case
study's heading. It is shown in a frame sandboxed to its script alone, which
cannot reach the page around it, and site/_headers gives its path a policy
of its own: its inline script, its own pictures and font, nothing loaded from
anywhere, framed only here. checks/model_turns.py serves the site with those
headers, and at 390 by 844 with touch and at 1280 by 900 requires the model to
be to the right of the heading, to draw on the ground the facts name, and to
turn when dragged.
The page states the product's version and names some of its commands.
None of them is typed here. gen/site.py reads them from the installed engine,
the release requirements-engine.txt pins, and refuses to name a command that
release does not have. So:
- a release that renames a command fails the build until the facts follow it;
- a new version reaches the site by moving the pin, in a pull request whose gate fails until the site is regenerated against it.
The same holds for what it does and every release:
- Everything it does lists each feature in the pinned release's
features.json, in our words (does.words). A feature with no words, or words for one the release no longer ships, stops the build. - The changelog (
site/changelog/) is built from the pinned release'sreleases.json: each release's version, date, title and sentence, a terminal card of what that release printed when it was run, and the features it brought, in our words. A release is on it the moment the pin moves to it, and the build stops if the engine names a feature in a release it does not list, or the newest release it lists is not the one pinned.
The site can never describe an engine it does not pin.
python3 -m venv .venv && . .venv/bin/activate
pip install --force-reinstall --no-deps -r requirements-engine.txt
wherefore build # regenerate site/ from the facts
wherefore gate # every file classified, every generated one current
wherefore check reach # every fact reaches what a visitor receives
python3 checks/self_contained.py # nothing loaded from elsewhere, no dead links
python3 checks/controls_press.py # every control works under a thumb (needs the browser)
python3 checks/model_turns.py # the pendulum's model turns under a thumb (needs the browser)
python3 checks/questions_turn.py # the questions turn, and stop when one is picked (needs the browser)
python3 checks/fits_phone.py # every page fits a phone, and no icon outgrows its words (needs the browser)
wherefore prove # each of those fails when it should
The press check and its proof need Playwright and its Chromium:
pip install -r requirements-browser.txt && python3 -m playwright install chromium.
Change a fact in facts/company.json, run wherefore build, and commit the
fact and the regenerated site/ together. --force-reinstall matters
whenever the pin moves, because pip leaves an installed package of the same
version where it is.
site.status in the facts is draft or launched, and the generator refuses
anything else.
- While it is a draft, the page carries a banner saying nothing is
launched, and asks search engines not to index it, both in the page and in
site/_headers. - Launching is changing that one word, and only the owner decides it. The gate holds the page and its headers to the word.
A null in the facts is a fact not decided yet. With no company.domain, the
page names no canonical address. With no company.contact, it offers no
address to write to. With no start.install, it says Wherefore is not public
yet, and with no case.quote the case study quotes no one. Each appears when its fact is filled in.
The case study's numbers - the pendulum's proofs, parts, print plates, and the
release they stand at - are not typed into the page. facts/pendulum.json
holds them as a dated snapshot, counted once from the pendulum's own
repository - private; its public face is https://chaoticpendulums.com/ - at
the commit and on the day it names.
- Nothing reads the pendulum's repository. The two repositories stay apart: the pendulum may be open-sourced or split out, and should carry nothing for this company, nor be readable from here. A case study is a dated story, so when it is retold a person counts again and rewrites the snapshot.
- Each stat names its count (
case.stats[].ofin the facts), and the as-of line fills{version}and{on}from the snapshot. A count the snapshot does not have stops the build. checks/case_study.pyrequires the snapshot to say where and when it was counted, requires every value in it to be on the page or excused with a reason, and moves each one on the page to see the page move. The release this pins sweeps only the facts for reach, so this does it for the snapshot.- The model beside the heading is a snapshot too: the file the pendulum's
home page embeds, copied into
cases/pendulum/model.htmlat the commitcase.model.takennames.gen/site.pywrites it tosite/pendulum/with a few lines that frame it - a view drawn back far enough that the whole swing stays in a small frame, then a swing, unless the visitor asks for less motion. It stands on the brand colourcase.model.groundnames. - The model and the counts are both watched. The model's captured group names
an
upstream: the pendulum's public home page, which embeds the same file.wherefore check upstreamreads it whenever the site is compiled - so before every deploy - and fails when the pendulum has moved, until the copy is taken again - re-copy the file, record the commit and day incase.model.taken, never the commit alone. The counts' group names the pendulum's/release.json, a mark that changes when a release is cut and says nothing else: when it no longer matchescaptured.release, count again - proofs, parts, plates, the release - and record the new mark with them.
.github/workflows/corpus.yml runs on every push, on a hosted runner, in about
two minutes, most of it installing the browser the press check drives. It
installs the engine release requirements-engine.txt pins from PyPI, so it
needs no secret to read it; the one secret it holds is the deploy's,
CLOUDFLARE_API_TOKEN (below).
Cloudflare Workers serves site/ exactly as committed, as static assets
(wrangler.jsonc), with the headers in site/_headers.
CI deploys it, and only after every check has passed. The deploy job in
.github/workflows/corpus.yml needs corpus, so a tree the gate or a proof
failed never reaches Cloudflare, and nothing builds on Cloudflare's side.
maindeploys to the site's address,wherefore.build, a Custom Domain declared inwrangler.jsonc(and still at itsworkers.devaddress).- Every other branch deploys to a preview of its own. The job posts the preview's address on the branch's pull request, with the commit it was built from, and updates that one comment on every push.
One-time setup (owner):
-
A Cloudflare API token made from the Edit Cloudflare Workers template (Zone Resources: All zones), saved here as the secret
CLOUDFLARE_API_TOKEN. Without it the deploy fails at its first step and says so. -
Cloudflare's own Git builds for the
whereforeWorker disconnected (Worker → Settings → Build), so that this is the only deploy. -
Nothing in the dashboard.
workers.devand Preview URLs are declared inwrangler.jsonc:main's deploy applies them, and before every preview CI applies them itself (.github/scripts/cloudflare_urls.py). They need the account to have aworkers.devsubdomain; if it has none, the deploy says so. The custom domain is declared there too, andmain's deploy attaches it. If that deploy fails on the domain for want of a permission, the token needs Zone · DNS · Edit onwherefore.buildas well. -
The other addresses redirect, by one Redirect Rule in each zone (Rules → Redirect Rules):
www.wherefore.build: the "Redirect from WWW to root" template in thewherefore.buildzone, every path tohttps://wherefore.buildwith the path and query kept, status 301.getwherefore.com: the "Redirect from one domain to another" template, every path tohttps://wherefore.build/with the path kept, status 301 - it only ever redirects.wherefore.sh: every request tohttps://wherefore.build/install.sh, status 302, socurl -fsSL https://wherefore.sh | shruns the installer (curl's-Lfollows the redirect). A 302, not a 301: a browser keeps a 301 for good, and the address may serve something else one day.
Each zone needs a proxied DNS record for its rule to see a request:
AAAA @ 100::, orange cloud on - and in thewherefore.buildzone,AAAA www 100::, since@there is the Worker's own.
site/install.sh is what curl -fsSL https://wherefore.sh | sh runs.
gen/install.py writes it from the release this site pins - the package's name
(wherefore-build), the command, the oldest Python, and the words for the two
commands it suggests next - so the installer moves with the pin. It installs
with uv if it is there, else pipx, else Python's own pip for this user (after
checking Python is new enough), and changes nothing else. site/_headers
serves it as plain text, so a browser shows it to read.
checks/install_script.py runs it under sh against stand-ins for uv, pipx and
Python, so each path is exercised with no network, and its proof plants the
wrong package, uv ignored, an old Python let through and a script sh cannot
parse. It installs nothing real until the release is on PyPI.
Previews open for anyone who has the address. The page asks not to be indexed while it is a draft.