Skip to content

Commit 6b0b20c

Browse files
dmealingclaude
andcommitted
feat(site): the llms mirrors are gated, and a tag cannot carry stale ones
Task 15's monorepo half. The site half is committed in the site repo and deliberately held — see the plan. The mirrors state the shipping versions in prose and the site will copy them from the release tag, so a mirror that lags makes the published site lag, silently: the copy succeeds and stale numbers look exactly like fresh ones. finish-release.mjs now refuses to tag a tree whose mirrors do not name the release. v0.24.5 would have failed that gate, which is why the site half is held rather than pushed. The test's first draft forbade any 0.x/7.x string other than the current one and failed on "were retired in 0.24.0" — a correct historical sentence. A doc is free to discuss an older release; it is not free to omit the current one, and a lagging mirror omits it because the old number sits where the new one goes. So the assertion is positive, the one-line summary is pinned separately because "mentions 0.24.5" and "says we ship 0.24.5" are different claims, and the remaining bound is stated in the test rather than hidden. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NynrRND6ZUwGvq3ZUTCfxG
1 parent be244e1 commit 6b0b20c

3 files changed

Lines changed: 123 additions & 7 deletions

File tree

docs/superpowers/plans/2026-08-29-website-self-updating-codegen.md

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2102,7 +2102,7 @@ git commit -m "feat(site): per-registry version payload (never one version strin
21022102
- Modify (site repo): `.github/workflows/deploy.yml`
21032103
- Test: `scripts/site/llms.test.ts`
21042104

2105-
- [ ] **Step 1: Write the failing test**
2105+
- [x] **Step 1: Write the failing test**
21062106

21072107
```ts
21082108
import { describe, test, expect } from "bun:test";
@@ -2128,7 +2128,7 @@ describe("llms mirrors", () => {
21282128
});
21292129
```
21302130

2131-
- [ ] **Step 2: Add the deploy copy step**
2131+
- [x] **Step 2: Add the deploy copy step**
21322132

21332133
In the same clone the Task 13 step already made — do not clone twice:
21342134

@@ -2137,26 +2137,58 @@ In the same clone the Task 13 step already made — do not clone twice:
21372137
cp /tmp/mo/docs/llms/llms-full.txt "$GITHUB_WORKSPACE/www/llms-full.txt"
21382138
```
21392139

2140-
- [ ] **Step 3: Stop tracking the mirrors in the site repo**
2140+
- [x] **Step 3: Stop tracking the mirrors in the site repo**
21412141

21422142
`git rm --cached www/llms.txt www/llms-full.txt` and gitignore them. A file that is both committed and overwritten at deploy invites someone to edit the copy that loses.
21432143

2144-
- [ ] **Step 4: Verify live**
2144+
- [~] **Step 4: Verify live**
21452145

21462146
After the deploy, `curl -s https://metaobjects.dev/llms.txt | head -20` must match `docs/llms/llms.txt`.
21472147

21482148
**State the claim honestly.** An earlier draft said this "removes 20 of the original 31 hand-edited version references." It does not. `docs/llms/llms.txt` contains literal `0.24.4` (3×), `7.24.4` (6×) and `0.24.0`; `llms-full.txt` has 10 more. Copying them de-duplicates the edit — those refs stop being maintained in TWO repos and stay maintained in ONE — but nothing here generates them from `SitePayload.registries`.
21492149

21502150
Generating them is a real follow-on (template + the `registries` block), deliberately not folded in: `docs/llms/*` is also consumed directly by agents from this repo, so templating it is a change to an adopter-facing artifact and deserves its own decision.
21512151

2152-
- [ ] **Step 5: Commit**
2152+
- [x] **Step 5: Commit**
21532153

21542154
```bash
21552155
git add scripts/site/llms.test.ts && git commit -m "feat(site): llms mirrors generated at deploy"
21562156
```
21572157

21582158
---
21592159

