How this site is built
A static site, in one repository, deployed by GitHub Pages from the dev branch. There is no framework, no build step you have to install, and no server. What there is is a release pipeline that refuses to publish a site that fails its own checks — and that pipeline is the first thing this site shipped, before it had a single influence entry to publish.
Everything below runs identically on a laptop and in CI. That is the whole design: the commands in the release checklist are the commands the workflow runs, so a release that passes locally passes in CI, and a release that fails in CI fails the same way in front of you.
The pipeline: three jobs, each gating the next
One workflow, .github/workflows/deploy-pages.yml, on every push to dev and main, on every pull request against them, and on manual dispatch.
# push to dev validate ──▶ tag-release ──▶ deploy │ │ │ │ │ └─ upload-pages-artifact, deploy-pages │ └─ git push origin refs/tags/vX.Y.Z └─ gen_*.py --check ; node admin/build/validate.js # pull request → validate only (no tag, no deploy) # push to main → validate, deploy; tag-release skips # dispatch → validate, deploy; tag-release skips
| Job | Runs when | What it does | If it fails |
|---|---|---|---|
| validate | always | Re-runs every generator in --check mode, then node admin/build/validate.js | Nothing is tagged and nothing is deployed. The live site is untouched. |
| tag-release | push to dev only | Works out this release's tag, verifies it against the version file and the commit subject, pushes it | No deploy. The release is wrong, not the site — fix the version and push again. |
| deploy | any push or dispatch, never a PR | Assembles the tree (minus .git, .github) and publishes it to GitHub Pages | The previously deployed version stays live. |
main is deploy-only, on purpose. tag-release is gated on refs/heads/dev, so a push to main validates and publishes without writing a tag. That makes main usable as a deploy test or a fallback without polluting the tag history.
The release gate
admin/build/validate.js is plain Node with no dependencies, so it runs anywhere Node exists. Six checks, in order, and any failure exits non-zero:
| # | Check | Why it exists |
|---|---|---|
| 1 | Version agreement — admin/build/version.txt against every page's version badge, the row in the versions table, llms.txt, llms-full.txt and index.md, with no version listed twice | A version that means different things in different files means nothing. The duplicate check exists because a blanket version-bump sed across a sibling site once rewrote the history table and shipped two rows with the same number. |
| 2 | Internal links — every relative href and src resolves to a file that exists, and every #fragment resolves to an id on the page it names | A static site's most common defect, and the one nobody notices until a reader does. The fragment half matters here because this site cross-links by block — a principle, a trace row, a gap spec — and a renamed heading turns every one of those into a link that lands at the top of the right page and says nothing. |
| 3 | Canonical host — every page declares a rel="canonical", and every canonical and og:url is on the host named in CNAME | A page that claims a canonical URL on the wrong host is worse than one that claims none. CNAME is the single source of truth for the hostname, here and in the generators. |
| 4 | The no-verbatim gate — every <blockquote> declares whose words it carries, and a third party's are capped at 40 words | This site's founding rule, made executable. See below. |
| 5 | The register is the data — every influence in data/influences.json has a page, every register page is in the data, and every tier count written into a page is recomputed from the data and must agree | Tier movement is the one event this site exists to record, and a hand-typed count is the first thing it makes wrong. See below. |
| 6 | The leak tripwire — nothing in the tree may look like a vault key, an AWS access key id, a GitHub token, an API secret, a private key block, a Slack token or an AWS account id | Inherited from the sibling sites unchanged. This site quotes fewer real sessions than coding.sgit.ai does, which is exactly why it is worth keeping: the check that only runs when you expect a problem is the check that is not running when you have one. |
The no-verbatim gate — this site's rule, as a check
Dinis Cruz set the rule himself: the site explains what resonates and why, and traces where it was applied — it does not reproduce the influences' own work. That instinct is also the legally correct one. This site's analysis, resonance text and trace tables are original work and carry CC BY 4.0 cleanly. The anchor works — talks, books, keynotes, music — are other people's copyrighted material, and a CC BY site that rehosted them would be relicensing what it does not own.
A rule that load-bearing is worth a gate rather than a paragraph, so:
- Every
<blockquote>on the site carriesdata-quote, naming whose words it holds. data-quote="founder"is unrestricted — Dinis Cruz's own articles, briefs and registers are his to publish, and the register format runs on them.- Anything else is a third party and is capped at 40 words: a sentence or two, quoted in order to be examined, which is the fair-dealing core.
What the gate cannot do, stated plainly. It cannot see an unmarked quotation woven into a paragraph, and it cannot tell you whether a 39-word quotation was fair. What it can do is make the marked case cheap and the unmarked case a review finding, and stop the one failure mode that actually ships: a long passage pasted in whole because it was useful at the time.
The fix for a tripped gate is to cut the quotation, never to raise the cap. That sentence is in the source of the check, next to the number, for the release where somebody is tempted.
Lyrics are the sharpest edge. Music publishers enforce aggressively even for a single verse, so the entries touching music describe and link, and quote at most a fragment under analysis. The same applies to likenesses: infographics of people use original artwork, never scraped photographs — several of these people are alive and known to this project's community, and that is a matter of respect before it is a matter of personality rights.
The register is the data
Twenty-five entries in three tiers, and the counts appear on the front page, the register index, the tiers page and llms.txt. None of those numbers is typed by a person. A page writes an empty slot —
<!-- in the page source --> <span class="tally" data-k="traced"></span> traced, <span class="tally" data-k="stated"></span> stated # chrome.py fills it, from data/influences.json # validate.js recomputes the same counts from the same file and fails on disagreement
Both halves have to be wrong in the same direction to ship a wrong count, and the check is independent of the filler rather than a re-read of its output. The reason this earns a gate at all: tier movement is this site's changelog. An entry going STATED → TRACED because a briefing document landed is the most interesting event this site records, and a hand-typed "seven stated" on three pages is what makes that event invisible.
The same check catches the two orphan cases: an influence in the data with no page (someone forgot to re-run the generator) and a page with no influence in the data (a claim with nothing behind it).
The leak tripwire
The gate scans every file in the tree, not just the HTML, for credential shapes: a vault key, an AWS access key id, a GitHub token, an API secret, a PEM private key block, a Slack token, and a bare 12-digit number — the shape of an AWS account id. The fix for a tripped wire is always to redact the snippet, never to widen the pattern. The one file exempt from the scan is validate.js itself, which necessarily carries the patterns; that exemption is what makes them writable at all.
Note what the patterns deliberately are not: none of them contains a real value. A tripwire that hard-codes the secret it is looking for has published the secret in the tripwire.
How CI decides the tag
Every push to dev is a minor release. The version is owned by one file and stated twice, and CI refuses to tag unless both agree:
# 1. the file that owns it admin/build/version.txt v0.1.0 # 2. the release commit's subject git commit -m "site v0.1.0: the pipeline, before the register" # CI then checks, in this order: newest release commit's version == version.txt # or: error, the two disagree tag vX.Y.Z sits on THAT commit # or: error, version was not bumped vX.Y.Z is the next minor after the latest tag # or a deliberate major to .0 git push origin refs/tags/vX.Y.Z # the load-bearing push
Three details are worth knowing, because each of them is a bug that was fixed once already on a sibling site:
- The release commit is not always HEAD. When a pull request lands as a merge commit, HEAD is the merge and the release commit is its parent. Every check anchors to the newest commit whose subject carries a version, not to HEAD.
- Historical tags are backfilled. On its first run the job reads the whole history and tags any commit whose subject carries a version it cannot find a tag for. It is idempotent, so it is a no-op on every run afterwards, and it checks the remote before pushing so it does not repeat work it has already done.
- A backfilled tag can be legitimately rejected.
GITHUB_TOKENcannot push a ref onto a commit whose tree carries a different.github/workflowsblob. Those are reported as warnings and the job still succeeds — the release's own tag is the load-bearing one, and a human can complete the set withgit push origin --tags.
One more trap, inherited from the shell script itself. The job reads the history once, into a variable, rather than piping git log into an early-exiting reader. Under set -o pipefail, a git log killed by SIGPIPE propagates its exit status out of the command substitution and kills the job — after the variable has been assigned, which makes it look like the next line failed. It is a race against the pipe buffer, so it passes on a short history and starts failing once the log outgrows it.
What is generated, and how drift is caught
Anything that restates something else is generated from it. A hand-maintained twin of a file is a stale artefact with a longer fuse — the sitemap that forgets a page, the "full text" file that is missing three documents, the markdown twin that still describes last month's trace table.
| File | Owns | Generated from |
|---|---|---|
admin/build/version.txt | The version — single source of truth | hand-edited, once per release |
data/influences.json | The register. Every entry, tier, anchor, principle, trace row and research plan | hand-edited; seeded from the commissioning pack |
admin/build/chrome.py | The nav and footer of every page, the version badge, and the tier tallies | one definition in the script, plus data/influences.json |
admin/build/pagelib.py | The shared page shell, the write-or-check writer, and the tiny markdown the register's prose is authored in | — |
admin/build/gen_register.py | register/ — every entry, its markdown twin, and its trace table as page, markdown and JSON | data/influences.json |
admin/build/gen_map.py | map/ — the influence graph and map/graph.json | data/influences.json |
admin/build/gen_documents.py | documents/ | data/documents.json + briefs/*.md |
admin/build/gen_llms_full.py | llms-full.txt | llms.txt, index.md, register/*/index.md, briefs/*.md |
admin/build/gen_sitemap.py | sitemap.xml | the tree, dated from the versions table |
admin/build/validate.js | The release gate | — |
Every generator has a --check mode, and CI runs all of them before the gate. A generated page that has drifted from its source is a build failure here, not a warning: the whole reason a page is generated is that nobody has to remember to update it, and a drifted page quietly breaks that promise.
Why the markdown twins are generated too
Every register entry publishes an HTML page and a markdown twin at the same URL stem, and the twin is not a second copy. Each sentence of prose is authored once, in data/influences.json, in a deliberately tiny markdown — links, bold, italic, code — and rendered twice. Two renderings of one string cannot drift. Writing the page by hand and the twin by hand is the arrangement that always does.
The release checklist
These are the commands the workflow runs, in this order, for the reason each note gives:
# 1. bump the version — exactly once per release — and add a row to admin/versions.html # 2. regenerate the pages that come from data python3 admin/build/gen_register.py python3 admin/build/gen_map.py python3 admin/build/gen_documents.py # 3. chrome AFTER the generators: it propagates the version badge, the nav and the # tallies into the pages they have just produced python3 admin/build/chrome.py # 4. the files that read the tree and the stamped twins — AFTER chrome, or they # assemble a stale version line python3 admin/build/gen_llms_full.py python3 admin/build/gen_sitemap.py # 5. validate exactly what CI validates python3 admin/build/gen_register.py --check python3 admin/build/gen_documents.py --check python3 admin/build/gen_map.py --check python3 admin/build/gen_llms_full.py --check python3 admin/build/gen_sitemap.py --check node admin/build/validate.js # 6. ship it git commit -am "site vX.Y.Z: ..." && git push -u origin dev
One inherited trap, pre-empted
The Python .gitignore this repository starts from carries build/, which silently swallows admin/build/ — a sibling site shipped a first release whose validate job died on a missing file before it could check anything. The checks appeared to run and did not. The !admin/build/ negation is in this repository's first commit.