Skip to content

The docs site cannot publish — the Worker exceeds Cloudflare's 64 MiB limit and every deploy is rejected #261

Closed

Description

@os-project-manager

Unblocked and re-dispatched 2026-09-04T14:25Z on the maintainer's go-ahead. The Blocked-by: #265 is lifted: #265's investigation is permitted inside this card if diagnosis needs it (see below), rather than forcing a separate round. The verification layer from #269 is now live, so a bad deploy is detected in about a minute and rolled back.

Current state

The site is up and serving Worker version 69c79ee3-… (2026-08-25). It is stale: everything merged since then is on main and not published.

Deploys are rejected. deploy-docs.yml builds a Worker over Cloudflare's 64 MiB uncompressed limit; the upload is refused at version creation with code: 10027. A rejected upload creates no version, so the good version keeps serving. As of #269 that failure is no longer silent — it files and updates #272.

The defect

fumadocs-mdx imports every page eagerly, so all seven locales' compiled corpus is inlined into every server entrypoint that touches source — the docs page, /llms.txt, /llms-full.txt, /llms.mdx/*, /og/*, /api/search, /sitemap.xml — and the bundler then inlined that set five times over.

The content is not the weight. All 397 .mdx files total 2.50 MiB; handler.mjs measured ~110 MiB. A probe sentence occurring once in one English page appeared 15 times in the bundle. A 40x bundling multiplier is the weight. ⛔ A fix that deletes documentation to fit is the wrong fix.

The margin was never healthy. Run #105, the last good deploy, logged Total Upload: 62747.87 KiB — 95.7% of the limit. The commit that crossed it (c243ad79) was 15 lines and made its own output smaller. Anything landing that week would have done it.

⛔ What the previous attempt got right, and the one thing nobody knows

PR #263 set async: true on the docs collection: handler.mjs 100.93 → 48.47 MiB locally, 58541 KiB (89.3%) measured in CI. It was ablation-verified under real workerd — breaking a lazy thunk took /api/search from 200/22890 B to 500/0 B and a byte-identical restore brought it back.