2160+
> **DONE 2026-08-30 except step 4, which waits on the next release — and the reason is a
2161+
> defect this task found in the tag, not in the copy.**
2162+
>
2163+
> Monorepo: `scripts/site/llms.test.ts`, plus a new gate in `scripts/finish-release.mjs`.
2164+
> Site repo: `deploy.yml` + `.gitignore`, **committed at `ef743d7` and deliberately NOT
2165+
> pushed.**
2166+
>
2167+
> **Why it is held.** `v0.24.5`'s `docs/llms/` still reads `0.24.4`: the docs refresh has
2168+
> always been a post-tag commit, and the tag was cut mid-release. Deploying the copy step
2169+
> against that tag would REGRESS the live mirrors by a release — worse than the drift this
2170+
> task removes. `finish-release.mjs` now refuses to tag a tree whose mirrors do not name the
2171+
> release, so the next tag is fresh by construction and the site half can be pushed
2172+
> immediately after it.
2173+
>
2174+
> **One structural change the plan did not call for.** The plan put the `cp` beside the
2175+
> injection, inside the guard that skips when the tag predates the injector. But step 3
2176+
> UNTRACKS the mirrors, so a skipped copy no longer leaves a stale file — it leaves **no
2177+
> file**, and `/llms.txt` 404s on a documented URL. The copy therefore runs BEFORE that
2178+
> guard, it has its own presence check, and a missing source warns rather than failing, on
2179+
> the same reasoning as the injector guard: one absent text file must not take down the
2180+
> site.
2181+
>
2182+
> **The test's first draft was wrong in a way worth keeping.** It forbade any `0.x`/`7.x`
2183+
> string other than the current one, and failed on *"`@verifiedBy` … were retired in
2184+
> `0.24.0`"* — a correct historical sentence. Rewritten to assert POSITIVELY that each
2185+
> mirror names the current npm and Maven versions, which is what a lagging mirror stops
2186+
> doing, and to pin the one-line summary separately: "the file mentions 0.24.5 somewhere"
2187+
> and "the summary says we ship 0.24.5" are different claims and only the second is acted
2188+
> on. Bound stated in the test: it passes if a mirror names the current version and leaves
2189+
> an old shipping claim beside it. Proven by staling `docs/llms/llms.txt` — both assertions
2190+
> red, green on restore.
2191+
21602192
### Task 16: `--metamodel --site` stops lying
21612193

21622194
A flag the CLI accepts and silently ignores. This ships to adopters and is worth fixing on its own, independent of the website.

scripts/finish-release.mjs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,25 @@ for (const rel of ["scripts/site-inject-ci.mjs", "examples/showcase/site-payload
132132
}
133133
ok("the deploy's injector and payload are both in the tagged tree");
134134

135-
// ── 5. the tag the DEPLOY will resolve is the one being cut ──────────────────
135+
// ── 5. the llms mirrors state this release ───────────────────────────────────
136+
// The site COPIES docs/llms/* from this tag (Task 15), so a mirror that lags makes the
137+
// published site lag — silently, because the copy succeeds and stale numbers look like
138+
// fresh ones. `v0.24.5` carries mirrors reading `0.24.4`, because the docs refresh has
139+
// always been a post-tag commit; with the tag moving to the end, it no longer has to be.
140+
for (const rel of ["docs/llms/llms.txt", "docs/llms/llms-full.txt"]) {
141+
const text = out(`git show HEAD:${rel}`);
142+
const missing = [
143+
...(text.includes(VERSION) ? [] : [`npm ${VERSION}`]),
144+
...(text.includes(MAVEN_VERSION) ? [] : [`Maven ${MAVEN_VERSION}`]),
145+
];
146+
if (missing.length > 0) {
147+
die(`${rel} does not name ${missing.join(" or ")} — refresh the mirrors before tagging, ` +
148+
`or the site publishes the previous release's versions (RELEASING-docs-checklist).`);
149+
}
150+
}
151+
ok("the llms mirrors state this release");
152+
153+
// ── 6. the tag the DEPLOY will resolve is the one being cut ──────────────────
136154
// The repo carries two tag lines and `sort -V` over a bare `v*` returns v7.20.12, which
137155
// has no examples/showcase at all. This mirrors deploy.yml's own filter, so a change to
138156
// either is caught here rather than on a deploy nobody is watching.
@@ -146,7 +164,7 @@ ok(`the site deploy will resolve ${tag}`);
146164

147165
if (CHECK_ONLY) { ok("--check: gates passed; no tag cut"); process.exit(0); }
148166

149-
// ── 6. cut it ────────────────────────────────────────────────────────────────
167+
// ── 7. cut it ────────────────────────────────────────────────────────────────
150168
execSync(`git tag -a ${tag} -m "metaobjects ${VERSION} / ${MAVEN_VERSION}"`, { cwd: REPO, stdio: "inherit" });
151169
execSync(`git push origin ${tag}`, { cwd: REPO, stdio: "inherit" });
152170
ok(`cut and pushed ${tag}`);

scripts/site/llms.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// The agent-facing mirrors the site serves at metaobjects.dev/llms.txt.
2+
//
3+
// These files are consumed two ways — read directly out of this repo by an agent, and
4+
// served by the website — so they are the one artifact where "correct in the repo" and
5+
// "correct on the site" have historically been different facts. Task 15 makes the site
6+
// copy from here at deploy so there is one maintained copy; these pin the properties that
7+
// copy assumes.
8+
import { describe, expect, test } from "bun:test";
9+
import { existsSync, readFileSync } from "node:fs";
10+
import { resolve } from "node:path";
11+
import { buildPayload } from "./payload.js";
12+
13+
const REPO = resolve(import.meta.dirname, "../..");
14+
const MIRRORS = ["llms.txt", "llms-full.txt"] as const;
15+
const read = (f: string) => readFileSync(resolve(REPO, "docs/llms", f), "utf8");
16+
17+
describe("llms mirrors", () => {
18+
for (const f of MIRRORS) {
19+
test(`docs/llms/${f} exists and is non-trivial`, () => {
20+
expect(existsSync(resolve(REPO, "docs/llms", f))).toBe(true);
21+
expect(read(f).length).toBeGreaterThan(1000);
22+
});
23+
}
24+
25+
test("neither mirror contains an absolute home path", () => {
26+
// The site publishes these verbatim. A leaked home path is the public-repo hygiene
27+
// rule failing in the one file that is served to the open internet by design.
28+
for (const f of MIRRORS) expect(read(f)).not.toMatch(/\/(home|Users)\//);
29+
});
30+
31+
test("each mirror STATES the shipping versions, so a lagging copy fails", () => {
32+
// The failure mode this exists for: a release bumps the versions, the mirrors are not
33+
// refreshed, the site copies them at deploy, and the site lags a release — silently,
34+
// because the copy succeeds and stale numbers look exactly like fresh ones.
35+
//
36+
// Asserted POSITIVELY, and the first draft of this test got it wrong in a way worth
37+
// recording: it forbade every 0.x/7.x string other than the current one, and failed on
38+
// `"...were retired in 0.24.0"` — a correct historical sentence. A doc is free to talk
39+
// about an older release. It is not free to omit the current one, and a mirror that
40+
// lagged would omit it, because the old number would be sitting where the new one goes.
41+
//
42+
// The bound, stated rather than hidden: this passes if a mirror names the current
43+
// version AND leaves an old shipping claim beside it. Catching that needs a rule for
44+
// which sentences are claims, which is what produced the false failure above.
45+
const { npm, maven } = buildPayload(REPO).registries;
46+
for (const f of MIRRORS) {
47+
const text = read(f);
48+
expect({ file: f, npm: text.includes(npm), maven: text.includes(maven) })
49+
.toEqual({ file: f, npm: true, maven: true });
50+
}
51+
});
52+
53+
test("the one-line summary names both current coordinates", () => {
54+
// The structural touch-point RELEASING-docs-checklist calls out first, and the line an
55+
// agent reads before anything else. Pinned separately from the containment check above
56+
// because "the file mentions 0.24.5 somewhere" and "the summary says we ship 0.24.5"
57+
// are different claims, and only the second is what a reader acts on.
58+
const { npm, maven } = buildPayload(REPO).registries;
59+
for (const f of MIRRORS) {
60+
const summary = read(f).split("\n").find((l) => l.startsWith("> A cross-language"));
61+
expect(summary).toBeDefined();
62+
expect(summary).toContain(npm);
63+
expect(summary).toContain(maven);
64+
}
65+
});
66+
});

0 commit comments

Comments
 (0)