diff --git a/.gitignore b/.gitignore index b22a798..283474d 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ test-results/ .env .env.* !.env.example +.wrangler/ diff --git a/README.md b/README.md index 55eae69..7e4224b 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,14 @@ A local-first scientific instrument for inspecting next-token prediction, changi ## Start here -| I want to… | Read or run | -| ----------------------------------- | ----------------------------------------------------------- | -| try the two-minute teaching loop | [User guide](docs/user-guide.md) | -| understand what the evidence means | [Scientific status](#scientific-status) | -| make a code or documentation change | [Developer guide](docs/developer-guide.md) | -| start a fresh coding-agent session | [Agent operating contract](AGENTS.md) | -| review release readiness | [Generated release evidence](release-evidence/summary.md) | -| deploy or roll back the static app | [Static release runbook](docs/deployment/static-release.md) | +| I want to… | Read or run | +| ----------------------------------- | ------------------------------------------------------------------------------------------ | +| try the two-minute teaching loop | [User guide](docs/user-guide.md) | +| understand what the evidence means | [Scientific status](#scientific-status) | +| make a code or documentation change | [Developer guide](docs/developer-guide.md) | +| start a fresh coding-agent session | [Agent operating contract](AGENTS.md) | +| review release readiness | [Generated release evidence](release-evidence/summary.md) | +| deploy or roll back the static app | [Static release runbook](docs/deployment/static-release.md) (`wrangler.toml` at repo root) | ## Current instrument diff --git a/docs/README.md b/docs/README.md index 543bea5..590190d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -31,6 +31,8 @@ instrument, browser or release criterion. Only the named evidence profile can do - `architecture/` explains package boundaries, runtime flow, replay integrity and static release. - `implementation/00-five-phase-plan.md` records the delivery sequence. - `implementation/05-session-05-handover.md` records the completed Phase 5 source and local gates. +- `implementation/06-cloudflare-workspace-deploy.md` records the Wrangler workspace-root deploy + target; it does not rebind the Phase 5 candidate. - `implementation/06-post-phase-5-review.md` is the post-candidate engineering review: split evidence, remaining launch blockers and owner decisions. - `deployment/static-release.md` is the host-neutral build, smoke and rollback runbook. diff --git a/docs/adr/0010-static-host-wrangler-target.md b/docs/adr/0010-static-host-wrangler-target.md new file mode 100644 index 0000000..7487fed --- /dev/null +++ b/docs/adr/0010-static-host-wrangler-target.md @@ -0,0 +1,35 @@ +# ADR 0010 — Explicit Wrangler target at the workspace root + +- Status: Accepted +- Date: 28 August 2026 + +## Context + +The Observatory is a pnpm workspace. The prepared static host builds from the repository root so +install and `pnpm build` can see every package. Cloudflare Workers Builds then runs +`npx wrangler deploy` from that same root. + +Wrangler 4.x application detection refuses to guess a package at a workspace root when no +configuration file is present. A first Pages deploy therefore failed after a successful Vite +build with: the detection logic was run in the root of a workspace instead of targeting a +specific project. + +Adding Wrangler as a workspace dependency would make a host tool part of the application lockfile. +Pointing the Cloudflare root directory at `apps/observatory` would break the workspace install. + +## Decision + +Commit a repository-root `wrangler.toml` that names the built observatory assets and no Worker +script. Keep Wrangler out of `package.json`. Dashboard builds continue to use `pnpm build` and +`npx wrangler deploy` from `/`. + +The Cloudflare project name must match `thinking-machine-observatory`. Classic Pages output-directory +uploads remain valid and do not require this file. + +## Consequences + +- The default host deploy command can target the static bundle without changing the dashboard + working directory. +- `_headers` remains the security-header contract; Vite still copies it into `apps/observatory/dist`. +- A renamed Cloudflare project must be reconciled with `wrangler.toml` before the next deploy. +- This does not introduce Functions, a Worker script or an application backend. diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 39b9c1d..3c42b13 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -83,7 +83,8 @@ repository and revision. - [Runtime and interaction flows](runtime-flows.md) - [Trace, replay and persistence](trace-integrity.md) - [Release, offline and deployment boundary](release-boundary.md) -- [Architecture decisions](../adr) +- [Architecture decisions](../adr), including [ADR 0010](../adr/0010-static-host-wrangler-target.md) + for the workspace-root Wrangler target - [Implementation handovers](../implementation) - [Model verification boundary](../../model-tools/README.md) - [Agent and contributor contract](../../AGENTS.md) diff --git a/docs/architecture/release-boundary.md b/docs/architecture/release-boundary.md index dd02f6d..635e580 100644 --- a/docs/architecture/release-boundary.md +++ b/docs/architecture/release-boundary.md @@ -55,6 +55,12 @@ or claim the service worker provides offline model availability. Static-host pol source, while the deployed-origin header smoke remains a distinct evidence record that cannot pass until a real URL is observed. +The Cloudflare host builds from the repository root and deploys with `npx wrangler deploy`. +Repository-root `wrangler.toml` is the explicit static-asset target for that command: it names +`apps/observatory/dist` and does not introduce a Worker script. Wrangler stays a host tool, not a +workspace dependency. See [static release](../deployment/static-release.md) and +[ADR 0010](../adr/0010-static-host-wrangler-target.md). + ## Browser and accessibility evidence The ordinary Playwright configuration runs Chromium, Firefox and WebKit. The release suite covers diff --git a/docs/deployment/static-release.md b/docs/deployment/static-release.md index 210d603..8ddae8e 100644 --- a/docs/deployment/static-release.md +++ b/docs/deployment/static-release.md @@ -2,10 +2,11 @@ ## Decision -The first public candidate remains a static HTTPS application with no Functions, backend, account -system or analytics. Cloudflare Pages is the prepared host because it can build the pnpm workspace, -serve immutable hashed assets and apply the checked `apps/observatory/public/_headers` rules to -static responses. This is a deployment target, not a new application dependency. +The first public candidate remains a static HTTPS application with no Functions, Worker script, +backend, account system or analytics. Cloudflare Pages / Workers Builds is the prepared host +because it can build the pnpm workspace, serve immutable hashed assets and apply the checked +`apps/observatory/public/_headers` rules to static responses. This is a deployment target, not a +new application dependency. Wrangler is invoked only by the host; it is not a workspace package. The model and tokenizer remain on their pinned upstream Hugging Face source. The release does not mirror 327.8 MB of weights. The interface discloses the download before the user initiates it, and @@ -20,6 +21,7 @@ the exact identities, hashes, licence and verification profile remain in the rep - build command: `pnpm build` - output directory: `apps/observatory/dist` - application base: `/` on a dedicated origin +- deploy target: repository-root `wrangler.toml`, which names `./apps/observatory/dist` `public/_headers` is copied into the production root. It provides the content security policy, cross-origin isolation boundary, permissions policy, referrer policy, MIME protection, immutable @@ -28,6 +30,32 @@ families required by the optional model path; inline scripts, framing, object em other origins, camera, microphone, location, payment and USB are denied. Inline styles remain allowed because probability bars use bounded React style values. +## Cloudflare dashboard + +Keep the project root at the repository root so `pnpm install --frozen-lockfile` can see the +workspace. Do not set the root directory to `apps/observatory`. + +| Setting | Value | +| ---------------- | ------------------------------ | +| Framework preset | None | +| Build command | `pnpm build` | +| Deploy command | `npx wrangler deploy` | +| Root directory | `/` | +| Project name | `thinking-machine-observatory` | + +The default host command `npx wrangler deploy` is load-bearing. Wrangler 4.x application detection +refuses to guess a package when it is started at a pnpm workspace root. The committed +`wrangler.toml` is the explicit project target: it selects the built observatory assets and +declares that there is no Worker script. `-c wrangler.toml` is optional once that file exists at +the root; keep it if a future host image starts Wrangler from another directory. + +The `name` field must match the Cloudflare project. If the dashboard project was created under a +different name, change the dashboard name to `thinking-machine-observatory` or change the config +to match — do not leave them split. + +Classic Pages Git integration that uploads a build output directory, without a Wrangler deploy +command, still uses `apps/observatory/dist` and `_headers`. That path does not need `wrangler.toml`. + ## Release smoke 1. Confirm the deployed commit and lockfile hash match `release-evidence/manifest.json`. @@ -56,6 +84,8 @@ browser/runtime cache and are outside the application service worker. ## Host references +- [Cloudflare Workers static assets](https://developers.cloudflare.com/workers/static-assets/) +- [Cloudflare Workers headers](https://developers.cloudflare.com/workers/static-assets/headers/) - [Cloudflare Pages custom headers](https://developers.cloudflare.com/pages/configuration/headers/) - [Cloudflare Pages serving and caching behaviour](https://developers.cloudflare.com/pages/configuration/serving-pages/) - [Cloudflare Pages limits](https://developers.cloudflare.com/pages/platform/limits/) diff --git a/docs/implementation/05-session-05-handover.md b/docs/implementation/05-session-05-handover.md index b566478..8ecc575 100644 --- a/docs/implementation/05-session-05-handover.md +++ b/docs/implementation/05-session-05-handover.md @@ -60,6 +60,11 @@ documentation map, fresh-agent start and handover rules, corrected three-engine and present-tense Phase 5/evidence wording. That change did not alter the built application, dependency lock, evidence manifest or accepted release claims. +A later Cloudflare Pages / Workers Builds deploy failed after a successful production build: +Wrangler 4.x application detection refuses to guess a package at the pnpm workspace root. That +host-target repair is recorded in [session 06](06-cloudflare-workspace-deploy.md) and does not +rebind this candidate. + The first published quality job exposed a shallow-checkout integration issue: `phase5:verify` correctly asks Git for the earlier candidate, but the default checkout contained only the PR head. The quality job now fetches full history before verifying candidate ancestry and lockfile contents. diff --git a/docs/implementation/06-cloudflare-workspace-deploy.md b/docs/implementation/06-cloudflare-workspace-deploy.md new file mode 100644 index 0000000..581954d --- /dev/null +++ b/docs/implementation/06-cloudflare-workspace-deploy.md @@ -0,0 +1,45 @@ +# Session 06 handover — Cloudflare workspace deploy target + +## Outcome + +The first Cloudflare Pages / Workers Builds deploy failed after a successful `pnpm build`. Wrangler +4.127.1 ran `npx wrangler deploy` at the pnpm workspace root and refused application detection. + +This change adds an explicit static-asset target so the host's default deploy command can run from +`/`. It does not change the built application, sampler, worker protocol, evidence manifest or +accepted release claims. The Phase 5 source candidate remains +`ae9f7605a9cf613695c4363faa3e5250b67673a8`. This note was rebased onto current `main` +(`a63181c`); the broader follow-up review is +[06-post-phase-5-review](06-post-phase-5-review.md). + +## Scientific boundary + +No model path, header contract or evidence class changed. The host still serves +`apps/observatory/dist` with `public/_headers`. There is still no Worker script, Pages Function or +application backend. + +## What landed + +- repository-root `wrangler.toml` naming `./apps/observatory/dist` and no `main` script; +- ADR 0010 for the workspace-root Wrangler target; +- runbook dashboard settings for Workers Builds and the classic Pages output-directory path; +- static-release policy checks for the Wrangler target and workspace-root deploy command. + +## Observed commands + +Recorded on this branch after the change: + +- `node --experimental-strip-types scripts/verify-static-release.ts` passed; +- `pnpm build` emitted `apps/observatory/dist` including `_headers` and `service-worker.js`; +- `npx wrangler@4.127.1 deploy` at a workspace root without `wrangler.toml` still failed with the + original application-detection error; +- `npx wrangler@4.127.1 deploy --dry-run` from `/` with the committed config read + `apps/observatory/dist` and exited 0. The same command with `-c wrangler.toml` also exited 0. + +A live Cloudflare deploy is not claimed here. That remains a deployed-origin evidence record. + +## Next bounded slice + +Redeploy the existing Cloudflare project after this lands. Confirm the dashboard project name +matches `thinking-machine-observatory`, then attach the live URL, observed headers and commit to +the release manifest. diff --git a/scripts/verify-static-release.ts b/scripts/verify-static-release.ts index cef320c..9a29a27 100644 --- a/scripts/verify-static-release.ts +++ b/scripts/verify-static-release.ts @@ -7,6 +7,7 @@ const licence = readFileSync(resolve(root, 'LICENSE'), 'utf8'); const notices = readFileSync(resolve(root, 'THIRD_PARTY_NOTICES.md'), 'utf8'); const privacy = readFileSync(resolve(root, 'PRIVACY.md'), 'utf8'); const deployment = readFileSync(resolve(root, 'docs/deployment/static-release.md'), 'utf8'); +const wrangler = readFileSync(resolve(root, 'wrangler.toml'), 'utf8'); const requiredHeaderFragments = [ "default-src 'self'", @@ -37,5 +38,24 @@ for (const statement of ['No account', 'No analytics', 'No prompt telemetry']) { if (!privacy.includes(statement)) throw new Error(`Privacy notice omits ${statement}.`); } if (!deployment.includes('Rollback')) throw new Error('Static deployment runbook needs rollback.'); +if (!deployment.includes('npx wrangler deploy')) { + throw new Error('Static deployment runbook needs the workspace-root Wrangler deploy command.'); +} +if (!deployment.includes('thinking-machine-observatory')) { + throw new Error('Static deployment runbook needs the Cloudflare project name.'); +} + +if (!wrangler.includes('name = "thinking-machine-observatory"')) { + throw new Error('Wrangler config must name the Cloudflare project.'); +} +if (!wrangler.includes('directory = "./apps/observatory/dist"')) { + throw new Error('Wrangler config must target the observatory production bundle.'); +} +if (/(^|\\n)\\s*main\\s*=/.test(wrangler)) { + throw new Error('Wrangler config must not introduce a Worker script.'); +} +if (wrangler.includes('pages_build_output_dir')) { + throw new Error('Wrangler config must stay on static assets, not a Pages Functions project.'); +} console.log('Static release policy, rights, third-party notices and privacy wording are complete.'); diff --git a/wrangler.toml b/wrangler.toml new file mode 100644 index 0000000..4548290 --- /dev/null +++ b/wrangler.toml @@ -0,0 +1,10 @@ +# Static-host contract for Cloudflare Workers Builds / Pages. +# Wrangler 4.x refuses to auto-detect a project at a pnpm workspace root. +# This file is the explicit target so `npx wrangler deploy` can run from `/`. +# There is no Worker script, Pages Function or application backend. + +name = "thinking-machine-observatory" +compatibility_date = "2026-08-28" + +[assets] +directory = "./apps/observatory/dist"