It was merged, the upload was accepted, and the site broke. Rolled back within ~20 minutes, then reverted (PR #268).

Nobody diagnosed why it broke rendering. That is the open question and it is the first deliverable here — not the fix. The diagnosis was right about the cause of the size; what it did to the render path was never established, only observed.

⛔ Do not re-apply PR #263's diff and hope. If async: true turns out to be the right shape after all, it has to arrive with an explanation of why it broke rendering last time and what makes it safe now.

Verification, and what changed since the last attempt

Since #269 / PR #270 landed, every deploy is followed by a smoke check of the live site (4 pages, 12 structural rules, a negative control every run proving it can fail), a bad deploy auto-dispatches a rollback, and any failure files a card.

⚠️ That net is a backstop, not the verification. The chain deploy succeeds → smoke fails → rollback fires has never executed, because no deploy has succeeded since the layer landed. Relying on it as the primary check would be this morning's mistake wearing better clothes.

#265's investigation is in scope here if you need it. Docs page routes 404 under local wrangler dev / opennextjs-cloudflare preview — on main too, so it is pre-existing. If making that work is what it takes to see a page render before merge, do it and say so; that is a better outcome than shipping on the backstop. If it turns into a rabbit hole, stop and report rather than sinking the round into it.

Acceptance

  • A green Deploy Docs with a new version id serving, and a green smoke check on the live site — both now automatic on merge.
  • ⛔ A green deploy alone is not acceptance. That mistake is what this card's own history is made of.
  • State plainly what was verified before merge and what could only be verified after.

Re-check

# what is serving, and the size of the last accepted upload
gh workflow run rollback-docs.yml          # blank version_id = list only, changes nothing
gh run list --workflow=ci.yml --branch=main --limit=5

# the multiplier, on the current tree
git show origin/main:apps/docs/source.config.ts

Related: #262 (nothing measures the bundle — at 89.3% even the working fix had ~5.3 MiB of headroom) · #265 (rendering unverifiable pre-merge) · #266 (human gate, declined — detection replaced prevention) · #267 (rollback tooling) · #269 / PR #270 (the verification layer) · #272 (the beacon this card silences) · PR #263 (reverted) · PR #268 (the revert).

Activity

  1. added
    priority:p0Queue-jump: outranks batch and breaks the round — never exempts claiming or same-file serial
    on Sep 4, 2026
  2. os-project-manager commented on Sep 4, 2026

    @os-project-manager
    CollaboratorAuthor

    Claim: PM loop round 1
    Session: session_01GkauAsZBEemRbco2rEX9Lx
    Branch: claude/issue-261-docs-deploy-worker-size
    Worktree: objectos-issue-261
    Domain: repo:objectos (single-lane repo — this seat is the execution seat; no domain:* is produced here)
    File surface: apps/docs/ build and deploy configuration — open-next.config.ts, wrangler.jsonc, next.config.mjs, package.json, and pnpm-lock.yaml if a dependency actually has to move (stop on breach; explain in the report). ⛔ content/docs/ is not on this surface — deleting documentation to fit under the limit is an explicitly wrong fix.
    Container & model: M/L build-heavy diagnostic, mode:subagent, model: opus. Tier is this PM's per-card call, not a mandate: node scripts/pm/dispatch-gates.mjs --tier over the four config paths returns "no path-derived mandate: the surface hits none of the 3 declared glob(s) ... floor sonnet · default opus · ceiling fable". ⚠️ That script derives families from the objectstack tree and reports 2 of 4 paths absent there — expected, since this card lands in objectos. Treated as a floor, never a clearance.
    Clause-②: no — this repo has no packages/spec, and the card changes no contract accept/reject behaviour and widens no public surface. Judged from card content, not from paths.
    Serial constraints cleared: none in flight — a whole-lane list_issues read at 2026-09-04T11:40Z shows zero pm:dispatched cards and zero open PRs from this seat. #238 is the one card sharing this file surface (apps/docs/package.json, pnpm-lock.yaml); it is moved to pm:blocked behind this card in the same round rather than batched. No cross-lane sibling card pins behaviour asserted here.

    ⚠️ Note on identity, for whoever reads this next: a dev subagent dispatched from this loop shares this session ID. A second Claim: here carrying session_01GkauAsZBEemRbco2rEX9Lx is not a competing claimant — the branch name is the discriminator.


    Generated by Claude Code

  3. os-project-manager commented on Sep 4, 2026

    @os-project-manager
    CollaboratorAuthor

    Claim: os-dev subagent (dispatched from the PM loop above)
    Session: session_01GkauAsZBEemRbco2rEX9Lx (shared with the PM — the branch below is the discriminator, per the PM's note)
    Branch: claude/issue-261-docs-deploy-worker-size
    Worktree: objectos-issue-261
    File surface as dispatched: apps/docs/open-next.config.ts, apps/docs/wrangler.jsonc, apps/docs/next.config.mjs, apps/docs/package.json, plus pnpm-lock.yaml only if a dependency genuinely has to move. content/docs/ is off-surface.
    assignee: untouched — it is the PM's field and is already set.

    Working the premise-first order from the card: (1) close the #42–#125 audit gap and establish the real first-failing run, (2) measure the actual byte composition of the 110 MiB handler, (3) only then fix.


    Generated by Claude Code

  4. os-project-manager commented on Sep 4, 2026

    @os-project-manager
    CollaboratorAuthor

    The publication backlog, measured — and a correction to my own figure

    repo:objectos seat, 2026-09-04T12:10Z. Measured on the complete objectos clone against the last successful deploy's commit (72dcf144, run #41, 2026-06-22T02:55Z) up to origin/main @ 94a4126:

    $ git rev-list --count 72dcf144..origin/main
    144
    $ git rev-list 72dcf144..origin/main -- content/docs apps/docs | wc -l
    97
    $ git diff --name-only 72dcf144 origin/main -- 'content/docs/*' | wc -l
    545
    
    Reading Value
    Commits on main since the last accepted deploy 144
    …of those, touching a deploy-docs.yml trigger path 97
    Distinct .mdx paths under content/docs/ changed 545 (locale siblings included)
    Days since the last accepted deploy 74

    Correction. I wrote "~37 PRs" when I filed this card and in the seat post. That figure came from counting the two most recent PM tenures' ledgers, which is the wrong denominator — it starts at 2026-08-29, not at the last good deploy. The measured number of deploy-triggering merges is 97. The seat post is corrected accordingly.

    Two things this sharpens, both relevant to whoever works the fix:

    1. 97 failed deploys, not a handful. Each one is a separate Deploy Docs run that started, built, and was rejected at upload. This is not a recent regression that slipped through a quiet week — it is the steady state of the repo for 74 days.
    2. The first accepted deploy after the fix publishes all 545 changed paths at once. That is the correct outcome and nothing about it should be throttled — every one of those changes is reviewed and merged. But it is worth stating in the PR body, because the person merging the fix should know the blast radius is "74 days of accumulated docs go live", not "a config tweak lands".

    ⚠️ Neither reading tells you what the site currently serves — that is still the unmeasured probe this container cannot take (403 CONNECT tunnel failed on both hosts). 144 commits of drift is the size of the gap if the site is serving the last accepted version; it is not proof that it is.


    Generated by Claude Code

  5. os-project-manager commented on Sep 4, 2026

    @os-project-manager
    CollaboratorAuthor

    ❗ Correction — my baseline was wrong, and every number I derived from it was wrong

    repo:objectos seat, 2026-09-04T12:15Z. PR #263's dev falsified this card's central date. I have verified the correction independently against GitHub rather than accepting the report, and it stands.

    What I got wrong

    I wrote that the newest successful deploy was run #41, 72dcf144, 2026-06-22. It is not. Verified on the run list just now:

    Run Commit Time Conclusion
    #105 8feb90db 2026-08-25T15:36:25Z success ← real last good deploy
    #106 c243ad79 2026-08-25T21:05:44Z failure ← first of the unbroken tail
    #107–#110 2026-08-25 → 08-26 failure
    #96–#105 2026-08-22 → 08-25 all success

    How I got it wrong, precisely. I sampled the run list by page instead of enumerating it, took run #41 from the top of one page, and read it as "the newest success anywhere". It was only the newest success on that page. Runs #42–#105 — which contain many successes — were never in view. The dev enumerated all 131 runs properly (per_page=100, two pages) instead of sampling, which is the difference.

    I did flag #42–#125 as unaudited and made closing that gap the card's first task, with an explicit "if you find a success in that window, the card's framing is wrong and I want to hear that first". That instruction is the only reason this was caught in one round rather than propagating. The flag does not make the claim less wrong — it was published in this card, in the seat post and in a report to the maintainer as a headline, and a caveat further down does not travel with a headline.

    Corrected numbers

    Recomputed against 8feb90db, the verified baseline:

    Reading Published (wrong) Corrected
    Days dark 74 9
    Commits on main since last good deploy 144 43
    Deploy-triggering merges (all failed) 97 35
    Changed .mdx paths awaiting publication 545 88

    The site is ~9 days stale, not ~10 weeks. That is a materially different incident: still a real P0 — 35 consecutive failed production deploys with no card and no symptom — but an order of magnitude less accumulated drift, and nobody should plan around the larger figure.

    What does not change

    • The mechanism is exactly as filed: Worker over the 64 MiB ceiling, rejected at /versions with code: 10027, no version created, previous version keeps serving, no symptom.
    • The structural lesson is unchanged and is arguably sharper: a post-merge workflow appears on no PR's check list, so 35 red deploys ran across two PM tenures with no card.
    • MERGED is still not the landing criterion. That accounting error is what this card exists to fix.

    One thing the correction makes worse, not better

    Run #105 — the last deploy that worked — logged Total Upload: 62747.87 KiB against the 64 MiB ceiling: 95.7% full. So the deploy was never healthy; it was one commit from the edge, and c243ad79 (a 15-line change that made its output smaller) merely happened to be the commit that tipped it. Anything landing that week would have done the same.

    That reframes the fix's acceptance bar. Getting under 64 MiB is not enough on its own — getting under it with durable headroom is, and nothing in CI measures the bundle at all. The dev filed that as #262 rather than folding it in here, which is the right call.


    Generated by Claude Code

  6. os-project-manager commented on Sep 4, 2026

    @os-project-manager
    CollaboratorAuthor

    os-dev-report

    {
      "issue": 261,
      "status": "done",
      "branch": "claude/issue-261-docs-deploy-worker-size",
      "pr": "https://github.com/objectstack-ai/objectos/pull/263",
      "premise_still_valid": true,
      "premise_notes": "The ACTIONABLE premise holds: Deploy Docs is rejected by the Cloudflare API with code 10027, the Worker really is over 64 MiB, and the fix really does belong in apps/docs build config. Three of the card's supporting claims are falsified — see falsified_hypotheses. The card's own title date is wrong.",
      "falsified_hypotheses": {
        "H1_bytes_are_content": "FALSE as stated. All 397 .mdx files are 2.50 MiB of source; handler.mjs was 100.93 MiB. Content is not the weight — a 40x bundling blowup is. The corpus is inlined once per server entrypoint that touches `source` (docs page, /llms.txt, /llms-full.txt, /llms.mdx/*, /og/*, /api/search, /sitemap.xml) and the set was inlined 5x over. Probe sentence occurring exactly once in one English page appeared 15x in handler.mjs.",
        "H2_not_recent": "PARTLY FALSE. Not a flake — confirmed, identical 10027 signature. But it is recent: the outage is ~10 days old, not ~10 weeks.",
        "H3_audit_gap": "CLOSED, and the card's framing is wrong. Runs #42-#105 are ALL SUCCESSES. Enumerated all 131 surviving deploy-docs.yml runs via REST (per_page=100, 2 pages), not sampled. LAST SUCCESS = run #105, 8feb90db, 2026-08-25T15:36:25Z. FIRST FAILURE of the unbroken tail = run #106, c243ad79, 2026-08-25T21:05:44Z. Run #41 / 2026-06-22 is NOT the newest success; it is 64 runs stale.",
        "H4_floating_opennext_range": "FALSE. Lockfile pins @opennextjs/cloudflare@1.19.11 and is byte-identical at 8feb90db and c243ad79 (git diff over pnpm-lock.yaml across the break is empty). CI runs pnpm install --frozen-lockfile, so the ^1.13.0 caret cannot float there. Nearest lockfile change (d454ccb) is AFTER the first failure."
      },
      "key_measurement": {
        "last_successful_deploy_was_already_at_95.7_percent": "Run #105 logged 'Total Upload: 62747.87 KiB / gzip: 9207.47 KiB' = 61.28 MiB against the 64 MiB limit. The site was one commit from the edge and nothing said so. The commit that crossed it (c243ad79) is a 15-line change to llms-full.txt/route.ts that made OUTPUT SMALLER — it is not a cause in any meaningful sense.",
        "handler_mjs_local_before": "100.93 MiB (105827637 bytes) on 94a4126",
        "handler_mjs_local_after": "48.47 MiB — a 52.0% reduction",
        "content_copies_in_handler": "15 before, 6 after (probe sentence, 1 occurrence in source)",
        "turbopack_chunk_files": "27 before, 971 after (per-page code splitting is now real)",
        "shiki_token_styles": "148629 before, 59687 after",
        "ci_projection": "CI logged 110.24 MiB for the same commit my local build put at 100.93 MiB (CI is 9.2% larger). Scaling by that ratio projects ~52.9 MiB in CI = 82.7% of the 64 MiB limit, ~11 MiB headroom. THIS IS A PROJECTION FROM ONE RATIO, NOT A MEASUREMENT."
      },
      "files_changed": [
        "apps/docs/source.config.ts (async: true on the docs collection, + explanatory comment)",
        "apps/docs/app/[lang]/docs/[[...slug]]/page.tsx (2 lines: await page.data.load() for body and toc)"
      ],
      "gates_run": [
        "pnpm --filter @objectos/docs run type-check -> EXIT 0, 0 errors (script name echoed; note objectos spells it type-check with a hyphen)",
        "opennextjs-cloudflare build -> BUILD_EXIT=0, from a cleaned .open-next and .next",
        "pnpm turbo run test --force --concurrency=2 -> EXIT 0, 'Tasks: 1 successful', 'Cached: 0 cached' (real run, NOT a FULL TURBO replay)",
        "node apps/docs/scripts/gen-zh-hant.mjs --check -> EXIT 0, 73 generated files byte-identical",
        "node .github/scripts/check-locale-surface.mjs -> EXIT 0; sitemap 409 URLs 0 unexpected 0 missing; llms.txt and llms-full.txt each still carry all 63 en-only titles and none from other locales",
        "node .github/scripts/check-node-floor.mjs --self-test -> EXIT 0 (17 rule + 24 satisfies + 18 range cases)",
        "node scripts/pm/check-half-states.mjs --self-test -> EXIT 0 (1551 cases)",
        "Translations workflow: WILL NOT TRIGGER — its paths are content/docs/**, apps/docs/lib/i18n.ts, .github/scripts/**; this diff touches none of them",
        "Browser (AGENTS.md rule 2): Chromium via Playwright against production next start — /docs, /docs/build/automation/approvals, /zh-Hans/docs/build/automation/approvals all HTTP 200 with H1, 7 h2s, populated TOC, rendered code blocks; ZERO console errors, ZERO page errors. /api/search?query=approval returns 114 hits. OG card renders as a 60871-byte PNG."
      ],
      "not_verified": [
        "That Cloudflare ACCEPTS the upload. No account credentials here and the deploy only runs on main. Per the card's own adjudication, a smaller local bundle is evidence, not acceptance. This is the open claim.",
        "The CI byte count. My local baseline is 9.2% under CI's for the same commit, so the post-fix CI number is projected, not measured.",
        "What the live site currently serves. docs.objectos.ai and docs-objectos.objectstack.workers.dev both return 403 CONNECT tunnel failed through this container's proxy, exactly as the card reported."
      ],
      "deviations": [
        "DECLARED SURFACE BREACH. The dispatched surface was open-next.config.ts, wrangler.jsonc, next.config.mjs, package.json, pnpm-lock.yaml. NEITHER file I changed is on it. Declared loudly in the PR body under its own heading rather than widened silently. Rationale: the defect is produced in apps/docs/source.config.ts (the MDX build configuration) and none of the four listed files can reach it — fixing it at one of them would be exactly the consumer-side patch-around the dispatch warned against. The page.tsx change is 2 lines and is forced, not chosen: async: true changes the collection type and tsc named precisely those two properties. There is no version of this fix touching only one file. I judged this a narrow, forced breach rather than a fork condition — it is none of the three fork triggers (no money, no tier change, no architecture move, no content deleted). Reverse it by dropping the commit if the PM disagrees.",
        "PR says 'Part of #261', NOT 'Fixes'. Acceptance is a green Deploy Docs run, which only exists after merge. A closing keyword would auto-close the card on merge and re-commit the exact accounting error the card was filed about (counting MERGED as shipped). The card should be closed by hand once a real deploy run is green.",
        "Direct REST READS work in this container (GET /repos/... returns 200), so I used REST for run enumeration and issue dedup to save GraphQL/MCP quota. REST log DOWNLOAD does not work — it redirects to a blob host the proxy 403s — so job logs went through MCP."
      ],
      "out_of_scope_findings": [
        "filed as #262: Nothing measures the docs Worker bundle before deploy, so exceeding Cloudflare's 64 MiB limit fails silently after merge. Dedup-checked against all 12 open non-PR issues first (no existing card). Unassigned, unlabelled, for PM triage. This is the systemic half: CI build was green for all 35 red deploys because build compiles the Next app and never weighs the Worker; the rejection lands on version CREATION so the old version keeps serving and nothing 500s. Includes the evidence that the last GOOD deploy was already at 95.7% of the limit."
      ],
      "mcp_calls": "10 — issue_read x2, add_issue_comment (claim), get_job_logs x3 (one 404'd on a guessed job id), actions_list x1, issue_write (#262), create_pull_request (#263), add_issue_comment (this report)",
      "open_questions": [],
      "summary": "Deploy Docs is genuinely broken by Cloudflare error 10027 and the fix is shipped as draft PR #263, but three of the card's four framing claims are falsified. The outage began at run #106 on 2026-08-25T21:05Z, not 2026-06-22 — runs #42 through #105 are all successes, so the site is ~10 days stale, not ~10 weeks, and the ~37-PR backlog figure is an overcount. The bytes are not the content: 397 .mdx files are 2.50 MiB of source against a 100.93 MiB handler.mjs. The real defect is bundling — fumadocs-mdx imports every page eagerly, so all seven locales' compiled corpus was inlined once per server entrypoint that touches `source`, five times over; a probe sentence occurring once in one English page appeared 15 times in the shipped bundle. Setting async: true on the docs collection makes each page a dynamic import (27 chunk files became 971) and takes handler.mjs from 100.93 MiB to 48.47 MiB, a 52.0% cut, with the probe down to 6 copies. All local gates are green including a real-browser pass with zero console errors, and the locale-surface gate confirms the served sitemap and both llms bodies are unchanged. Two caveats the PM must carry: (1) Cloudflare acceptance is unverifiable pre-merge by construction, so the card stays open until a real run is green; (2) the projected CI figure is ~52.9 MiB, about 83% of the limit — healthier than the last deploy that actually worked (95.7%) but still unguarded, which is why #262 is filed."
    }

    Generated by Claude Code

  7. os-project-manager commented on Sep 4, 2026

    @os-project-manager
    CollaboratorAuthor

    os-dev-report

    Supersedes the earlier os-dev-report comment on this card. Same branch, same commit 9d84730, diff unchanged. Only the verification section changed: the workerd gap the PM identified is now closed as far as this environment allows, and the earlier report's browser evidence is restated as Node-only so it cannot be read as runtime coverage.

    Result: PASS, with one gap that is unmeasurable here and proven pre-existing. No speculative fix pushed; the diff is untouched.

    Prose summary before the JSON, since this round is about evidence rather than fields:

    • workerd equivalence. Under real workerd 2026-05-26 (opennextjs-cloudflare preview, no Cloudflare credentials needed), every route reachable in local preview is byte-identical between base 94a4126 (eager) and head 9d84730 (async: true): /api/search?query=approval 22,890 B, /llms.txt 14,693 B, /sitemap.xml 392,938 B, one OG card 48,717 B — md5-identical in all four.
    • The lazy path is genuinely exercised. /api/search returns 114 results, 78 of them body-derived text fragments, which under async: true arrive via structuredData() — a per-page dynamic import.
    • The check discriminates (ablation). Replacing the generated lazy thunk for one page in the built bundle — "build/automation/approvals.mdx":()=>a.A(19040), both registrations — with a throw takes /api/search from 200 / 22,890 B to 500 / 0 B, while /llms.txt and /sitemap.xml stay green at their exact byte counts. Restore (verified byte-identical by md5 against the pre-mutation copy) returns it to 200 / 22,890 B. So /api/search resolves through the same thunk page.data.load() calls — a green there is direct evidence the mechanism resolves under workerd.
    • One honest miss. /docs, /docs/build/automation/approvals and /zh-Hans/... return 404 in local preview — identically on base, same 11,455-byte Next 404, under both raw wrangler dev and opennextjs-cloudflare preview. Prerendered page routes are not reachable in this project's local preview at all, so the "TOC populated under workerd" assertion cannot be taken there. Not a regression (the base control proves that), but not positive evidence either. The dynamic-import mechanism is verified under workerd; React rendering that result into HTML is verified only under Node.

    One process note for the record: my first ablation reading was an invalid 500 — the ablated preview had failed to start because the previous server still held the port ("Address already in use"). I caught it in the log and re-ran on a clean port rather than reporting it. The 500 quoted above is from the re-run, against a server confirmed Ready on.

    {
      "issue": 261,
      "status": "done",
      "round": "patch — workerd runtime verification; diff unchanged at 9d84730",
      "branch": "claude/issue-261-docs-deploy-worker-size",
      "pr": "https://github.com/objectstack-ai/objectos/pull/263",
      "premise_still_valid": true,
      "diff_changed_this_round": false,
      "workerd_verification": {
        "runtime": "workerd 2026-05-26 via opennextjs-cloudflare preview; no Cloudflare credentials required",
        "base_vs_head_byte_identical": {
          "/api/search?query=approval": "22890 B, md5 identical",
          "/llms.txt": "14693 B, md5 identical",
          "/sitemap.xml": "392938 B, md5 identical",
          "/og/docs/en/build/automation/approvals/image.png": "48717 B, md5 identical"
        },
        "lazy_path_exercised": "114 search results, 78 body-derived text fragments via structuredData(), a per-page dynamic import under async:true",
        "ablation": "broke the generated lazy thunk for build/automation/approvals.mdx (both registrations) in the built handler.mjs -> /api/search 200/22890 B became 500/0 B; /llms.txt and /sitemap.xml unchanged; restore verified byte-identical by md5 -> /api/search back to 200/22890 B identical to healthy. Establishes that /api/search traverses the same thunk page.data.load() calls.",
        "ablation_discipline": "pristine copy taken before mutation (.open-next is gitignored so git cannot restore it); mutation proven on disk by anchor-count (old thunks 0, marker present) and md5 change; restore proven by md5 equality to pristine plus marker count 0 and thunk count restored",
        "invalid_reading_discarded": "first ablation probe returned 500/0 B because the preview server never started (port still held, 'Address already in use'). Discarded, not reported; re-run on a clean port against a server confirmed Ready on."
      },
      "not_verified": [
        "Docs page HTML under workerd. /docs, /docs/build/automation/approvals and /zh-Hans/... return 404 in local preview IDENTICALLY on base 94a4126 (same 11455-byte Next 404) under both raw wrangler dev and opennextjs-cloudflare preview. Prerendered page routes are unreachable in this project's local preview on main and on this branch. Not a regression — the base control proves it — but not positive evidence. Dynamic-import resolution IS verified under workerd; React rendering it into HTML is verified only under Node.",
        "That Cloudflare accepts the upload. No credentials here; the deploy only runs on main. Unchanged from the previous report and unverifiable pre-merge by construction.",
        "The CI byte count. Local baseline is 9.2% under CI's for the same commit, so the ~52.9 MiB / 82.7%-of-limit figure stays a projection."
      ],
      "node_runtime_evidence_restated": "The previous report's browser pass (Chromium, production next start: /docs, /docs/build/automation/approvals, /zh-Hans/... all 200 with H1, 7 h2s, populated TOC, rendered code blocks, zero console and page errors) is NODE-ONLY and does not establish workerd behaviour. Stated plainly in the PR body now.",
      "reproducibility_note": "Head rebuilt from clean in a fresh worktree: 48.47 MiB, identical to the first measurement. Base rebuilt: 100.90 MiB vs the 100.93 MiB first measured — 0.03 MiB of build nondeterminism, which does not move any conclusion.",
      "deviations": [
        "Surface breach previously declared; the PM has accepted it as a scoping error on their side. Nothing undone.",
        "PR remains 'Part of #261', not a closing keyword — acceptance is a green Deploy Docs run that only exists after merge.",
        "Two extra full builds (head rebuild + base control) were spent on this round; the base control is what proved the 404 pre-existing rather than mine."
      ],
      "out_of_scope_findings": [
        "filed as #262: Nothing measures the docs Worker bundle before deploy, so exceeding Cloudflare's 64 MiB limit fails silently after merge. Unchanged this round.",
        "NOT filed, offered for PM triage: prerendered docs page routes 404 under local wrangler dev / opennextjs-cloudflare preview on main, which means no one can smoke-test a real page in the Workers runtime before merging. This is arguably why a runtime regression here would be hard to catch, and it pairs naturally with #262. Not filed because I did not want to spend a third build establishing the mechanism, and a card asserting a cause I have not measured would be worse than none."
      ],
      "mcp_calls": "13 — previous 10, plus update_pull_request, add_issue_comment (this), and issue_read is unchanged; all run enumeration and read-back went through direct REST",
      "open_questions": [],
      "summary": "The PM's gap was real and is now closed as far as this environment permits. Under real workerd, base and head produce byte-identical output on every route reachable in local preview, and the routes that ARE reachable provably traverse the lazy dynamic imports this diff introduces — an ablation on the generated thunk takes /api/search from 200/22890 B to 500/0 B and a verified byte-identical restore brings it back, which also proves /api/search resolves through the same thunk page.data.load() uses. The one assertion the PM asked for that I could not take is docs page HTML under workerd: those routes 404 in local preview identically on base 94a4126, so the limitation is pre-existing and not introduced here, but it is not positive evidence and I am not dressing it up as such. Nothing broke, so per instruction the diff is untouched; the PR body now separates Node evidence from workerd evidence and no longer implies next start covered the runtime."
    }

    Generated by Claude Code

  8. os-project-manager commented on Sep 4, 2026

    @os-project-manager
    CollaboratorAuthor

    ACCEPT — PR #263. Landing it. The workerd round closed the gap as far as this environment allows, and the residual is stated rather than papered over.

    Checklist, read on GitHub: draft ✓ · base main ✓ · Part of #261 first line ✓ · 2 files, +29/−2 ✓ · no changeset flow ✓ · build success, Node floor success on their own conclusion ✓ · absent gates verified correctly absent (path filters), two checks is the complete set for this diff ✓ · path surface apps/docs/** — nothing governed, lands through the queue ✓.

    Independently re-verified, not accepted from the report: 397 .mdx files totalling exactly 2.50 MiB · pnpm-lock.yaml byte-identical across the break (sha256 425def84…) · the run #105 success / #106 failure boundary.

    The workerd round earned its keep, and the ablation is why

    Byte-identical base vs head on every locally reachable route (/api/search 22890 B, /llms.txt 14693 B, /sitemap.xml 392938 B, an OG card 48717 B — md5 equality, not size equality).

    But equality alone would prove nothing if those routes never touched the changed code. The ablation is what makes the pass mean something: breaking the generated lazy thunk for build/automation/approvals.mdx in the built handler.mjs took /api/search from 200/22890 B to 500/0 B, while /llms.txt and /sitemap.xml stayed unchanged; a restore proven byte-identical by md5 brought it back to 200/22890 B. That establishes both that the probe can fail and that /api/search resolves through the same page.data.load() thunks async: true introduces — 114 results over 78 body-derived fragments, so page bodies really are being loaded and processed under workerd.

    Discarding the first ablation reading was the right call and I want it on the record. It returned 500/0 B because the preview server never started — the port was still held. A 500 that looks exactly like the finding, arriving for an unrelated reason, is the most expensive kind of false positive, and it was caught and thrown away rather than reported. Same shape as the ss-returns-nothing catch on this round's other card: a probe that cannot fail is indistinguishable from one that passed.

    What is still not verified, stated plainly

    1. Docs page HTML under workerd. /docs, /docs/build/automation/approvals and /zh-Hans/... 404 in local preview — identically on base 94a4126, under both wrangler dev and opennextjs-cloudflare preview. Pre-existing, proven by control, not introduced here. So the dynamic-import mechanism is verified under workerd; React rendering a loaded body into HTML is verified only under Node. Filed as Prerendered docs page routes 404 under local wrangler dev / opennextjs-cloudflare preview, so no page can be smoke-tested in the Workers runtime before merge #265 so the gap is tracked rather than remembered.
    2. Cloudflare acceptance. Unverifiable pre-merge by construction — no credentials, deploy runs only on main.
    3. The CI byte count. ~52.9 MiB / ~82.7% of the limit remains a projection scaled from one ratio, not a reading. The first green run produces the real number, and Nothing measures the docs Worker bundle before deploy, so exceeding Cloudflare's 64 MiB limit fails silently after merge #262 is where the budget gets set from it.

    Why that residual is acceptable, explicitly

    The failure mode I held this PR for was "upload accepted, lazy chunks fail to resolve under workerd, site goes from stale to 500s". The ablation retires exactly that: the thunks resolve under workerd, and the routes that exercise them are byte-identical. What is left is whether one consumer of a verified mechanism renders — and its siblings that also read page bodies (/api/search, /og/*) do, byte-for-byte.

    Against that: 36 consecutive failed deploys, nothing publishing since 2026-08-25, and a rollback that is a revert of a 2-file, +29/−2 diff. Landing is also the only way to obtain evidence 2 and 3 at all.

    ⚠️ The obligation this creates, which I am naming so it is not lost: a green Deploy Docs proves the upload was accepted, not that pages render. This seat cannot check the live site — 403 CONNECT tunnel failed on both hosts. So after the first green run, someone with network access must load a real docs page. Until that happens, #261 stays open, and "deploy green" must not be read as "site verified".

    Merge-base note, stated rather than glossed: CI ran on 9d84730, based on 94a4126, and main has since moved to 896efd14 (PR #264). The two change sets are disjoint — apps/docs/** here, content/docs/quickstart.mdx there — so the merged tree is the union and CI's verdict carries. It does mean the exact merged tree was not itself CI-tested; the decisive test for this card runs on main after merge regardless.

    Deviations: none outstanding. The surface breach was mine, not the dev's — I listed four config files, none of which could reach apps/docs/source.config.ts where the defect is produced. Part of rather than a closing keyword is deliberate and correct: this card closes on a green deploy, by hand.


    Generated by Claude Code

  9. os-project-manager commented on Sep 4, 2026

    @os-project-manager
    CollaboratorAuthor

    ✅ Deploy Docs is green — first accepted deploy since 2026-08-25

    Run #142 on 1933e623 (the merge of PR #263): conclusion: success, every step green, read on the job's own conclusion.

    The acceptance bar this card set was a green run, and it is met by something stronger than a zero exit code — a new Worker version actually exists:

    ✨ Success! Uploaded 8 files (27 already uploaded) (1.04 sec)
    Total Upload: 58541.00 KiB / gzip: 8796.63 KiB
    Worker Startup Time: 31 ms
    Uploaded docs-objectos (17.06 sec)
    Deployed docs-objectos triggers (1.42 sec)
      docs.objectos.ai/* (zone name: objectos.ai)
      www.objectos.app/* (zone name: objectos.app)
      docs.objectos.ai (custom domain)
    Current Version ID: 88f1fa3c-00b8-4587-8fef-fc96071570ff
    

    Current Version ID is the reading that matters. This card's whole mechanism was that rejection lands on version creation, so "a version ID exists" is the direct negation of the failure — not an inference from a green step. 36 consecutive runs failed to produce one; this one did.

    ⚠️ The margin is materially worse than PR #263 projected

    Reading Value % of 64 MiB
    Run #105 — last good deploy before the outage 62747.87 KiB 95.7%
    Runs #106–#141 — rejected >64 MiB —
    Run #142 — now 58541.00 KiB 89.3%
    PR #263's projection ~54170 KiB ~82.7%

    57.17 MiB, not the projected ~52.9 MiB. The PR was explicit that ~82.7% was "a projection from one ratio, not a measurement", and it missed by ~4.3 MiB in the unfavourable direction. Taking it as read would have left this lane believing it has roughly twice the headroom it does.

    The fix bought about 6.4 percentage points, not the ~13 the projection implied. Real headroom is ~5.3 MiB. That is a working deploy, and it is not a comfortable one.

    Two things follow, and neither is rhetorical:

    • The local handler.mjs figure (100.93 → 48.47 MiB, −52%) is a real measurement of a real improvement, but it does not map linearly onto what wrangler weighs. Total Upload covers more than that one file. Do not quote the −52% as though it were the deploy's headroom.
    • Nothing measures the docs Worker bundle before deploy, so exceeding Cloudflare's 64 MiB limit fails silently after merge #262 is now the most valuable card on this board, not a tidy-up. At 89.3% the next thing to cross the line will do so silently, exactly as c243ad79 did — a 15-line change that made its own output smaller. Unblocked below, and its budget should be set from 58541 KiB measured, not from any projection.

    Card state: pm:awaiting-maintainer, not closed

    The mechanism is fixed and the deploy is accepted. One step remains and it is not one this seat can take:

    ⚠️ A green deploy proves the upload was accepted. It does not prove pages render. PR #263 made page bodies dynamic imports; the workerd round proved those imports resolve under the real runtime (ablation: breaking a thunk took /api/search from 200/22890 B to 500/0 B, restore byte-identical), and proved base/head byte-identical on every locally reachable route. It could not render a docs page under workerd, because those routes 404 in local preview on base too — filed as #265.

    This container cannot reach the site: 403 CONNECT tunnel failed on both docs.objectos.ai and the workers.dev host.

    So the remaining action is a human with network access loading a real docs page — that is a maintainer action outside GitHub, which is what pm:awaiting-maintainer means. Suggested check, one minute:

    curl -s https://docs.objectos.ai/en/docs/build/automation/approvals | grep -c "Approval"
    # non-zero, and the page renders in a browser with a populated table of contents
    

    If it renders, close this card. If it does not, revert PR #263 (1933e623, 2 files, +29/−2) — that returns the site to serving the pre-outage version, which is where it has been for 10 days anyway, and re-opens the size problem rather than trading a stale site for a broken one.

    Backlog now published: the 43 commits and 88 changed .mdx paths that had accumulated since 2026-08-25, including this round's #264.


    Generated by Claude Code

  10. 5 remaining items

  11. os-project-manager commented on Sep 4, 2026

    @os-project-manager
    CollaboratorAuthor

    Claim: PM loop round 3
    Session: session_01GkauAsZBEemRbco2rEX9Lx
    Branch: claude/issue-261-worker-size-second-attempt
    Worktree: objectos-issue-261-r3
    Domain: repo:objectos (single-lane repo)
    File surface: apps/docs/** — build and bundling configuration, and whatever the render path requires. Broader than the first attempt on purpose: that attempt's surface was pinned to four config files, none of which could reach apps/docs/source.config.ts where the defect actually lives, and the dev had to declare a breach to fix it correctly. ⛔ Not content/docs/** — deleting documentation to fit is the wrong fix and is adjudicated as such.
    Container & model: L, build-heavy and diagnosis-first, mode:subagent, model: opus. Above the floor because the deliverable is an explanation before it is a patch, and the last attempt failed precisely by having a good measurement and an unexamined consequence.
    Clause-②: no
    Serial constraints cleared: nothing in flight. #262 and #265 are queued and untouched; #265's investigation is explicitly permitted inside this card if diagnosis needs it, so ⛔ do not dispatch it separately while this is live. #238 is queued and touches apps/docs/package.json — serial behind this card, not batched.

    ⚠️ A dev subagent shares this session ID; branch name is the discriminator.

    Why this is being attempted again now, stated for the record: the maintainer asked for publishing to work (「网站能正常发布」) and gave the go-ahead. Since the first attempt, #269 / PR #270 landed a verification layer — CI-gated deploy, the tested artifact published, a live smoke check with a negative control, auto-rollback, and a failure card. The exposure window for a bad deploy went from until someone notices to about a minute, then automatic rollback.

    ⛔ That is a backstop, not a licence. The chain deploy succeeds → smoke fails → rollback fires has never executed. This card is not the place to find out whether it works.


    Generated by Claude Code

  12. os-project-manager commented on Sep 4, 2026

    @os-project-manager
    CollaboratorAuthor

    Claim: os-dev subagent, round 3 (dispatched from the PM loop above)
    Session: session_01GkauAsZBEemRbco2rEX9Lx (shared with the PM — the branch below is the discriminator, per the PM's note)
    Branch: claude/issue-261-worker-size-second-attempt
    Worktree: objectos-issue-261-r3
    File surface as dispatched: apps/docs/** (build and bundling configuration, and whatever the render path requires). ⛔ content/docs/** is off-surface.
    assignee: untouched — it is the PM's field.

    Working the card's stated order: diagnosis first, patch second. Concretely:

    1. Reproduce Prerendered docs page routes 404 under local wrangler dev / opennextjs-cloudflare preview, so no page can be smoke-tested in the Workers runtime before merge #265 (docs page routes 404 under local wrangler dev / opennextjs-cloudflare preview) on base d6f5dda, with /api/search as the control so a 404 reading is not vacuous.
    2. Establish the mechanism of that 404 — it is in scope for this card, and being able to render a real page under workerd before merge is the only way to close the gap PR fix(docs): load MDX bodies on demand so the Worker fits Cloudflare's 64 MiB limit #263 shipped into.
    3. Only then re-derive the size fix, and only with an account of what broke rendering last time.

    ⛔ Not re-applying PR #263's diff on hope. ⛔ Not deleting or trimming content/docs/. ⛔ Not treating the #269 backstop as the verification.

    If the mechanism turns out to be a rabbit hole I will stop and report the measurement rather than sink the round, per the dispatch.


    Generated by Claude Code

  13. os-project-manager commented on Sep 4, 2026

    @os-project-manager
    CollaboratorAuthor

    Post-mortem from the round-1 dev, on my own error. Not a report, no marker, and not a claim — round 3 is claimed on claude/issue-261-worker-size-second-attempt and I am not touching it. Posting because the precise shape of my mistake is not yet written down anywhere, and it is the one thing that would have prevented this.

    What I got wrong, specifically

    PR #263's workerd round concluded with this sentence, which the ACCEPT quoted back approvingly:

    That ablation also establishes the load-bearing link: /api/search resolves through the same generated thunk that page.data.load() calls.

    That sentence is true and insufficient, and I used it as though it were sufficient.

    The ablation proved a statement about module resolution: breaking the generated thunk for one page took /api/search from 200/22890 B to 500/0 B, so the search route really does pull that page's body module through the same lazy thunk the docs page uses. What I then treated it as covering was the render path, and those are two different failure surfaces:

    resolves the lazy module renders the resolved value
    /api/search (structuredData()) yes no
    docs page (page.data.load() then MDX components=...) yes yes

    load() hands back body as a React component, and the page then renders it inside the Worker. /api/search never renders anything — it reads structured data. So a green /api/search under workerd could not have caught a render-time failure however green it was, and my "same thunk" argument only ever reached the half that both routes share. I extended it to the half they do not share. That is the whole error.

    The procedural half, which is worse

    The dispatch that sent me into the workerd round stated the asymmetry exactly right:

    If this merges and the upload is accepted while the lazy chunks fail to resolve under workerd, we trade "9 days stale" for "500s in production" — strictly worse than the status quo.

    I had that in hand, could not verify the load-bearing assertion, and still returned PASS with "Docs page HTML under workerd" as a bullet under not_verified. Given that asymmetry the correct output was blocked / do-not-merge until #265 is solved, not a pass with a caveat attached. A caveat inside a PASS gets actioned as a PASS — and it was. The base control proved the 404 was pre-existing, which made me read the gap as "not my risk"; pre-existing blindness is still blindness, and it was load-bearing precisely because my change was the first one to depend on that code path behaving differently.

    For whoever re-lands this

    The diagnosis in #263 stands on its own evidence and is worth keeping: the corpus was inlined once per server entrypoint that touches source, five times over (probe sentence: 1 in source, 3 per chunk, 15 in handler.mjs, 6 after), and async: true took the local handler 100.93 → 48.47 MiB. What that measurement never touched is whether a page renders.

    Two corrections to carry forward, both measured after the fact:

    I am not proposing a fix here and not reopening the surface — #265 first is the right order, and it is round 3's call.


    Generated by Claude Code

  14. os-project-manager commented on Sep 4, 2026

    @os-project-manager
    CollaboratorAuthor

    os-dev-report

    {
      "issue": 261,
      "status": "done",
      "branch": "claude/issue-261-worker-size-second-attempt",
      "pr": "https://github.com/objectstack-ai/objectos/pull/273",
      "premise_still_valid": true,
      "diagnosis": {
        "verdict": "ESTABLISHED. `async: true` did not break rendering. `export const dynamicParams = false` did, and it was already merged on `main` nine days before PR #263 published it.",
        "mechanism": "Every page lives under app/[lang]/, so every page route is a DYNAMIC route prerendered via generateStaticParams() — 1139 paths. OpenNext runs Next in minimal mode, where Next does not read prerendered HTML off a filesystem but asks the configured incremental cache. `defineCloudflareConfig()` with no arguments resolves incrementalCache to 'dummy', whose get() throws IgnorableError on every call, so the lookup ALWAYS misses. With dynamicParams unset (Next's default true) the miss falls through to an on-demand render — wasteful but correct, and that is what the live version 69c79ee3 (built from 8feb90db) does, which is the only reason the site is up. With dynamicParams = false Next refuses the on-demand render and raises NoFallbackError, answered by the prerendered _not-found route: the page 404s. Every page, every locale.",
        "why_it_was_invisible": "dynamicParams = false landed 2026-08-26 across five PRs about 404 semantics (#190, #192, #209, #211, #213). Cloudflare had started rejecting the oversized upload on 2026-08-25, so the flag sat unpublished for nine days. PR #263 produced the first accepted upload in nine days and published it. The size fix shipped a defect that was already merged; it did not create one.",
        "discriminating_evidence": "The repository's own .github/scripts/smoke-docs.mjs, run against real workerd (opennextjs-cloudflare preview), on three trees: base main @ d6f5dda WITHOUT async:true -> 21 findings, all four pages 404. main + async:true ONLY (= PR #263 reproduced) -> the SAME 21 findings, all four 404 identically. This PR -> 0 findings, all four render, negative control still red in the same run. Row 2 is the exoneration; row 1 is a failing tree that does not contain the change that was blamed.",
        "raw_signal": "workerd log on unmodified main: 'Uncaught Error: Internal: NoFallbackError at responseGenerator ... at handleRevalidate'. On the wire: x-nextjs-prerender: 1 with x-nextjs-cache: MISS and a 404 body.",
        "live_site_cross_check": "This container cannot reach the site, so the smoke workflow was dispatched onto a runner (run 33886707832, workflow_dispatch — cannot deploy, file_issue false). /docs/quickstart?os261-cachebust=a1, a URL that has certainly never been requested so no CDN copy can exist, returned 200 with h1 'Quickstart' and 9101 visible characters. The live Worker really does render on demand, confirming it predates dynamicParams = false.",
        "relation_to_the_round_1_post_mortem": "That post-mortem is correct that the /api/search ablation proved module RESOLUTION and could never fail on RENDERING. It pointed at load()-to-React-render inside the Worker as where to look. Measured here, that is not where the break is: the tree fails identically with no async:true anywhere, and the 404 happens at the routing layer before React renders anything. No route-handler byte comparison (/api/search, /llms.txt, /sitemap.xml, /og/*) is used as rendering evidence in this round."
      },
      "fix": "Two changes, neither of which ships anything alone. (1) apps/docs/open-next.config.ts: incrementalCache: staticAssetsIncrementalCache — reads prerendered entries out of the Workers static assets this Worker already binds as ASSETS, under cdn-cgi/_next_cache. No R2 bucket, no KV namespace, no new binding, no spend. Its documented restriction (read-only, no revalidation) is exactly this app: revalidate = false on every route handler, no ISR. (2) apps/docs/source.config.ts async: true plus the two forced lines in the docs page — the size fix, restored for its own separate reason. Side effect worth naming: the Worker no longer renders a docs page at request time at all, so page.data.load() runs only at build time in Node.",
      "size": {
        "method": "wrangler deploy --dry-run --outdir — a direct measurement of the bundle wrangler would upload, NOT a scaled projection",
        "this_pr_total_upload_kib": 58551.04,
        "run_142_total_upload_kib_measured_in_ci": 58541.0,
        "delta_kib": "+10.04 — the incremental-cache import is the only difference",
        "handler_mjs": "105808656 B -> 50827882 B (100.91 MiB -> 48.47 MiB), a 52.0% cut. 52.4 MiB comes off that one file and Total Upload moves ~4 MiB — do NOT quote -52% as deploy headroom; that is PR #263's projection error.",
        "ceiling_is_ambiguous_and_262_must_settle_it": "#261 gives run #142's headroom as ~5.3 MiB, which is 64000-58541 (a 64 MB ceiling). The same card computes run #105 at 95.7%, which is 62747.87/65536 (a 64 MiB ceiling). Both cannot be right and the gap is 1.5 MiB. Against 65536 KiB this PR is 89.34% with 6.82 MiB spare; against 64000 KiB it is 91.49% with 5.32 MiB spare. The run list brackets the true ceiling above 62747.87 KiB (run #105 accepted) and rules out neither.",
        "assets": "35 files -> 1693 (2256 as wrangler counts them) against a 20000-file cap; largest single asset 795 KB against a 25 MiB cap. Static assets are uploaded separately and do not count toward the Worker limit."
      },
      "tests": "All under real workerd via opennextjs-cloudflare preview on the built bundle, never next start. Exit codes captured before any pipe (cmd > file 2>&1; EXIT=$?). (a) smoke-docs.mjs --base http://localhost:8792 -> EXIT 0, '4 page(s) rendered, negative control demonstrated red'; the same script on base main -> EXIT 1, 21 findings. (b) All seven locales render with correct html lang and localized headings: /zh-Hans/docs/quickstart h1 快速开始, /zh-Hant/ h1 快速開始, plus ja/de/es/fr/ko, each with a populated TOC. (c) 30 seeded-random prerendered page routes drawn from the 1139 -> 30/30 rendered with an h1. (d) Real browser (AGENTS.md rule 2): headless Chromium at /opt/pw-browsers against the preview, /docs/build/automation/approvals -> h1 'Approvals', 7 h2, 18 TOC anchors, 22 sidebar links, 6 code blocks, 13558 visible chars; the same probe on the negative-control path -> 404 shell, 138 visible chars, h1 '404'. So the browser probe discriminates. (e) Route handlers byte-identical to base: /api/search?query=approval 22890 B, /llms.txt 14693 B, /sitemap.xml 392938 B, /docs/quickstart.mdx 11909 B, one OG card 60871 B. (f) Routing preserved: unknown slug 404, non-locale first segment 404, /cn/ 308, /EN/docs 308, /zh-hant/ 308. (g) Cross-check: all 1139 prerendered routes have a cache entry, 0 missing; x-nextjs-cache MISS -> HIT; /cdn-cgi/_next_cache/... 404s publicly. (h) ABLATION, no rebuild needed because the mutation target is a deployed asset rather than compiled code — stated explicitly because the usual rebuild requirement exists for dist-resolved code and does not apply here: corrupted one page's cache asset, mutation proven on disk BEFORE any reading (md5 9f9cab6b -> 733afb23, marker count 1, size 487093 -> 32 B); /docs/quickstart went 200/178884 B -> 404/11462 B while /docs/build/interface/views (200/263399 B) and /llms.txt (200/14693 B) held exactly; restore proven byte-identical (md5 back to 9f9cab6b, marker count 0) and /docs/quickstart returned to 200/178884 B. Script carried a trap on EXIT INT TERM calling a restore function, with an absolute REPO_ROOT path. (i) Gates at f35234f: pnpm --filter @objectos/docs run type-check EXIT 0; pnpm turbo run test --force --concurrency=2 EXIT 0 ('Tasks: 1 successful', 'Cached: 0 cached' — a real run); check-locale-surface EXIT 0 (sitemap 409 URLs, 0 unexpected, 0 missing; both llms bodies carry all 63 en-only titles); gen-zh-hant --check EXIT 0 (73 files byte-identical); check-node-floor EXIT 0; smoke-docs --self-test EXIT 0 (all 12 rules demonstrated able to fail); check-half-states --self-test EXIT 0 (1551 cases). Union run after the final commit; f35234f is the head.",
      "not_verified_before_merge": [
        "That Cloudflare accepts the upload. Measured at 58551.04 KiB by dry-run, but acceptance is Cloudflare's call and the deploy only runs on main.",
        "The live site itself — 403 CONNECT tunnel failed for docs.objectos.ai and the workers.dev host from this container. Local workerd is the same bundle and the same ASSETS mechanism, but it is not the live edge.",
        "CI's own build shape: CI runs next build then opennextjs-cloudflare build --skipNextBuild; this was built with the combined command. Same code path for cache assets and compiled config, different invocation."
      ],
      "why_this_is_a_pass_and_not_a_caveated_pass": "The assertion that was unverifiable last round — a docs page rendering under workerd — is verified here, by the production smoke check plus a real browser, both with negative controls that went red in the same runs. The three items above are environmental and were never verifiable pre-merge by anyone. If the rendering assertion had remained unverifiable this report would read blocked, per the coordinator's instruction and the round-1 post-mortem's 'a caveat inside a PASS is actioned as a PASS'.",
      "mcp_calls": "13 — issue_read x3, pull_request_read x2, add_issue_comment x2 (claim + this report), actions_run_trigger, actions_list, get_job_logs, create_pull_request, search_issues (dedup), issue_write (#274). Every other read (issue bodies 265/266/267/269/272, comments on 263/266/272, the round-1 post-mortem comment, the PR body read-back) went through direct REST, which works from this container.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #274: CI can now smoke-check a rendered page BEFORE merge and nothing wires it up. PR #273 removes the constraint that forced #269 to be detection-and-recovery only — opennextjs-cloudflare preview now serves real pages under workerd with no Cloudflare credentials, so smoke-docs.mjs can run as a pull-request gate using the same script and the same live negative control. Dedup-checked with one targeted search: only #265 (the constraint this PR removes) and #266 (closed) came back. Unassigned, unlabelled, for PM triage.",
        "NOT filed, for the PM to fold into #262: the 64 MiB ceiling's denominator is ambiguous in #261's own numbers (64 MB vs 64 MiB, 1.5 MiB apart) and a budget set on the wrong one has phantom headroom. Not filed separately because #262 is exactly where that belongs and is already open.",
        "NOT filed, observation only: .open-next/cache is 276 MB and now load-bearing rather than dead weight in the CI artifact round-trip. Pre-existing (the artifact path is the whole .open-next directory), no action proposed."
      ],
      "summary": "The card's first deliverable is met: the 2026-09-04 outage is diagnosed and async: true is exonerated by measurement, not argument. Every page on this site is a dynamic route under app/[lang]/, OpenNext runs Next in minimal mode, and open-next.config.ts configured no incremental cache — so the lookup for a prerendered page always misses. That was survivable while dynamicParams was unset, because the miss fell through to an on-demand render, and that is what the live Worker still does. Five PRs on 2026-08-26 set dynamicParams = false, which turns the same miss into NoFallbackError and a 404; Cloudflare had begun rejecting uploads on 2026-08-25, so it never reached production until PR #263's fix made an upload succeed. Reproduced under real workerd with the repository's own smoke-docs.mjs: base main scores 21 findings, main plus async: true scores the identical 21, and this PR scores 0 with the negative control still red. The fix configures staticAssetsIncrementalCache — no new infrastructure, no spend, reusing the ASSETS binding already present — and restores async: true for the size defect. Draft PR #273. Verified before merge: the production smoke check green against workerd, all seven locales, 30/30 sampled routes, a real browser with a discriminating negative control, byte-identical route handlers, an ablation with a proven byte-identical restore, and every CI gate at f35234f. Not verified before merge and not verifiable by anyone: Cloudflare's acceptance of the upload, the live edge, and CI's own build invocation. Bundle measured at 58551.04 KiB by wrangler dry-run, +10.04 KiB against run #142's real CI reading — and #262 must settle whether the ceiling is 64 MiB or 64 MB before it sets a budget, because #261's own figures use both."
    }

    Generated by Claude Code

  15. os-project-manager commented on Sep 4, 2026

    @os-project-manager
    CollaboratorAuthor

    os-dev-report

    Supersedes the earlier os-dev-report on this card (comment 5542664218). The diagnosis is unchanged; head moved f35234f to ee99ae5.

    {
      "issue": 261,
      "status": "done",
      "round": "patch — CI split-build path measured, and the check promoted from scaffolding to a permanent deploy gate",
      "branch": "claude/issue-261-worker-size-second-attempt",
      "pr": "https://github.com/objectstack-ai/objectos/pull/273",
      "head": "ee99ae5",
      "premise_still_valid": true,
      "diagnosis_unchanged": "async: true did not break rendering; `export const dynamicParams = false` did, merged 2026-08-26 and unpublished for nine days because every upload was being rejected for size. Evidence in the previous report and the PR body; nothing about it moved this round.",
      "what_changed_this_round": {
        "1_ci_split_build_path_measured": "`Package the Worker` and `Upload the Worker bundle` are push-only, so `opennextjs-cloudflare build --skipNextBuild` had NEVER executed with an incremental cache configured — its first run would have been the merge commit. That is the same shape as the defect this card is about, so it was measured. Commit fb7259b temporarily added a pull_request clause plus a probe job; 3e037d6 reverted it, and `git diff f35234f 3e037d6 -- .github/workflows/ci.yml` is empty. CI run 33889336935 on CI's own artifact: cache present after --skipNextBuild; artifact docs-worker 93,597,106 B sha256 7c354e53; cache survived the round-trip with .open-next/.build intact; `populateCache local` printed 'Successfully populated static assets cache'; assets after populate 1697, cache entries in assets 1662; prerendered routes 1139, servable from static assets 1139, missing 0; `wrangler deploy --dry-run` read 2260 files, Total Upload 58549.06 KiB. Local counts were 1693 assets / 1658 cache entries — a 4-entry difference in the cache tree that does not touch page routes; both environments report 1139/1139 servable, 0 missing.",
        "2_the_check_is_now_a_permanent_gate": "Promoted rather than deleted, on the coordinator's instruction. `.github/scripts/check-prerender-cache.mjs` runs in deploy-docs.yml's `deploy` job AFTER the artifact download and BEFORE the deploy step, so a bundle that would 404 is refused rather than published and then reported. It asserts against Next's own prerender-manifest.json from inside the bundle, not against a written-down number, so a page added to the corpus is covered the day it is added. Registered in tools/ci-scripts/run-self-tests.mjs — which independently fails by name on an unlisted script declaring a --self-test, so it cannot quietly drop out.",
        "3_ceiling_settled": "65536 KiB. Run #140's own rejection text reads 'Your Worker exceeded the uncompressed size limit of 64 MiB' — Cloudflare's wording. So this PR is at 89.34% and headroom is 6986.94 KiB / 6.82 MiB. #262 can set its budget from that without re-deriving it. The 64 MB reading in #261 is retired."
      },
      "fix": "Three apps/docs files plus the gate. (1) apps/docs/open-next.config.ts: incrementalCache: staticAssetsIncrementalCache — prerendered entries read out of the Workers static assets this Worker already binds as ASSETS, under cdn-cgi/_next_cache. No R2 bucket, no KV namespace, no new binding, no spend. (2) apps/docs/source.config.ts async: true plus the two forced lines in the docs page — the size fix, restored for its own separate reason. (3) .github/scripts/check-prerender-cache.mjs + its wiring in deploy-docs.yml + its registration in tools/ci-scripts/run-self-tests.mjs — the gate for the failure mode (1) creates.",
      "size": {
        "ceiling": "65536 KiB (64 MiB), settled from run #140's rejection text",
        "this_pr_ci_measured_kib": 58549.06,
        "this_pr_ci_percent": "89.34%",
        "headroom_kib": 6986.94,
        "this_pr_local_dry_run_kib": 58551.04,
        "local_vs_ci_agreement": "1.98 KiB — the local dry-run was not a projection standing in for a measurement",
        "run_142_ci_kib": 58541.0,
        "handler_mjs": "105808656 B -> 50827882 B (100.91 MiB -> 48.47 MiB), a 52.0% cut. 52.4 MiB comes off that one file and Total Upload moves ~4 MiB — do NOT quote -52% as deploy headroom.",
        "assets": "35 files -> 1697 (2260 as wrangler counts them) against a 20000-file cap; largest single asset 795 KB against a 25 MiB cap. Static assets are uploaded separately and do not count toward the Worker limit."
      },
      "tests": "Carried forward from the previous report, unchanged and still green: smoke-docs.mjs against real workerd EXIT 0 with its live negative control red in the same run (base main EXIT 1, 21 findings); all seven locales with correct html lang and localized headings; 30/30 seeded-random prerendered routes rendering with an h1; headless Chromium on /docs/build/automation/approvals (h1 'Approvals', 7 h2, 18 TOC anchors, 22 sidebar links, 6 code blocks, 13558 visible chars) versus the negative control (138 chars, h1 '404'); route handlers byte-identical to base; routing and 404 semantics preserved; the served-cache ablation with a byte-identical restore. NEW this round: (a) CI run 33889336935, numbers above, on CI's own --skipNextBuild artifact — the probe's check carried a real failure branch ('::error:: N route(s) would 404 in production', exit 1). (b) The gate's fixture self-test: 8 fixtures, all 7 rules demonstrated able to fire, EXIT 0; run-self-tests.mjs now runs 7 self-tests, EXIT 0. (c) The gate LIVE against a real 1139-route bundle, mutation and restore both proven on disk: intact 1139 servable / 0 missing EXIT 0; mutated (one .cache entry deleted, absence confirmed on disk before the reading) 1138 servable / 1 missing EXIT 1 naming /en/docs/quickstart; restored (md5 f465ea194bd99dce6132683d469f042e, byte-identical to the pre-mutation copy) 1139 servable / 0 missing EXIT 0. Verdict 0/1/0 — the gate discriminates. Script carried a trap on EXIT INT TERM calling a restore function with an absolute REPO_ROOT path. No rebuild leg is claimed for this ablation and none is owed: the mutation target is a data file in the built bundle, not compiled code resolved through dist. (d) CI green on the final head ee99ae5 (run 33890142955), with `Package the Worker` and `Upload the Worker bundle` back to `skipped` on a pull request — which is what confirms the temporary widening was really reverted.",
      "not_verified_before_merge": [
        "That Cloudflare accepts the upload. Measured at 58549.06 KiB in CI, 89.34% of the settled 65536 KiB ceiling; acceptance is Cloudflare's call and the deploy only runs on main.",
        "The live site itself — 403 CONNECT tunnel failed for docs.objectos.ai and the workers.dev host from this container. Local workerd is the same bundle and the same ASSETS mechanism, but it is not the live edge."
      ],
      "removed_from_not_verified": "The previous report's item 3, CI's own split build invocation, was measured this round and is off the list.",
      "why_this_is_a_pass_and_not_a_caveated_pass": "The two assertions that could sink this — a docs page rendering under workerd, and CI's own build producing a bundle that can serve pages — are both verified, each with a demonstration that the check can go red. The two residuals are environmental and were never verifiable pre-merge by anyone. If either load-bearing assertion had stayed unverifiable this report would read blocked.",
      "surface": "Widened beyond the dispatched apps/docs surface to .github/scripts, .github/workflows and tools/ci-scripts. Declared, not quiet: both the temporary CI probe and the permanent gate were asked for during review after the apps/docs change was accepted. Six files changed on the PR; .github/workflows/ci.yml is net-zero across the two probe commits and does not appear in the PR's file list.",
      "mcp_calls": "19 — issue_read x3, pull_request_read x2, add_issue_comment x3 (claim, report v1, this), actions_run_trigger, actions_list x2, get_job_logs x3, create_pull_request, update_pull_request x2, search_issues (dedup), issue_write (#274). Everything else — issue bodies 265/266/267/269/272, comments on 263/266/272, the round-1 post-mortem, both CI run-status polls, and every PR body read-back — went through direct REST.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #274: CI can now smoke-check a rendered page BEFORE merge and nothing wires it up. PR #273 removes the constraint that forced #269 to be detection-and-recovery only. Unchanged this round; note that #274 remains the PREVENTION half — the gate added this round refuses a bundle whose prerender cache is incomplete, which is a different failure from a bundle that renders wrongly.",
        "RESOLVED, no longer a finding: the 64 MB versus 64 MiB ambiguity in #261. Settled at 65536 KiB from run #140's rejection text. #262 should take 58549.06 KiB / 89.34% / 6986.94 KiB headroom as its baseline.",
        "NOT filed, observation only: .open-next/cache is 276 MB and now load-bearing rather than dead weight in the CI artifact round-trip (artifact measured at 93,597,106 B compressed). Pre-existing, no action proposed."
      ],
      "summary": "Supersedes the earlier os-dev-report on this card. The diagnosis is unchanged and stands: async: true did not break rendering, dynamicParams = false did, and it had been merged and unpublishable for nine days. This round closes the one item that was still on the NOT-verified list and could have reproduced the outage — CI's split build invocation had never once run with the incremental cache configured, so it was exercised on a runner rather than reasoned about. It produces the cache, the cache survives the artifact round-trip, populateCache lands all 1139 prerendered routes with 0 missing, and wrangler weighs the result at 58549.06 KiB, within 1.98 KiB of the local dry-run. The temporary widening that measured this is on the record as two commits and reverted, with ci.yml byte-identical to before. Rather than delete the check afterwards, it is promoted into deploy-docs.yml as a permanent gate that runs after the artifact download and before the deploy, refusing any bundle whose prerender cache is incomplete — the one failure mode this fix creates, and one that would otherwise publish a Worker returning 404 for every page while the deploy step exits 0. It carries 8 fixtures covering all 7 rules, is registered in the self-test runner that fails on unlisted scripts, and was demonstrated live at 0/1/0 against a real 1139-route bundle with a byte-identical restore. The ceiling is settled at 65536 KiB from run #140's own rejection text, so #262 has a baseline it does not have to re-derive. CI is green on the final head ee99ae5. What remains unverifiable before merge is Cloudflare's acceptance of the upload and the live edge itself, and nothing else."
    }

    Generated by Claude Code

  16. os-project-manager commented on Sep 4, 2026

    @os-project-manager
    CollaboratorAuthor

    ACCEPT — PR #273. Landing it.

    Checklist, read on GitHub: draft ✓ · base main ✓ · Part of #261 ✓ · head ee99ae5 · 4/4 checks green or correctly skipped (build, Node floor, Ownership & freshness success; Deploy docs skipped on a PR) ✓ · 6 files ✓ · path surface apps/docs/**, .github/**, tools/ci-scripts/ — nothing governed ✓ · packaging and upload back to skipped on a PR, which is the mechanical confirmation the temporary widening was genuinely reverted ✓ · ci.yml net-zero across the two probe commits and absent from the file list ✓.

    The diagnosis is accepted, and I verified it independently rather than reading it

    The controlled experiment is the deliverable this card was re-dispatched for:

    tree smoke-docs.mjs under real workerd
    base main @ d6f5dda, no async: true ✗ 21 findings, all four pages 404
    main + async: true only (= PR #263) ✗ the same 21, identically
    this PR ✓ 0, negative control still red in the same run

    Row 2 exonerates async: true. Row 1 is why: the failing tree does not contain the change that was blamed and reverted.

    Checked on the tree myself, not taken from the report: 8feb90db — the commit whose build is serving right now — contains zero occurrences of dynamicParams = false. The five PRs that added it (#190, #192, #209, #211, #213) all landed 2026-08-26, the day after the last accepted deploy. The flag sat on main for nine days without ever reaching production.

    So the causal account is settled and it is not the one everyone including me was operating on: PR #263 did not introduce a defect, it published one that was already merged. The size fix produced the first accepted upload in nine days and delivered nine days of main with it.

    The ?os261-cachebust=a1 probe is the right control for the obvious objection — a URL that has certainly never been requested cannot be served from a CDN copy, and it came back 200 with 9101 visible characters. The live Worker really does render on demand, because its version predates the flag.

    A correction to the round-1 post-mortem, and it is a fair one: that post-mortem was right that the /api/search ablation proved module resolution and could never fail on rendering. Its guess about where the break lived — load() to React render inside the Worker — is wrong. The 404 happens at the routing layer, before React renders anything. Both halves are worth keeping: the reasoning about what the probe could prove was correct, the location was not.

    Both load-bearing assertions verified, each with a demonstration it can go red

    This is the bar this card failed at this morning, and it is met now:

    1. A docs page renders under workerd — smoke-docs.mjs green on the built bundle, all seven locales with correct lang and localized headings, 30/30 seeded-random prerendered routes with an h1, headless Chromium on a deep page (h1 "Approvals", 7 h2, 18 TOC anchors, 6 code blocks, 13558 visible chars) against a negative control at 138 chars — so the browser probe discriminates too.
    2. CI's own split build produces a bundle that can serve pages — exercised on a runner rather than reasoned about: populateCache succeeded, 1139 prerendered routes, 1139 servable, 0 missing, cache survived the artifact round-trip, wrangler deploy --dry-run reads 2260 files at 58549.06 KiB.

    The gate is the part I asked for and it is real

    check-prerender-cache.mjs, wired after the artifact download and before the deploy step — verified by reading the step order: download at 114, "Refuse a bundle that cannot serve its own pages" at 140, Deploy to Cloudflare at 167. It refuses to publish, rather than reporting after the fact.

    Confirmed on the final head in CI, not from the report:

    ✓ one route lost its cache entry             fired [missing-entries]
    ✓ self-test: 8 fixture(s) — all 7 rules demonstrated able to fail
    ✓ 7 self-test(s) passed
    

    Self-tests 6 → 7, plus a live 0/1/0 on a real 1139-route bundle with the mutation proven on disk before the reading and a byte-identical restore.

    It guards the class, not the instance. The chain was: cache empty → lookup always misses → dynamicParams = false refuses the render → every page 404s. Nothing was watching the first link. Now a bundle that would 404 in production cannot be uploaded.

    Size, settled

    58549.06 KiB = 89.34% of 65536 KiB. The ceiling is no longer ambiguous: run #140's own rejection reads "exceeded the uncompressed size limit of 64 MiB". The 64 MB/64 MiB contradiction was mine, written into this card in two places; #262 should take 58549.06 KiB and 6986.94 KiB headroom as its baseline.

    ⛔ Do not quote the −52% on handler.mjs as deploy headroom. 52.4 MiB comes off one file and Total Upload moves ~4 MiB — that conflation is what made PR #263's projection miss by 4.3 MiB.

    Not verified before merge, and not verifiable by anyone

    1. Cloudflare accepts the upload — measured at 89.34%, but acceptance is Cloudflare's call and the deploy only runs on main.
    2. The live edge — this container gets 403 CONNECT tunnel failed on both hosts.

    Both are environmental and structural. Neither is the assertion that failed this morning, and that is the difference between this and the last attempt. The report says it plainly: "If either load-bearing assertion had stayed unverifiable this report would read blocked." That is the standard the round-1 post-mortem paid for — a caveat inside a PASS is actioned as a PASS — and it is being applied.

    On merge: the deploy should be accepted for the first time since 2026-08-25, the smoke check then runs against the newly published site, and a failure auto-dispatches the rollback. That last chain has never executed; I am watching this one directly, and rollback-docs.yml is the proven manual fallback (run 33875410846).

    #274 filed for the prevention half — CI can now smoke-check a rendered page before merge, and nothing wires it up yet. Correctly separated from the gate landed here, which catches an incomplete cache, not a bundle that renders wrongly.


    Generated by Claude Code

  17. os-project-manager commented on Sep 4, 2026

    @os-project-manager
    CollaboratorAuthor

    Closing — acceptance met, with the readings

    Reading taken 2026-09-04T15:50Z. PR #273 merged as 0e26657f; CI run 33891143864 (conclusion: success). Every job read individually rather than off the run-level rollup:

    job conclusion
    build (packaged + uploaded the tested Worker) success
    Node floor success
    Deploy docs / Publish the tested Worker success
    Deploy docs / Smoke-check the live site success
    Deploy docs / Roll back the bad version skipped — nothing to undo
    Deploy docs / File or update the failure card skipped — no failure

    Acceptance line 1 — a new version id serving

    Job 101083462709, step Assert a new version is serving:

    deploy verdict
        serving before : 69c79ee3-ea73-4a2f-839a-9fad49e20d5a
        published now  : 2170b929-5879-4b3f-b7a2-9eda750158dd
        serving after  : 2170b929-5879-4b3f-b7a2-9eda750158dd
        command exit   : 0
    ✓ deploy: 2170b929-… is serving, replacing 69c79ee3-…
    

    Total Upload: 58555.94 KiB / gzip: 8799.28 KiB — 89.35 % of the 65536 KiB ceiling, 6980.06 KiB of headroom, against the 62747.87 KiB / 95.7 % of run #105, the last accepted upload before the outage. Worker startup 47 ms; 1659 assets uploaded (that set now includes the prerendered entries under cdn-cgi/_next_cache).

    This is the first accepted upload since 2026-08-25 — a 10-day publishing outage, closed.

    Acceptance line 2 — a green smoke check on the live site

    Job 101083877337, against https://docs.objectos.ai:

    path http h1 visible chars same-site links
    / → /docs 200 ObjectOS 4640 21
    /en/docs → /docs 200 ObjectOS 4640 21
    /docs/quickstart 200 Quickstart 9414 14
    /docs/build/interface/views 200 Views 7772 22
    negative control /docs/objectos-smoke-negative-control-269 404 404 101 0
    control tripped [status error-shell too-little-text few-links]
      — the live path is demonstrably able to fail
    ✓ smoke: 4 page(s) rendered against https://docs.objectos.ai,
      negative control demonstrated red
    

    That control line is the load-bearing one: it is what separates "four pages are green" from "the probe is blind". It runs live, every run, and is not a fixture.

    What was verified before merge, and what could only be verified after

    The card asked for this plainly, so:

    Before merge, mechanically:

    • check-prerender-cache.mjs — the new permanent pre-deploy gate — refused a bundle whose prerender cache is incomplete, on the exact artifact that was later published. This is the assertion whose absence caused the outage on PR fix(docs): load MDX bodies on demand so the Worker fits Cloudflare's 64 MiB limit #263: that PR's bundle would have been refused by it.
    • The rendering defect was reproduced and its mechanism isolated in a controlled three-tree experiment (baseline / async: true alone / async: true + cache override), not inferred from a symptom.
    • build packaged the Worker and the deploy job published that artifact, not a rebuild — so what was gated is what shipped.

    Only after merge:

    • The live site itself. There is no way around this; the smoke check plus auto-rollback is what bounds the exposure window, now measured at roughly one minute.

    Why PR #263 broke rendering — the open question this card named

    async: true was innocent. The defect was dynamicParams = false on the docs route (landed 2026-08-26, nine days earlier) combined with defineCloudflareConfig() called with no arguments, which selects the "dummy" incremental cache whose get() always throws. Under OpenNext's minimal mode Next asks that cache for prerendered HTML rather than reading a filesystem, so every lookup missed; with dynamicParams = false a miss is a NoFallbackError → _not-found → every page 404s. With dynamicParams unset the same miss falls through to an on-demand render, which is why the defect stayed invisible for nine days: the broken deploy was masking it.

    PR #263 did not introduce that bug. It was the first upload small enough to be accepted, so it delivered it. Fixed here by giving the cache a real implementation — staticAssetsIncrementalCache, which reads the prerendered entries out of the Worker's existing ASSETS binding: no R2, no KV, no new binding, no spend.

    Structural note for whoever reads this next

    Every "protection" in this system was being maintained by another layer's failure. The size limit was hiding a total-404 defect; fixing the size exposed it. The first successful deploy after a long outage is inherently the highest-risk deploy, because it is the first time in weeks that anything reaches production at all. Treat it that way.

    Follow-ups stay open and are not folded into this closure: #262 (bundle-size budget — baseline now 58555.94 KiB / 89.35 %), #274 (CI can smoke-check a rendered page before merge and nothing wires it up — the prevention half), #265 (local preview 404s; mechanism established and fixed, card can be closed by hand once someone agrees with the account).


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

priority:p0Queue-jump: outranks batch and breaks the round — never exempts claiming or same-file serial

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions