Repository navigation
fix(deps): stop layout and plugin-dashboard installing a React of their own - #8911
Conversation
…ir own Both manifests asked the host for React as a peer AND pinned an exact React 19 in `dependencies` — two contradictory statements, where the pin is the one the installer acts on. A consumer on React 18 satisfied the peer range and still got a second React pulled into their graph, which surfaces as `Invalid hook call` at first render in their own component, pointing nowhere near these packages. `react` moves to `devDependencies` at the same pin (19.2.8); `peerDependencies` is untouched. That is the shape app-shell, auth, collaboration, i18n, mobile and permissions already have. Re-measured across all four pnpm-workspace globs (46 packages): 2 packages had the contradiction, 0 had it for react-dom, and 5 pin React with no peer at all (runner, site, 3 examples) — applications that supply React rather than ask for it, deliberately untouched. `react-peer-range-norm-3741.test.ts` gains the invariant, asserting the contradiction rather than "React in dependencies" so those 5 stay green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01611D6ZaRaMmwTNQmSbk8MH
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract review at
|
|
Handoff provenance — director seat, summon #18 segment 6 ( Generated by Claude Code |
…ct-dep-peer-contradiction
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
阻塞说明 — 两条红是同一个根因,且它需要一次本 PR 范围之外的裁决完整红清单(由实现席自己枚举,⛔ 非转述)
⭐ 它们是同一个不变量的两个载体: 根因(对齐两棵树实测,均为
|
Doc Snippet Type Check |
门自己的 pin | |
|---|---|---|
main 4fa0eb9c4 |
Root bound: no block … · 638 of 638 judged, 0 failed · exit 0 |
exit 0,101 passed |
本分支 edf2aced3 |
Root bound: 34 block(s) … (react) · 604 of 638 judged, 1 failed · exit 1 |
exit 1,1 failed / 100 passed |
机理由门自己打印:89 specifier(s) mapped → 88。恰好一个 specifier 离开了 map:react。 门自身的五组对照两侧读数完全相同。
⛔ 没有可移植的修复,且这不是判断,是实测
- 门自己推荐的
declare conststand-in 对react不成立。 它点名的范例(packages/layout/README.md"Usage with React Router")本身就是被拒的 34 个之一;探针照做后跳出 bound、直接撞出 6 个新 TS2786 —— 当作 JSX 组件用的 stand-in 必须让返回类型可赋给 React 真实的ReactNode,而真实 React 类型正是刚离开 map 的那个。34 个里 27 个含 JSX;@types/react在全仓零个包的dependencies里。 - ⛔ 没有记账接受这条路:那条 pin 是
expect(offenders).toEqual([])平铺断言,无豁免表、无账本、无理由字段 ⇒ CI 会一直红,不是安静降级。 - ⛔ 未做任何「为了绿」的动作:没有
fragment、没有 skip / disable / 豁免、没有把react挪回dependencies、peerDependencies全程未动、也没有在根 manifest 声明(objectui#6120 的 2026-08-24 裁定按名否决了那条路)。
⇒ 处置
已立 objectui#8919 作为决策卡:doc-snippet 门的 map 该不该纳入「工作区自身能解析的 peerDependencies」。⭐ 门自己的头注早已预见这次加宽 —— dependencies-only「fails CLOSED, and widening it later is a visible edit with a reason, not a silent drift」—— 而理由在这里很具体:门排除 peer 是因为「peer 是对消费者的一个可能未满足的要求」,
本 PR Blocked-by objectui#8919。 裁决落定并落地后,把 main 再并进来,那 34 个 block 回到语义阶段,两条红自行清除,且一个文档文件都不用改。
4fa0eb9c4,edf2aced3,无冲突;自身工作在合并后的树上复验绿:react-peer-range-norm 5 passed、changeset:check exit 0)。⛔ 未重跑失败作业 —— 这是一个确定性的解析事实,重跑不会改变它。
Generated by Claude Code
Complete red list on head
|
| # | failing check | surface |
|---|---|---|
| 1 | Doc Snippet Type Check | scripts/check-doc-snippet-types.mjs, exit 1 |
| 2 | Test (shard 1/4) | the vitest pin scripts/__tests__/check-doc-snippet-types.test.ts → "the ROOT BOUND … no COVERED snippet imports a specifier that only the root declares" — expect(offenders).toEqual([]), 34 offenders |
⇒ These are not two problems. They are the same measurement read through two instruments: the gate script and the pin that asserts the gate's result. Everything else in that CI run is green — 716 test files passed, 9570 tests passed, 1 failed.
⭐ CI's own output independently confirms the #8919 measurement, to the file
Every one of the 34 offenders is the same single specifier, react. Nothing else left the map. And the split across ownership is exactly what objectui#8919 reported from a local run, arrived at by a different route:
| blocks | distinct files | |
|---|---|---|
in the two packages this PR touches (layout, plugin-dashboard) |
2 | 2 |
| in packages and docs this PR does not touch | 32 | 19 |
| total | 34 | 21 |
The 19 untouched files span auth, providers, react, react-runtime, i18n, app-shell, plugin-chatbot, fields, the root README.md, and eleven content/docs/ pages.
⇒ The mechanism is confirmed, not merely inferred: packages/layout and packages/plugin-dashboard were the only two published packages declaring react in dependencies, and the gate builds its specifier map from dependencies only (deliberately). They were therefore supplying React type-resolution for the entire documentation corpus. Removing the declaration — which is precisely what this card asked for — drops react out of the map and un-resolves 34 blocks repo-wide, 94% of them in files with no relationship to this change.
path:line citations are CI's snapshot of a mutable quantity; ⛔ they are not anchors to hand to anyone, and they will move.
Why there is no fix to push, and no re-run coming
This failure is genuinely this PR's — ⛔ I am not claiming it belongs to someone else. It is red on this branch and green on main, it is caused by this diff, and it is deterministic (identical on two heads). "Flake" is not a root cause here and a re-run would measure nothing, so I am ⛔ not spending one.
What blocks the fix is that every route out has been measured and only one is live:
- A — widen the gate's map to workspace-resolvable
peerDependencies. The only live option. The gate's own header pre-authorizes widening "with a reason, not a silent drift". ⛔ Needs a ruling; it changes a shared gate's contract. - B — the gate's own recommended
declare conststand-in. ⛔ Tested and dead. Its named worked example is itself one of the 34, and the probe produced 6 newTS2786. 27 of the 34 blocks contain JSX, and@types/reactis in zero packages'dependencies. - C — fragment the blocks. Forbidden.
- D — declare the debt. ⛔ Unavailable: the pin is a flat
toEqual([])with no exemption list, no ledger, no reason field. There is nowhere to record an accepted offender. - E — revert. That is
wontfixon this card.
⇒ Blocked-by: objectui#8919, which is in the decision box awaiting a one-word ruling. This PR stays watched and stays red until that lands; ⛔ I will not skip, disable, quarantine or exempt the pin to manufacture a green, and ⛔ I will not push an empty commit to kick CI.
Generated by Claude Code
|
Tier notice — the contract-review-tier requirement on this PR is lifted (skills seat, session Maintainer ruling, verbatim: 「现有的卡片如果写了要求fable的,也要让相关的项目经理知道,opus就够了。」 Under the same ruling set (quoted in full on objectstack-ai/objectstack#17285), the contract-review tier is reserved for the skills seat (protocol files + the published For this PR: its Generated by Claude Code |
…ct-dep-peer-contradiction
…#8919) `check-doc-snippet-types.mjs` built its third-party specifier map from `dependencies` only. That was stated as a conservative, fail-closed edge — and the header reserved the right to widen it later "with a reason, not a silent drift". This is that widening, and this is the reason. The field split was not merely conservative here: it was reading a manifest DEFECT as coverage. `react` reached the whole documentation corpus only because `@object-ui/layout` and `@object-ui/plugin-dashboard` pinned it in `dependencies` while also asking their host for it as a peer — the contradiction objectui#8303 removed. Correcting those two manifests took the map's only `react` entry with it and refused 34 blocks across 21 files in 19 packages the correction never touched. The map now also reads a specifier an imported package REQUIRES of its consumer in `peerDependencies` — not marked optional in `peerDependenciesMeta`, and resolving from inside that package's own directory to a declaration file outside any package's `src/`. A required peer is not an optional extra a reader may lack: the package declares it cannot function without it, so it reaches every reader who can use the package at all. What stays closed: optional peers, devDependencies, required peers that ship no types, and every specifier no imported package names in either field. The UNDECLARED and ROOT-DECLARED controls are unchanged and still red, and the peer half runs as a SECOND pass so it can only add specifiers — never re-own or re-resolve one `dependencies` already backs. Every run now prints how many mapped specifiers came from the peer half, in both gates that share this resolver, so the size of the widening is readable off a green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01611D6ZaRaMmwTNQmSbk8MH
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
|
os-dev-report {
"issue": 8303,
"status": "done",
"branch": "claude/issue-8303-react-dep-peer-contradiction",
"pr": "https://github.com/objectstack-ai/objectui/pull/8911",
"premise_still_valid": true,
"summary": "Implemented the objectui#8919 ruling (option A) on top of PR #8911's manifest correction, as commit 8428db85f over a real `git merge --no-ff origin/main` (merge commit 4b59f15d3, bbe285ee7 merged in, zero conflicts). `deriveDeclaredDependencyPaths` in scripts/check-doc-snippet-types.mjs built the third-party specifier map from `dependencies` only; it now also maps a specifier an imported package REQUIRES of its consumer -- named in its own `peerDependencies`, NOT marked optional in `peerDependenciesMeta`, and resolving from inside that package's own directory to a declaration file outside any package's `src/`. New named predicate `requiredPeerSpecifiers()`. The peer half runs as a SECOND pass after every owner's `dependencies` is decided, so it can only ADD a specifier -- never re-own or re-resolve one `dependencies` already backs; a pin holds that ordering over a tree where a peer of the alphabetically-first package and a dependency of the second name the same specifier. The reason is written into the header under three headings a new pin requires to exist (THE CLASS IT NOW RESOLVES / WHY THAT IS SOUND / WHAT STAYS CLOSED), honouring the edge's own demand that widening be 'a visible edit with a reason, not a silent drift'. Soundness as written: a required peer is not an optional extra a reader may lack -- the package declares it cannot function without it, npm 7+ and pnpm (auto-install-peers=true here) install it, and a reader who has not supplied it holds an install the package itself calls broken; so it reaches every reader who can use the package at all. What stays closed and is stated as such: optional peers, devDependencies, required peers that resolve to no declaration file, and every specifier no imported package names in either field. Nothing was declared at the repository root (the 2026-08-24 objectui#6120 ruling stays honoured). Both gates that share the resolver now print how many mapped specifiers came from the peer half, so the size of the widening is readable off a green.",
"tests": "ALL readings on the merge result. BEFORE (4b59f15d3, pre-change): `node scripts/check-doc-snippet-types.mjs` exit 1 -- 'Root bound: 34 block(s) import a specifier that resolves only through the ROOT manifest (react) and were NOT semantically checked.' / 'Semantic phase: 604 of 638 block(s) judged, 1 failed.' / 'Third-party resolution: 88 specifier(s) mapped'; `pnpm exec vitest run scripts/__tests__/check-doc-snippet-types.test.ts` exit 1 -- 'Tests 1 failed | 100 passed (101)', AssertionError 'expected [ ...(34) ] to deeply equal []'. AFTER (8428db85f): gate exit 0 -- 'Root bound: no block imports a specifier that resolves only through this repository's ROOT manifest.' / 'Semantic phase: 638 of 638 block(s) judged, 0 failed.' / 'Third-party resolution: 93 specifier(s) mapped ..., 5 of them from a REQUIRED peerDependency this workspace resolves'; pin test exit 0 -- 'Test Files 1 passed (1)' / 'Tests 107 passed (107)'. The five newly mapped, measured: react (via @object-ui/app-shell -> @types/react/index.d.ts), react-dom (@types/react-dom), react-router-dom, react-grid-layout (@object-ui/plugin-dashboard), tailwindcss (@object-ui/components). The last remaining semantic failure (packages/react-runtime/README.md:72 TS2307 Cannot find module 'react') is gone with them. Derived build filter byte-identical before/after in BOTH gates (diff exit 0). INJECTION (the required bright-line deliverable): 4 blocks appended to content/docs/guide/troubleshooting.md, trap-guarded ('trap restore EXIT INT TERM', restore = `git checkout HEAD -- path`, absolute REPO_ROOT). Mutation proven on disk BEFORE the run: marker count 0 -> 4, blob fe98cec31 -> 9f4b2ab6a, 'git diff --stat' 1 file changed 27 insertions. Result GATE_WITH_INJECTION_EXIT=1 -- 'Root bound: 1 block(s) ... (vitest)', 'Semantic phase: 641 of 642 block(s) judged, 3 failed': [bound] troubleshooting.md:389 'vitest'; [semantic] :397:33 TS2307 Cannot find module '@floating-ui/react-dom'; [semantic] :405:14 TS2322 Type 'number' is not assignable to type 'string' (the NEWLY MAPPED react types, misused -- this is the one that proves react was not resolved to any); [semantic] :411:50 TS2322 (ill-typed block against a workspace package). Restore proven by STATE: 'git diff HEAD' empty, blob back to fe98cec31, marker count 0, 'git status --short' empty. ABLATION (the peer half is what does the work): one-line mutation of requiredPeerSpecifiers to return [], trap-guarded; mutation proven on disk (blob 69a2c372d -> 089f67b3e, marker count 1); GATE_ABLATED_EXIT=1 with exactly the pre-change reading back -- '88 specifier(s) ..., 0 of them from a REQUIRED peerDependency', 'Root bound: 34 block(s) ... (react)', '604 of 638 judged, 1 failed'; PIN_ABLATED_EXIT=1 with 5 failed (the four new peer pins plus the root-bound pin). Restore proven by state: 'git diff HEAD' empty, blob back to 69a2c372d, marker absent. SHARED RESOLVER: scripts/check-skill-examples.mjs imports deriveDeclaredDependencyPaths from this gate, so it widened too (61 -> 65 mapped, 4 from peers) and carries the same printed count; exit 0 before and after, coverage unchanged (14 of 14 ts fences judged, 0 failed). OTHER VERDICTS, exit codes captured before any pipe, heavy runs through the container's shared verify lock (OS_VERIFY_LOCK_SLOT=objectui-8303): turbo filtered build 35/35 successful (VERDICT command-exit 0); check-doc-snippet-types.mjs --emit-census exit 0 (report-only, still names bound + semantic findings); vitest over the 11 script test files that name either edited script -- 'Test Files 11 passed (11)' / 'Tests 557 passed (557)'; type-check:scripts exit 0; eslint on the 3 changed files exit 0; check:control-bytes exit 0 'OK (scanned 7127 tracked text file(s); skipped 85 binary)' plus a manual control-byte grep over the changed files (grep exit 1, no hits); check-changeset-presence exit 0 '0 of them published source of a package the release covers ... no changeset is owed' (this half is scripts/** only; the manifest half's existing changeset is untouched); check-governed-queue-guard --test exit 0 'NOT GOVERNED -- 5 path(s) checked against 5 governed surface(s); none matched.'. NOT MEASURED (exit 2 / prerequisite): none -- every exit-2 encountered was none; the gate's prerequisite (a built closure) was satisfied by the filtered turbo build before every gate run. Repo-wide `pnpm lint` and the full shard matrix are left to CI.",
"mcp_calls": "0 -- every GitHub read and the PR-body write went through repo-scoped REST; no MCP GitHub tool was called",
"open_questions": [],
"out_of_scope_findings": [
"noted, not filed: the bound's refusal message still points at packages/layout/README.md 'Usage with React Router' as the worked stand-in example, and that block no longer NEEDS the stand-in (both react and react-router-dom are now mapped). It still compiles and still demonstrates the technique, which remains the remedy for an optional or unresolvable peer, so nothing is broken. objectui#8919 section 6 parked 'the example must be re-pointed' explicitly as not ruled by that card. Carrier: that follow-up, or whoever next edits that README.",
"noted, not filed: packages/react-runtime/README.md's `typeof import('react')` is not seen as an import by the bound's AST scanner, so the same specifier gets two different verdicts depending on import syntax. Moot for react now that it is mapped; the asymmetry itself remains for any future bounded specifier. Also parked by objectui#8919 section 6. Carrier: the same follow-up.",
"noted, not filed: .changeset/6120-doc-snippet-dependency-resolution.md is still pending (unreleased) and describes the map as 'dependencies only (not peers, not devDependencies)'. It correctly records what objectui#6120 did, so it was NOT edited -- but it will land in the CHANGELOG describing a rule this PR has since widened. Carrier: the next release's changeset review.",
"noted, not filed: the earlier contract-review-tier verdict on this PR raised F2 (LOW) -- .changeset/8303-react-dep-peer-contradiction.md is CHANGELOG-visible yet carries internal tooling analysis. Not touched here: this dispatch scoped me to the resolver gap, and the contract-review tier on this PR has since been lifted (comment 5612090324). Carrier: this PR, if the lane seat still wants the trim.",
"noted, not filed: @types/react is in ZERO packages' `dependencies` repo-wide, so `react`'s types reach the map through TypeScript's @types lookup from the declaring package's directory -- root-hoisted for @object-ui/layout and @object-ui/plugin-dashboard, package-local for @object-ui/react and @object-ui/plugin-charts. That is the same lookup the map used before this change; recorded as a measured fact about what backs the mapping, not as a defect. Carrier: none.",
"Nothing above was filed, so nothing needed deduplicating."
]
}Generated by Claude Code |
Fixes #8303
@object-ui/layoutand@object-ui/plugin-dashboardwere each making two contradictory statements in one manifest: asking the host for React as a peer (^18.0.0 || ^19.0.0) and also pinning an exact React 19 of their own independencies. The peer range is what a consumer reads; the pin is what their installer acts on. A consumer on React 18 therefore satisfied the peer range and still got a second, pinned React 19 pulled into their graph on these two packages' account.Two React copies in one tree is the classic cause of
Invalid hook call— and the failure is not at install time. It is at first render, in the consumer's own component, with nothing pointing back at these manifests.reactmoves todevDependenciesat the same pin (19.2.8).peerDependenciesis untouched, byte for byte.The population reading, re-derived rather than inherited
Re-measured on
origin/mainat348725a7cacross all fourpnpm-workspace.yamlglobs (46 packages), not carried over from the card'se2409120freading:reactpeer and pinsreactindependencieslayout,plugin-dashboardreact-dompeer and pins it independenciesreactindependencieswith no react peerrunner,site, 3examples/*That last row is the control that keeps this narrow, and it is a refinement of the card: the card says "no other released package does this", and that is true of the contradiction. Three packages plus the apps do pin React in
dependencies— they simply declare no peer beside it. They are applications that supply React rather than libraries that ask for it: one statement, not two, and correctly untouched here.Against the 20+ sibling libraries this converges on the existing house shape.
app-shell,auth,collaboration,i18n,mobileandpermissionsalready carryreactindevDependenciesat exactly this pin.The pin, and proof it can fail
scripts/__tests__/react-peer-range-norm-3741.test.tsgains the invariant. That file was already the same-class guard — it owns React manifest norms across the fixed version group, already walks thepnpm-workspace.yamlglobs, and already governsreact+react-dom— so this is a second assertion on an existing guard, not a new gate.It asserts the contradiction, not "React in
dependencies". Widening it further would go red on the five applications above for doing the right thing.Ablation, both legs, with the mutation proven on disk before the run and the restore proven by state:
Pre-fix —
Tests 2 failed | 3 passed (5), both new assertions red, naming both packages:The three pre-existing assertions stayed green through the mutation, which is what says the two new ones are the discriminating part rather than the file being broken. Post-fix:
Tests 5 passed (5).Two greens that are not readings of this
Both instruments below are green on this shape by construction, and neither green was evidence the defect was absent:
check:unused-depsasks whether a declared runtime dependency has a consumer.reactis imported by both packages, so the declaration has one. This is exactly why objectui#8198 could see and remove thereact-domhalf from these same two manifests and could not see this one — that card's licence covered declarations with no consumer.check-changeset-presence.mjsreads an eight-field allowlist (sideEffects,exports,main,module,types,files,peerDependencies,engines) that deliberately excludesdependencies. Its verdict on this very branch:0 of them a manifest whose published contract moved … no changeset is owed.Its own docblock states the trade rather than claiming invisibility — "a runtime dependency bump can be just as user-visible as any of the eight, and this gate still does not see it."The changeset here is therefore owed by the house rule, not demanded by the gate. It is
patchon both packages, following.changeset/5753-providers-drop-spec-dep.md, which removed adependenciesedge from a published package at the same level. Per AGENTS.md's version-alignment rule no changeset in this repo declaresmajor;changeset:checkconfirms.Verification
vitest run scripts/__tests__/react-peer-range-norm-3741.test.tsTest Files 1 passed (1)·Tests 5 passed (5)turbo run type-check --filter=@object-ui/layout --filter=@object-ui/plugin-dashboardTasks: 15 successful, 15 total(includes the^buildclosure)vitest run packages/layout/ packages/plugin-dashboard/Test Files 122 passed (122)·Tests 1119 passed (1119)check:phantom-depsEvery in-scope import is declared by the package that publishes it.check:unused-depsEvery gated declaration has a consumer in the package that declares it.check:lockfile-integrityVERDICT clean — no @objectstack/* identity moved backward and no package gained a copy.check-changeset-presenceno changeset is owed(quoted above — the gate is blind here on purpose)changeset:checkAll workspace packages are in the changeset fixed group.·No changeset declares a major bump.check:control-bytesOK (scanned 7088 tracked text file(s))check-governed-queue-guard --testNOT GOVERNED — 5 path(s) checked against 5 governed surface(s); none matched.Every heavy run went through the container's shared verify lock; exit codes were captured before any pipe.
@object-ui/layouthas notestscript but 21 test files, so its suite was run from the repo root —pnpm --filter @object-ui/layout testmatches zero scripts and exits 0 having run nothing.pnpm installmovedreactbetween the two importers' sections inpnpm-lock.yamland nothing else (6 insertions, 6 deletions). Thereact-domline that remains under each importer's lockfiledependenciesis pnpm'sauto-install-peers=truebookkeeping for a resolved peer, not a manifest entry — confirmed againstplugin-charts, which declares both React peers with no manifest edge and shows the identical lockfile shape.Position axis
Verdict (B) — nothing stale. Judged by anchor, not by line number, on both carrier classes:
scripts/check-doc-example-types.mjsUNGATED_EXAMPLES— the confirmed stored carrier the census cannot see. Its 90 keys include three underpackages/layout, all pointing at.tsxsources (AppSchemaRenderer,NavigationRenderer,ResponsiveGrid). This diff touches no.tsxfile, so none of those lines move.scripts/cross-file-line-citation-census.mjs— run on the branch and on a realgit merge --no-ffresult tree againstmain; the two drift sets are byte-identical (diffexit 0). All reported drift is pre-existing and names none of the touched files.Clause-② —
yesJudged from this diff rather than inherited, and it does not rest on the mechanical floor (no new exported symbol, and this removes an entry rather than adding a key):
package.jsonships inside the tarball, anddependenciesis the field a consumer's installer acts on. This change alters what every consumer of two published packages installs — that is a published-contract change in substance, whatever the changeset gate's allowlist happens to cover.dependenciesis a documented, accepted trade — quoted above — not a finding that the field is invisible. Reading its green as "no contract moved" would be the same instrument-blindness error this card exists to correct.needs:contract-reviewis attached to both carriers — this PR and objectui#8303 — in the same act.⭐ Kept verbatim below as the record of how the coupling was found and what was measured about the alternatives. The ruling on objectui#8919 landed as option A, and the gate change that implements it is the section after this one. Both checks are green on
8428db85f.Rebased onto the moved base first:
origin/mainadvanced to4fa0eb9c4while this was open, somainwas merged in (edf2aced3) and every reading below was retaken with both trees at that same base. An A/B across two different trees would not have been comparable.The complete red list, enumerated rather than relayed
Taken from
GET /repos/objectstack-ai/objectui/commits/…/check-runs?per_page=100on the head, because a per-check CI notification is a subset by construction:Two, and only two. Both are the same defect in this PR, so this is one problem, not two.
Both reds are one cause
Test (shard 1/4)is not a separate breakage. It isscripts/__tests__/check-doc-snippet-types.test.ts:1255— the doc-snippet gate's own pin test — asserting the identical invariant and receiving the identical 34 entries:100 of that file's 101 tests still pass. So the gate script's exit code and this unit test are two carriers of one invariant, and one fix moves both.
A/B on matched trees, the gate's own summary lines verbatim
The mechanism is a one-specifier delta the gate prints itself:
Third-party resolution: 89 specifier(s) mapped …on main against88 …here, from the same 34 imported packages, with all of the gate's own controls reading identically on both sides. The single specifier that left the map isreact.packages/layoutandpackages/plugin-dashboardwere the only two published packages declaringreactindependencies, so between them they were supplyingreacttype-resolution for the entire documentation corpus. The gate builds one program-wide specifier map fromdependenciesonly — deliberately, "notpeerDependencies, notdevDependencies". Removing the contradiction removes the map entry. Only 2 of the 34 refused blocks are in the READMEs of the two packages this card touches; the other 32 sit in 19 files belonging to packages it does not.The 34 span 21 files:
packages/providers/README.md(4),packages/auth/README.md(4),content/docs/guide/troubleshooting.md(3),packages/react/README.md(2),packages/react-runtime/README.md(2),packages/i18n/README.md(2),packages/app-shell/README.md(2), rootREADME.md(2), and one each inpackages/plugin-dashboard/README.md,packages/plugin-chatbot/README.md,packages/layout/README.md,packages/fields/README.md,content/docs/plugins/plugin-kanban.mdx,content/docs/guide/user-state-persistence.md,content/docs/guide/schema-rendering.md,content/docs/guide/schema-playground.md,content/docs/guide/plugin-development.md,content/docs/guide/component-registry.md,content/docs/fields/location.mdx,content/docs/core/theme-schema.mdx,content/docs/core/schema-renderer.mdx. The one semantic failure ispackages/react-runtime/README.md:72:36,TS2307: Cannot find module 'react'— a block that already uses the stand-in pattern and reaches the semantic phase only because the bound scanner does not treattypeof import()as an import.The gate's recommended remedy was tested, and it does not work for
reactThe refusal names stand-ins, and names
packages/layout/README.md"Usage with React Router" as the worked example. That example is now itself one of the 34 refused blocks — it importsComponentTypeandReactNodefromreactprecisely because the package declaredreactindependencies.Applying the remedy to it as a throwaway trap-guarded probe (restored;
git diff HEADempty, blob back tocd39170bb) moved the block out of the bound —Root bound: 33,Semantic phase: 605 of 638— and straight into 6 new semantic errors:(The two
Promisegenerics are spelled with square brackets in that quote only — GitHub body sanitising eats angle-bracket fragments, so the original would not survive the round trip. Nothing else in the message is changed.)That is structural. A stand-in used as a JSX component must be typed so its return is assignable to React's real
ReactNode, and React's real types are exactly what left the map. JSX itself keeps compiling — which is why 604 blocks still pass — because the JSX runtime types arrive through the root, not through this map; so such a block can neither import React's types nor hand-roll them. Classifying all 34: 27 contain JSX and are in the class just measured as failing; only 7 have no JSX, and several of those import hooks (useState,useEffect,forwardRef) whose real generic signatures a stand-in would also have to reproduce to check anything.@types/reactis in zero packages'dependenciesrepo-wide, so React's type vocabulary is unreachable by the map by any route.The repository had already registered the shape of this: the pin test's own header (objectui#8059) says the refusal "came to name two remedies, one of which reads as impossible on a peer (the package DOES declare it, in a field this map does not read)". This measures that "reads as impossible" as is impossible, for 27 of 34.
There is no declared-debt route either
check-doc-snippet-types.test.ts:1255assertsexpect(offenders).toEqual([])flat — no exemption list, no ledger, no reason field. So the 34 cannot be recorded as accepted debt even if someone wanted to; the invariant is absolute and a test holds it.⛔ What was deliberately NOT done
No green was manufactured. Not declared
fragment(the gate's stated last resort — it stops the whole block compiling, the documented package's own surface included, which is a larger coverage loss than the one it hides, and it would silence the pin test by removing the blocks from the compiled set). Not skipped, disabled or exempted. Not reverted todependencies, which reinstates the defect.peerDependenciesuntouched throughout. And not routed through a root declaration: the 2026-08-24 ruling on objectui#6120 rejects declaring a specifier at the repository root to make a checker pass, by name.This was a trade-off between the correctness of two published packages' dependency declarations and the type coverage of 34 documentation blocks, and it was a ruling rather than an implementation choice — so it was referred, on objectui#8919, rather than decided here. It has since been ruled: option A. The implementation is the next section. The gate's own header anticipates one possible answer's shape:
dependencies-only "fails CLOSED, and widening it later is a visible edit with a reason, not a silent drift." Whetherreact— a peer a consumer of a React component library must by definition already have — is that reason is the open question, and it is not this card's to answer.⭐ The ruling, implemented — the specifier map now reads REQUIRED peers (objectui#8919, option A)
Commit
8428db85f, on top of a realgit merge --no-ff origin/main(4b59f15d3,bbe285ee7merged in, no conflicts). Every reading below was retaken on that merge result, not on the pre-merge base.The resolver change
deriveDeclaredDependencyPathsbuilt the third-party map fromdependenciesonly. It now also maps a specifier an imported package REQUIRES of its consumer, and only that class — all three conditions, or it is not mapped:peerDependencies;peerDependenciesMetadoes not mark itoptional;dependenciesuses — to a declaration file outside any package'ssrc/.requiredPeerSpecifiers()is the new named predicate for (1)+(2). The peer half runs as a SECOND pass over the imported packages, after everydependenciesentry of every owner is already decided, so it can only ever add a specifier: it cannot re-own an existing mapping or re-resolve one from a different directory. A new pin holds that ordering over a tree where a peer of the alphabetically-first package and a dependency of the second name the same specifier —dependencieswins.The reason written into the source, as the header asked for
That edge reserved the widening in these words: dependencies-only "fails CLOSED, and widening it later is a visible edit with a reason, not a silent drift". The reason now lives in the file, under three headings a new pin requires to be present —
THE CLASS IT NOW RESOLVES,WHY THAT IS SOUND,WHAT STAYS CLOSED:auto-install-peers=truehere) install it, and a reader who has not supplied one is holding an install the package itself calls broken. It therefore reaches every reader who can use the package at all — the same reachability adependenciesentry has, arriving through the reader's own tree instead of the package's. On a React component library the case makes itself: a consumer with noreactcannot render one node of any package documented here.devDependencyreaches no consumer and stays unmapped. A required peer that resolves to no declaration file stays unresolvable rather than approximated. Every specifier no imported package names in either field is still refused by the bound. Nothing was declared at the repository root; the 2026-08-24 ruling on objectui#6120 rejects that route by name and it stays rejected.Both failing checks, on matched trees
4b59f15d3)8428db85f)node scripts/check-doc-snippet-types.mjsRoot bound: 34 block(s) … (react) and were NOT semantically checked·Semantic phase: 604 of 638 block(s) judged, 1 failedRoot bound: no block imports a specifier that resolves only through this repository's ROOT manifest.·Semantic phase: 638 of 638 block(s) judged, 0 failedvitest run scripts/__tests__/check-doc-snippet-types.test.tsTests 1 failed | 100 passed (101)·expected [ …(34) ] to deeply equal []Tests 107 passed (107)Third-party resolution: 88 specifier(s) …becomes93 specifier(s) …, 5 of them from a REQUIRED peerDependency this workspace resolves. The five, measured rather than assumed:react,react-dom,react-router-dom,react-grid-layout,tailwindcss. The one semantic failure that was left (packages/react-runtime/README.md:72,TS2307: Cannot find module 'react') is gone with them, so the corpus is back to the coveragemainhad — 638 of 638 — with the manifest defect fixed rather than reinstated.The derived build filter is byte-identical before and after in both gates (
diffexit 0): the widening grows what resolves, not what CI builds.⭐ The bright line: the gate can still report failures
A resolver that resolves everything would be worthless, so this is measured, not asserted. Four blocks were injected into one covered document (
content/docs/guide/troubleshooting.md), trap-guarded, with the mutation proven on disk before the run (marker count 0 -> 4; blobfe98cec31->9f4b2ab6a) and the restore proven by state afterwards (git diff HEADempty, blob back tofe98cec31, marker count 0):Each injection answers a different way this could have gone wrong:
vitest) is still refused by the bound, and still kept out of the semantic program.@floating-ui/react-dom) still fails TS2307. Resolution did not widen intonode_modules.reactand misusing it goes red with a real TS2322. The mapping supplies React's real types; it did not resolvereacttoany, which is the failure mode that would have made every one of the 34 blocks green without checking anything.The gate's own five controls are unmoved and still red where they must be:
sentinelTS2305,undeclared(@floating-ui/react-dom) TS2307,root-declared(vitest) TS2307,resolutionlanding onpackages/types/dist/index.d.ts,positiveclean. Neither control specifier is a peer of anything — a new pin asserts that for both, in both fields the map now reads, withreactas its known positive so the two zeros are readings rather than an empty probe.Ablation — the peer half is what does the work
One-line mutation of
requiredPeerSpecifiersto return[], trap-guarded, mutation proven on disk (blob69a2c372d->089f67b3e, marker count 1) and restore proven by state (git diff HEADempty, blob back to69a2c372d, marker count 0):Exactly the pre-change reading returns, so the 34 are closed by this predicate and not by anything else in the diff.
The shared resolver, and what else was run
scripts/check-skill-examples.mjsimportsderiveDeclaredDependencyPathsfrom this gate, so the skills corpus widened with it —61 -> 65mapped, 4 from peers — and it is carried deliberately: it prints the same peer count. It was green before and is green after (exit 0,Root bound: no selected fence …,Semantic phase: 14 of 14 ts fence(s) judged, 0 failed), and its coverage is unchanged.check:doc-snippets638 of 638 block(s) judged, 0 failedcheck-doc-snippet-types.mjs --emit-census(report-only)check:skill-examples14 of 14 ts fence(s) judged, 0 failedvitest run× the 11 script tests that name either edited scriptTest Files 11 passed (11)·Tests 557 passed (557)type-check:scriptstsc -p tsconfig.scripts.json, no diagnosticseslinton the three changed filescheck:control-bytesOK (scanned 7127 tracked text file(s); skipped 85 binary)check-changeset-presence0 of them published source … no changeset is owed(this half isscripts/**; the manifest half's changeset is unchanged)check-governed-queue-guard --testNOT GOVERNED — 5 path(s) checked against 5 governed surface(s); none matched.Exit codes were captured before any pipe. Heavy runs went through the container's shared verify lock.
验收备注
Out-of-scope observations, none filed as cards, all
noted, not filed:reactindependencies;@object-ui/runneris the third. It declares no react peer, so it is not this defect, and it is left alone. Carrier: this PR body and the new test's docblock, which names the five no-peer packages as the deliberate exclusion. No separate card — there is no defect to file.@types/reactindevDependencies, unlike the siblings whose shape they now otherwise match.type-checkis green for both (15/15 tasks), so nothing is broken; the rootdevDependencies.reactplus pnpm's peer auto-install cover local resolution. Unifying that is the "don't tidy other packages' dependency shapes" boundary of this card. Carrier: objectui#8300 already owns the React peer-convention question across these same two manifests and would be the natural place. No card filed — no reproducible defect and no violated declared contract.Nothing above is filed, so nothing needed deduplicating; the scan was run anyway to see whether a card already owns either observation. REST
GET /repos/objectstack-ai/objectui/issues, most-recently-updated page ofstate=openand ofstate=closed(130 issues after dropping PRs), grepped locally forreact/peer/dependenc: 33 hits, none of them this defect or either observation. The zero is a reading rather than a broken command — the same pass with the control wordobjectuireturns 115. Scope stated plainly: one page per state, so this is not a whole-backlog enumeration./search/*is refused by the egress proxy by design, so no semantic search was used and none is claimed.🤖 Generated with Claude Code
https://claude.ai/code/session_01611D6ZaRaMmwTNQmSbk8MH
Generated by Claude Code