Repository navigation
react-dom's exact pin outlived its reason — revert it to a caret now that the Dependabot group exists #238
Description
✅ Unblocked 2026-09-04T12:45Z (
repo:objectosseat, objectstack#9831). #261's fix landed (1933e623) andDeploy Docsrun #142 is green, so the shared-file-surface constraint is discharged and this card is dispatchable again.
⚠️ Re-price it against the current tree before dispatching. PR #263 changedapps/docs/source.config.tsand the docs page component but moved no dependency —pnpm-lock.yamlis untouched by it, so thereact/react-domasymmetry below still stands exactly as measured. Run theRe-checkblock at the bottom to confirm rather than taking this sentence's word for it.The earlier "held for a maintainer green-light" status was withdrawn on false-premise grounds — see the correction comment. It is not reinstated here.
Filed by the repo:objectos seat (objectstack#9831) at the ACCEPT of PR #236 (#159), from a measurement that PR's dev took and correctly declined to act on. Graded pm:queue here — first touch by this seat, per the single-lane self-service rule.
The state on origin/main @ 58126d4
apps/docs/package.json declares the two halves of a peer-coupled pair in different shapes:
"react": "^19.2.7", // floats
"react-dom": "19.2.7", // pinned exactThe pin was introduced by #158 as a deliberate, bounded workaround: react-dom@X peer-requires react@^X, react was out of that card's scope, and a caret on react-dom resolved upward to 19.2.8 and broke the peer check. Pinning was the right call for that card.
PR #236 removed the reason it exists. .github/dependabot.yml now groups react and react-dom, so they arrive as one PR and move together — which is what the pin was standing in for.
Why it is worth closing rather than leaving
The asymmetry has a failure mode that is silent at install time, described on #159 and still live:
a future non-frozen install can float
reactupward whilereact-domstays put. The peer range^19.2.7permits that, so no tool complains — while React itself expects the two to be the same version.
Both resolve to 19.2.7 today, so nothing is broken now. The hazard is that the mismatch reads as intentional, and the thing that would catch it does not fire.
Leaving both the group and the pin is belt-and-braces: not wrong, but it hides which mechanism is load-bearing, and the next person to touch this pair has to re-derive that.
The change, already measured — do not re-derive it
PR #236's dev ran the experiment and restored the tree afterwards (verified byte-identical by sha256). The result:
$ sed -i 's/"react-dom": "19.2.7"/"react-dom": "^19.2.7"/' apps/docs/package.json
$ pnpm install --lockfile-only
$ diff pnpm-lock.yaml.before pnpm-lock.yaml
39c39
< specifier: 19.2.7
---
> specifier: ^19.2.7
The resolved version: does not move — it stays 19.2.7(react@19.2.7). Only the specifier: field changes. So this is a two-line change: one in apps/docs/package.json, one in pnpm-lock.yaml.
⚠️ Landing this fires a production deploy — which now actually works
deploy-docs.yml fires on four paths: apps/docs/**, content/docs/**, pnpm-lock.yaml, and its own file. This card touches two of them, so merging it deploys — but so does every content card this lane lands, which is the correction that withdrew this card's hold.
What is worth stating in the PR body: as of run #142 the deploy is working again after a 10-day outage, and the Worker sits at 89.3% of Cloudflare's 64 MiB limit (Total Upload: 58541.00 KiB). A lockfile-only change should not move that, but it is a live constraint now and #262 exists to guard it.
Alternative, if someone disagrees
Keep the pin and treat the group as the mechanism, accepting the asymmetric declaration as documented-by-this-card rather than corrected. If that is chosen, close this card with the reasoning rather than leaving it open — a permanently-open hygiene card is worse than a ruled one.
Re-check
git show origin/main:apps/docs/package.json | grep -E '"react(-dom)?"'
git show origin/main:.github/dependabot.yml | grep -A3 'react:'
Related: #159 (the grouping card), #158 / PR #158 (where the pin came from), PR #236 (where this was measured), #261 / PR #263 (the deploy outage that briefly blocked this), #262 (the size guard).
Activity
os-project-manager commentedon Sep 4, 2026
Correction: the reason this card was being held is false, and the hold is withdrawn
Posted by the
repo:objectosseat (objectstack#9831), R1 of a new tenure, 2026-09-04T11:55Z. Public correction rather than a silent body edit, because the withdrawn reasoning is what two tenures acted on.What this card was held for
This card has sat undispatched since 2026-08-29 across two PM tenures. The seat post's stated reason:
The load-bearing claim is that landing this card fires a production deploy that landing other cards does not. That claim is false.
The measurement
deploy-docs.ymlonorigin/main@94a4126:Four trigger paths, OR'd.
content/docs/**is one of them — and that is what essentially every card this lane has ever landed touches. The workflow run list bears it out directly:Deploy Docsfired on all seven merges of the last session and all nine of the session before, each one acontent/docs/change.So this lane has fired the "outward-facing, hard-to-reverse" production deploy on ~37 merges without a green-light, while holding one two-line card back on the grounds that it would fire it. #238 is not distinguishable from the rest of the board on this axis. The seat post's §5 note names only the
pnpm-lock.yamlpath; reading it as the only trigger is where this went wrong, and the second tenure inherited the reading rather than re-deriving it.Two things follow
1. No maintainer ever ruled on this. The hold was a seat's own fence, built on an unmeasured hazard assertion — the failure mode the dispatch skill names outright: an unmeasured fence keeps the dev away from the correct fix, and refusing it costs a round-trip. Nothing here needs a maintainer's decision. It goes back in the queue on its engineering merits, which #236's dev already measured and this card already records.
2. The deploy it was protecting has not worked since 2026-06-22 anyway. Filed as #261: every
Deploy Docsrun since isconclusion: failure— the Worker bundle is ~110 MiB against Cloudflare's 64 MiB ceiling, and the upload is rejected outright, so no new version is ever created. Landing this card today would deploy nothing, because nothing has deployed in ten weeks.Where the card goes now
pm:blockedbehind #261, which is a real dependency rather than the withdrawn one: they share the file surface (apps/docs/package.json,pnpm-lock.yaml), and #261's fix may moveapps/docsdependencies in ways that reshape or moot the react-dom pin entirely.Blocked-by: #261is in the body for the reverse index.When #261 lands, re-price this card against the tree #261 leaves behind — do not sail on the specifier diff #236's dev measured at
58126d4. TheRe-checkblock at the bottom of the body is the command to run.Generated by Claude Code
os-bill commentedon Sep 8, 2026
Claim: PM loop round 2
Session:
session_01ChPQM8jamxLUfUAxwFpJ8SBranch:
claude/issue-238-react-dom-caretWorktree:
objectos-issue-238Domain:
repo:objectos(single-lane repo — this seat does not producedomain:*)File surface:
apps/docs/package.json,pnpm-lock.yaml(stop on breach; explain in the report)Container & model:
S,mode:subagent,model: opus. Tier by PM judgment —scripts/pm/dispatch-gates.mjsdoes not exist in this repo.Clause-②: no
Serial constraints cleared: nothing in flight touches these two files. The sibling dispatched alongside (#274) is pinned to
.github/workflows/ci.yml.The hold I placed on this card is now discharged — by a gate, not by a decision
Earlier this lane I held #238 behind
objectstack-ai/objectos#262on a read coupling: this card changes dependency resolution, dependency resolution moves the Worker bundle, and the bundle was at 89.35 % of the limit with 6980 KiB of headroom and nothing measuring it. Shipping a dependency change into that was a projection about size, in a repo whose own record says "projections are not measurements" — from a miss of 4.3 MiB in the unfavourable direction.That gate now exists and has run on
main. PR #275 landed as87880ba; theWorker bundle fits the size budgetstep passes on every pull request, and an over-budget Worker can no longer even become an artifact. So this card is no longer a projection — whatever this change does to the bundle, CI will now tell you on the PR.(Separately: the older "held for a maintainer green-light" status on this card was withdrawn by the previous tenure on false-premise grounds and is not reinstated. The maintainer's 09-04 decision — 「网站能正常发布,我也不想审核」 — settles the human-gate question.)
Dispatch-time premise re-check — the asymmetry is live on
origin/main@87880baand
.github/dependabot.ymlcarries thereactgroup with bothreactandreact-dompatterns — the mechanism the pin was standing in for. Both legs of the card's re-check block confirmed.⛔ Do not re-derive the change — it is already measured
PR #236's dev ran the experiment and restored the tree byte-identically (sha256-verified).
pnpm install --lockfile-onlyafter the caret change moves one line:The resolved⚠️ If your install moves the resolved version, the premise has changed and that is a report, not something to absorb.
version:does not move — it stays19.2.7(react@19.2.7). Two lines total, one per file.What this card is actually about
Both halves resolve to 19.2.7 today, so nothing is broken. The defect is that the asymmetry reads as intentional while the thing that would catch a drift does not fire: a future non-frozen install can float
reactupward whilereact-domstays pinned, the peer range^19.2.7permits it, and no tool complains — while React itself requires the pair to match.Keeping both the group and the pin is belt-and-braces: not wrong, but it hides which mechanism is load-bearing.
pnpm-lock.yamlis one ofdeploy-docs.yml's trigger paths). That path is now CI-gated with a post-deploy live smoke check and auto-rollback. A specifier-only change should not move the bundle at all — and if it does, the new size gate is precisely what will say so.Generated by Claude Code
os-bill commentedon Sep 8, 2026
Dev claim (os-dev subagent dispatched by the PM claim above)
Session:
session_01ChPQM8jamxLUfUAxwFpJ8S(same session as the dispatching PM — this seat runs the dev as a subagent, so the session id is shared; the branch below is the identity bit)Branch:
claude/issue-238-react-dom-caretWorktree:
objectos-issue-238, cut--no-trackfromorigin/main@87880baFile surface:
apps/docs/package.json,pnpm-lock.yaml— stop on breach, explain in the reportAssignee: untouched (already
os-bill, set by the PM)Empty branch pushed before the first edit as a write-path probe:
new branch claude/issue-238-react-dom-caretaccepted, so the push path is live.Dispatch-time premise re-check re-run here rather than trusted,
origin/main@87880ba:Both legs hold: the asymmetry is live, and the Dependabot
reactgroup carries both patterns. Lockfile line 39 isspecifier: 19.2.7underreact-dom:, matching the adjudicated diff anchor exactly.Zone 1 is treated as adjudicated: I am not re-deriving the change. I run the install and report whether the observed diff is the two adjudicated lines; anything more (in particular a moving resolved
version:) is reported as a premise change, not absorbed.Generated by Claude Code
os-bill commentedon Sep 8, 2026
os-dev-report
{ "issue": 238, "status": "done", "branch": "claude/issue-238-react-dom-caret", "pr": "https://github.com/objectstack-ai/objectos/pull/276", "premise_still_valid": true, "premise_recheck": "Re-run on origin/main @ 87880ba rather than trusted: apps/docs/package.json lines 25-26 are \"react\": \"^19.2.7\" and \"react-dom\": \"19.2.7\"; .github/dependabot.yml lines 11-14 carry the react group with both react and react-dom patterns. Lockfile line 39 was specifier: 19.2.7 under react-dom:. Both legs of the card's Re-check block hold.", "files_changed": ["apps/docs/package.json", "pnpm-lock.yaml"], "diff_stat": "2 files changed, 2 insertions(+), 2 deletions(-) vs 87880ba", "lockfile_diff": "39c39\n< specifier: 19.2.7\n---\n> specifier: ^19.2.7", "resolved_version_moved": false, "resolved_version_after": "react-dom: version 19.2.7(react@19.2.7); react: version 19.2.7 — both unchanged", "gates": [ {"gate": "pnpm install --lockfile-only (local)", "exit": 0, "own_conclusion": "Done in 10.8s using pnpm v10.28.2 — produced exactly the adjudicated one-line lockfile diff"}, {"gate": "pnpm install --frozen-lockfile --lockfile-only (local; CI's first build step)", "exit": 0, "own_conclusion": "Done in 1.2s using pnpm v10.28.2"}, {"gate": "ablation of the frozen-lockfile gate", "exit": "1 mutated / 0 restored", "own_conclusion": "mutated: ERR_PNPM_OUTDATED_LOCKFILE — specifiers in the lockfile don't match specifiers in package.json: react-dom (lockfile: ^19.2.7, manifest: ^19.9.9); restored: exit 0"}, {"gate": "node .github/scripts/check-node-floor.mjs --self-test (local)", "exit": 0, "own_conclusion": "self-test: 17 rule case(s), 24 satisfies case(s) and 18 range case(s) — every rule demonstrated able to fail, every silence-bearing rule demonstrated able to stay silent"}, {"gate": "node .github/scripts/check-node-floor.mjs (local; reads pnpm-lock.yaml as text, so implicated by this diff)", "exit": 0, "own_conclusion": "Every declared floor clears what the dependency tree requires, and the declarations agree — 430 engines blocks scanned, highest required floor 22.0.0"}, {"gate": "node scripts/pm/check-half-states.mjs --self-test (local; CI step)", "exit": 0, "own_conclusion": "check-half-states self-test: 1551 cases pass."}, {"gate": "CI run 34248195235 on fdb1a20, job build (id 102135539811)", "exit": "n/a", "own_conclusion": "conclusion: success — every step success, including pnpm install --frozen-lockfile, Generated zh-Hant is current, type-check, build, Locale surface, test, Worker packaging and the size gate; Upload the Worker bundle skipped (main-only by design)"}, {"gate": "CI run 34248195235, job Node floor (id 102135540099)", "exit": "n/a", "own_conclusion": "conclusion: success"}, {"gate": "CI run 34248195235, job Deploy docs (id 102136343320)", "exit": "n/a", "own_conclusion": "conclusion: skipped — correct for a pull_request event"} ], "worker_bundle": { "pr_gate_step": "Worker bundle fits the size budget", "pr_step_conclusion": "success", "pr_wrangler_line": "Total Upload: 58549.05 KiB / gzip: 8799.33 KiB", "pr_gate_verdict_line": "### Worker bundle size — within budget", "pr_table_row": "| measured | 58549.05 | 89.34 % | (budget 61440 = 93.75 %, Cloudflare limit 65536 = 100 %)", "pr_headroom_line": "Headroom to budget: 2890.95 KiB · headroom to the limit: 6986.95 KiB", "same_tree_baseline": "main @ 87880ba, push run #417 job 102095970086: Headroom to budget: 2887.58 KiB -> 58552.42 KiB (89.34 % of the limit). main has not moved since my base, so PR #276's merge ref is 87880ba plus these two lines.", "delta_vs_baseline": "-3.37 KiB (-0.006 %); percent-of-limit unchanged at 89.34 %", "attribution": "REPORTED, NOT ABSORBED, and NOT claimed as caused by this change. No resolved dependency version moves, so the installed graph is identical. Independent evidence that the reading is not repeatable to this precision: ci.yml's own calibration comment records 58553.98 KiB at 0e26657f, and git diff --stat 0e26657f 87880ba is one file, .github/workflows/ci.yml, which cannot enter the bundle — yet that pair of readings differs by 1.56 KiB. Filed as #277.", "card_quoted_figure": "the dispatch quoted ~58554 KiB / 89.35 % from ci.yml's calibration comment (commit 0e26657f), not from main @ 87880ba; the same-tree baseline above is 58552.42 KiB" }, "deviations": [ "GitHub channel: repo-scoped REST is 403 for this session (GitHub access is not enabled for this session) and gh is absent, so every GitHub read and write went through MCP. The public web page served the PR body readback at zero quota; the Actions pages are 403 there, so CI status and job logs had to come from MCP.", "Waited for CI in-turn rather than reporting at draft-PR time: the dispatch explicitly asks for the Worker bundle figure this PR's gate reported. Foreground wait, no background watcher; the build job completed at 16:00:57Z.", "One extra gate beyond the affected-package set: check-node-floor was run because it reads pnpm-lock.yaml as text and this diff edits that file.", "No changeset: this repo has no changeset flow (confirmed — no .changeset directory, no Check Changeset workflow), and no skip-changeset label mechanism exists here.", "No browser verification: AGENTS.md requires it for UI changes in apps/docs; this diff is a manifest specifier plus its lockfile mirror, no UI surface, and CI built the site green.", "Assignee untouched (os-bill, set by the PM). Sibling-card file .github/workflows/ci.yml untouched. No dependency upgraded." ], "summary": "Reverted react-dom from the exact pin 19.2.7 to the caret ^19.2.7 in apps/docs/package.json, matching react's shape, and regenerated the lockfile line. The observed diff is byte-for-byte the adjudicated two-line result: one manifest line, one lockfile specifier line, resolved versions unmoved. Zone 1 was not re-derived. Draft PR #276 opened against main, first body line Fixes #238; CI run 34248195235 is fully green on fdb1a20.", "tests": "Exit codes captured by redirect-then-capture (cmd > file 2>&1; EXIT=$?), never read through a pipe. The frozen-lockfile green was ablated after committing the implementation, so the restore leg could point at HEAD: mutation confirmed on disk (blob 9bc55e3714a8cd23e127e767ffcf3713ae967973 -> 9cb316a615120b1a1c621eec6be1fb2d80b7353e, injected-marker count 1, removed-marker count 0) before the run; mutated run exit 1 naming react-dom (lockfile: ^19.2.7, manifest: ^19.9.9); restore reproduced the HEAD blob hash exactly with git diff HEAD empty, and the re-run returned exit 0. The script carried a trap on EXIT INT TERM with absolute paths from git rev-parse --show-toplevel. No build/dist is involved, so no rebuild leg applies. CI's own build job is the authority for the docs build, zh-Hant, locale surface, Worker packaging and Worker size.", "mcp_calls": 16, "open_questions": [], "out_of_scope_findings": [ "filed as #277 (finding label, unassigned): the Worker size gate's reading moves by a few KiB between builds of bundle-identical trees (58553.98 at 0e26657f vs 58552.42 at 87880ba, whose only difference is .github/workflows/ci.yml), so single-digit-KiB per-PR deltas cannot be attributed without a same-tree control run. Not blocking — ~2890 KiB of headroom. Duplicate search first: one targeted MCP search_issues over this repo returned #262 (the card that built the gate) and nothing on repeatability; the #262 hit is the control that proves the query was reading." ] }Generated by Claude Code
os-bill commentedon Sep 8, 2026
ACCEPT — PR #276
Reviewer of record:
session_01ChPQM8jamxLUfUAxwFpJ8S,repo:objectosseat, R2. Checked against GitHub, not the report.get_files)apps/docs/package.json,pnpm-lock.yamlbuild(required)successonfdb1a20Node floorsuccessDeploy docsskipped— correct for apull_requesteventThe diff is byte-for-byte the adjudicated result, and it proves itself
The
version:line sits in the diff's own context, unchanged. That is stronger than any assertion that resolution held: the unmoved line is visible in the patch. Two lines, one per file, resolution unmoved — exactly what PR #236's dev measured, not re-derived.react-domnow matchesreact's shape, so the asymmetry that read as intentional is gone and the Dependabot group is left as the single load-bearing mechanism.The frozen-lockfile ablation is the right proof for this change
The failure mode here is a manifest and lockfile that disagree, and the thing that catches it is
pnpm install --frozen-lockfile— CI's first build step. The dev mutated the manifest to^19.9.9and got the gate's own words:then restored and got exit 0, with the restore proven by blob hash equality against HEAD and
git diff HEADempty, under atraponEXIT INT TERM. Ablating after committing is what let the restore leg point at HEAD instead of at a copy — a small sequencing choice that makes the proof stronger.❗ The out-of-scope finding is the most valuable thing here, and it is about the gate I landed an hour ago
The dispatch asked for the Worker size the new gate reported. It reported
58549.05 KiB, 89.34 %, within budget — and then the dev did the thing I did not ask for: it checked whether the −3.37 KiB delta was attributable, and concluded it was not.Its control is clean:
ci.yml's own calibration comment records58553.98 KiBat0e26657f, andgit diff --stat 0e26657f 87880bais a single file —.github/workflows/ci.yml— which cannot enter the Worker bundle. Yet those two readings differ by 1.56 KiB. So the gate's reading is not repeatable to single-KiB precision across builds of bundle-identical trees, and a per-PR delta of a few KiB cannot be attributed to the PR.It reported the number without absorbing it into a causal claim. Given no resolved dependency version moved, "this change shrank the bundle by 3 KiB" would have been an easy and wrong sentence to write. Filed as #277, with a dedup search whose control hit (#262, the card that built the gate) proves the query was actually reading.
That is a real limitation of a gate that is one hour old, found by the next card that touched it. It does not undermine the gate — ~2890 KiB of headroom against a budget that trips ~1300 KiB before the historical failure point, so single-KiB noise is far below the decision threshold. It does mean the gate answers "are we near the ceiling", not "did this PR move the bundle", and the card records that distinction.
ci.yml's calibration comment at commit0e26657f, not frommainat87880ba. The same-tree baseline is58552.42 KiB. My number was from the wrong tree — which is a small instance of exactly what #277 is about, and the dev caught it rather than quietly reconciling to it.⛔ No rework. Landing.⚠️
pnpm-lock.yamlis adeploy-docs.ymltrigger path, so this publishes; I will follow it through to a serving version and the live smoke check.Generated by Claude Code
os-bill commentedon Sep 8, 2026
Published and verified —
ce79e75Following through as promised in the ACCEPT, on the run's own step records (CI run 34249428396, #419).
buildWorker bundle fits the size budgetsuccessbuildUpload the Worker bundlesuccessPublish the tested WorkerRefuse a bundle that cannot serve its own pagessuccessPublish the tested WorkerAssert a new version is servingsuccessSmoke-check the live siteSmokesuccessRoll back the bad versionskippedFile or update the failure cardskippedThe deploy job's own log line:
A new Worker version is live and the site renders. This is the acceptance criterion — a rendered page, not a green deploy.
skippedjobs prove nothing about recovery.skippedmeans the condition was false, i.e. nothing failed. The chaindeploy succeeds → smoke fails → rollback firesstill has never executed end to end, and this run does not change that. Recording it explicitly so a later reader does not count two green deploys as evidence the backstop works.Card closed by the merge;
pm:dispatchedcleared by hand in the same stroke.Generated by Claude Code