commit 9c05b02eff32397f1d0e55b23f0d976ad39e0ee2
parent 388bbeb6a543244523bac0c907089a937b292f2b
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Tue, 6 Oct 2026 10:35:20 -0400
plans: release 18 — S2's wiring round (the merges, the deploy stages wired, the hub blocker); the changelog's hub bullet
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
2 files changed, 55 insertions(+), 0 deletions(-)
diff --git a/editor/CHANGELOG.md b/editor/CHANGELOG.md
@@ -2,6 +2,7 @@
## [Unreleased]
- **Deploys are pinned and checked live.** wrangler is an exact dependency of the workspace (4.147.0), so a deploy runs the version installed with the code instead of whatever `pnpm dlx` fetched that day, and every deploy names its branch: production is `--branch main`, never taken from the checkout it ran in (where a "production" deploy from a feature branch used to land as a preview). The publish stages' deploy (release 18) refuses before wrangler runs when there is no Cloudflare credential at all — "set CLOUDFLARE_API_TOKEN in .env" — and says "REFUSED by Cloudflare — the API token was not accepted" when Cloudflare rejects one; it refuses a production deploy of a build made from a branch other than `main`. After each deploy it reads `corpus.json` at the site's address twice, as a visitor would and cache-busted, and records the verdict: ok, stale-edge (the deployment is right, Cloudflare's edge still serves an older copy), mismatch, or unreachable. A verdict short of ok is a warning in the log; the deploy itself succeeded. What each target last shipped, where, and how it read is kept in `deployed.json` beside its build.
+- **The hub builds again.** Since 2026-10-05 every hub build was refused as "still carries a site's data (reports, m)": the export app's own report and moment pages are part of every build, the hub's included. A hub build is now refused only for report data a site's build wrote — a report index, a report's page, citations, exports or history, a moment — and still for any other site data.
- **Withdrawn X posts ship tombstones.** While X posts are private, a public site's build no longer just leaves an X channel's posts out: at every path they were served from it ships an empty stand-in — the channel's posts manifest with no pages, and an empty page for each page the channel has — served uncached. The hub, which carries no posts, ships the same for every X channel a public site carries, with an empty posts manifest; a channel only private sites carry is never named on the hub. Leaving a path out of a deploy does not take it off Cloudflare's edge, which kept serving a withdrawn copy for up to a week; a changed object at the same path replaces it. The hub's deploy reads each of those paths back.
- **Publishing is stages, from the command line: `archilyzer publish`.** `publish index` updates the index — the LMDB index, the stats datasets and the chart templates, in one child process with an 8 GB heap — and writes an index stamp (`export/.export-index/stamp.json`) naming, for each site, a signature of everything that site's build reads. `publish build <id|all>` builds a site from that index (no data phase of its own) into its own bundle, `export/.export-builds/<id>/out`, and stamps it (`built.json`); a site whose bundle already matches the index is a no-op unless `--force`. `publish deploy <id|all> [--preview <branch>] [--to local]` ships that bundle — to Cloudflare Pages, or with `--to local` into the directory the docker `site` service serves — and records the deploy (`deployed.json`); deploying the same build again is a no-op unless `--force`. `all` passes over private sites and, to Pages, sites with no Pages project; any other site it cannot deploy is a failure, said after the rest are tried. `publish hub [--deploy]` and `publish homepage [--deploy]` do the same for the hub (`_hub/out`) and the homepage. A stage whose input is not there says so and exits 3: "update the index first", "no build of jeralyzer — archilyzer publish build jeralyzer". Production refuses a bundle built on a branch other than `main`, or with no branch recorded (a detached checkout; an image sets `ARCHILYZER_BRANCH`) — a preview of it is fine. Exit codes: 0 done or nothing to do, 1 failed, 2 usage, 3 precondition not met, 130 cancelled.
- **One publish at a time on a machine.** Every stage takes `export/.export-builds/.publish.lock`; a second one — an `archilyzer publish` beside the editor, say — waits for it, saying once whom it waits for, and Ctrl-C ends the wait. A lock left by a process that is gone is taken over. A cancelled stage takes the whole process tree it started with it (`next build`'s workers, wrangler, docker).
diff --git a/plans/release-18.md b/plans/release-18.md
@@ -609,6 +609,60 @@ Gates after the fixes: tsc (all workspaces) clean; **common 3,199/3,199**, 135 s
| Run | At | Specs | Result |
|---|---|---|---|
| 4 (editor) | `801124c3` | the five specs of run 1 | **29 passed**, 0 failed, 2.4 min (no queue wait) |
+
+#### Wiring round (after S1 merged, 2026-10-06)
+
+`r18/integration` merged twice: at `0ce00f76` (the plan, S5's image half, Node 22, the host id, S1) and at `de1b9174`
+(S5's second half).
+
+| Commit | What |
+|---|---|
+| `3b467ac3` | merge of `r18/integration` `0ce00f76`. `editor/CHANGELOG.md` and this file: both sides kept. `envVars.ts`: S5's `CLOUDFLARE_API_TOKEN`, `XDG_CONFIG_HOME`, `ARCHILYZER_HOMEPAGE_OUT` rows kept, S2's marked copies deleted, their `readBy` (and `ARCHILYZER_SITE_OUT`'s) name `pagesDeploy.ts` / `deployStage.ts`; ENVIRONMENT.md regenerated. `stamps.ts`'s local `imageBuildFacts` re-exported from `lib/envVars.ts` |
+| `553a94ed` | **the wiring.** `stageBodies.ts`: `deploy-site`, `deploy-hub` and `deploy-homepage` run `runDeployStage` end to end (bundle guards, credential preflight, R2, the pinned wrangler through `wranglerBin`, the live check, `deployed.json`, `--to local`); S1's interim deploy wrapper — the `build.ts` deploy calls, its own local copy (`publishLocal`, `localSiteOut`, `localHomepageOut`) and URL watcher — is gone: one implementation. `stageRun.ts`: a `DeployStageError`'s exit code is the stage's, and its sentence (logged by the stage) is not printed again — the runner adds only `[stage] <kind> <target>: FAILED (exit 1)`. `deployStage.ts` / `liveCheck.ts` now import `BuiltStamp`, `DeployRecord`, `DeployedFile`, `LiveCheck`, `Probe` from S1's `stamps.ts` and use its readers and `recordDeploy` |
+| `6bccf923` | **the hub blocker — found on main 2026-10-05, fixed here** (below) |
+| `f1541070` | merge of `r18/integration` `de1b9174` (S5's second half): `stamps.ts` takes S5's one-line `imageBuildFacts` re-export; `envVars.ts` merged clean (one row per name, `readBy` combined); ENVIRONMENT.md regenerates unchanged |
+| this commit | `plans:` this table; the changelog's hub bullet |
+
+**What changed to fit S1's shapes (S1's win):** `builtAt`, `at` (DeployRecord, LiveCheck) are ms numbers, not ISO
+strings; `Probe.age` is a number of seconds; `built.json` is read through S1's strict `readBuiltStamp` (a stamp missing
+any field is no build) and `deployed.json` written through `recordDeploy`. Production now also refuses a build with
+**no** branch recorded (a detached HEAD, an image built without `ARCHILYZER_BRANCH`), S1's rule. `builtAfter` is
+`needs()`'s alone (stages.ts `needsDeploy` knows a no-op build's `checkedAt`); the stage's own copy is gone.
+"`--to local` needs ARCHILYZER_SITE_OUT / ARCHILYZER_HOMEPAGE_OUT" is S1's sentence and exit 3. The stage's
+`needs()` runs first and answers most refusals (no build, private, no project, production branch, freshness) with
+S1's `StageFailure`; `runDeployStage` asks them again as the last word before wrangler. A kind/target mismatch from
+the `stage` row is refused by S1's argv parser before either. One S1 test changed: its `--to local` destination is
+seeded with an `index.html`, since the stage refuses to empty a directory that does not look like a bundle it made.
+
+**The hub blocker.** `HUB_FORBIDDEN_TREES` (`lib/builtExport.ts`) listed `reports` and `m`, but the export app renders
+its report and moment routes into EVERY build — `reports/index.html`, `reports/_none/…`, `m/_none/…` — so since
+2026-10-05 every hub bundle was refused ("still carries a site's data (reports, m)"; S1's smoke hit it). `reports` and
+`m` are off the list; `hubReportDataIn` refuses the report DATA a site's reports stage writes there instead, by the
+names `lib/report/views.ts` gives them: `reports/index.json`, `m/index.json`, `reports/<id>/page.json`, its
+`citations.{json,csv}`, its exports (`report.{html,pdf,md}`, `evidence-pack.zip`), its history (`history.json`,
+`history/repo/`), and any `m/**/moment.json`. `media` stays on the list; a tombstone-only `posts/` still passes and a
+real post does not (`builtExport.test.ts`).
+
+Gates after the wiring (at `f1541070`): tsc (all workspaces) clean; **common 3,281/3,281**, 162 s (one run at
+`6bccf923`, before the second merge, was 3,277/3,278 at a load average of 23–27: `fetchPosts.test.ts`'s timing case
+again); **editor unit 142/142**.
+
+Smoke (`$T/s2-smoke.sh`, the `s1-smoke-build.sh` pattern over a scratch corpus in `$T/s2-smokec`, never the real one;
+`WRANGLER_BIN` = the fake, `E2E_LIVE_CHECK=skip`): `publish index` 0; `publish build smoke` 0 (61 s); **`publish hub`
+0 (61 s) — the bundle carries `reports/index.html` and `m/_none/` and passes `builtHubProblem`**, `_hub/built.json`
+written; `stage deploy-site smoke --preview smoke` 0 through the stage row (fake wrangler argv `pages deploy
+<builds>/smoke/out --project-name w3c-never-real --branch smoke`, `[preview]` line, live check `skipped`, `deployed.json`
+`previews.smoke` with ms times) — the preflight passed on this host's `wrangler login` file, the no-credential refusal
+is the unit tests'; the same again: no-op (fresh); `E2E_FAKE_WRANGLER_AUTH_FAIL=1`: exit 1, `[deploy] REFUSED by
+Cloudflare — the API token was not accepted` once, then `[stage] deploy-site smoke: FAILED (exit 1)`, no record;
+`stage deploy-hub _hub --preview smoke` 0 (`previews.smoke` in `_hub/deployed.json`); `stage deploy-homepage smoke`
+refused by S1's argv parser ("the target is _homepage", exit 1); `publish deploy smoke --to local` 0 (195 files into
+the scratch `siteout`, `local` recorded with `liveCheck: null`).
+
+ | Run | At | Specs | Result |
+ |---|---|---|---|
+ | 5 (editor) | `f1541070` | the five + `build`'s neighbours: `deploy-page`, `site-publish-preview`, `sites-homepage`, `build`, `site-scope`, `cut-release` | not run: the export webServer timed out (120 s) — the smoke, run while this one waited 16 min in the queue, had composed the hub into this worktree's `export/public` (no `summaries/`). `export/public` restored (`git clean -X` + the fixture links), then run 6 |
+ | 6 (editor) | `f1541070` | the same six | **35 passed**, 0 failed, 2.3 min (no queue wait) |
### Slice S1, as shipped — the stage contract, the stamps, the lock, per-target bundles and the CLI (2026-10-06)
Branch `r18/stage-core` off `ce66f2d3` (the plan commit on `r18/integration`), worktree `~/Projects/r18-stage-core`