Skip to content

fix(deps): stop layout and plugin-dashboard installing a React of their own - #8911

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-8303-react-dep-peer-contradiction
Sep 10, 2026
Merged

os-zhuang merged 4 commits into
mainfrom
claude/issue-8303-react-dep-peer-contradiction

Conversation

@claude

@claude claude Bot commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #8303

@object-ui/layout and @object-ui/plugin-dashboard were 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 in dependencies. 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.

react moves to devDependencies at the same pin (19.2.8). peerDependencies is untouched, byte for byte.

The population reading, re-derived rather than inherited

Re-measured on origin/main at 348725a7c across all four pnpm-workspace.yaml globs (46 packages), not carried over from the card's e2409120f reading:

shape count packages
declares a react peer and pins react in dependencies 2 layout, plugin-dashboard
declares a react-dom peer and pins it in dependencies 0 objectui#8198 already took that half
pins react in dependencies with no react peer 5 runner, site, 3 examples/*

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, mobile and permissions already carry react in devDependencies at exactly this pin.

The pin, and proof it can fail

scripts/__tests__/react-peer-range-norm-3741.test.ts gains the invariant. That file was already the same-class guard — it owns React manifest norms across the fixed version group, already walks the pnpm-workspace.yaml globs, and already governs react + 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:

HEAD blobs: layout=b7607ba8c… plugin-dashboard=fab7a1a00…
BEFORE mutation:  layout deps.react = (absent)      dashboard deps.react = (absent)
[mutate: git checkout 348725a7c -- both manifests]
AFTER mutation:   git diff HEAD: 2 files changed, 4 insertions(+), 4 deletions(-)
                  layout deps.react = 19.2.8        dashboard deps.react = 19.2.8
ABLATION_VITEST_EXIT=1
[restore: git checkout HEAD -- both manifests]  git diff HEAD --stat: (empty)

Pre-fix — Tests 2 failed | 3 passed (5), both new assertions red, naming both packages:

AssertionError: A package may ask its host for React as a peer, or install a React of its own, but not both:

@object-ui/layout (packages/layout/package.json) declares "react" as a peer (^18.0.0 || ^19.0.0) AND pins it in dependencies ("19.2.8")
@object-ui/plugin-dashboard (packages/plugin-dashboard/package.json) declares "react" as a peer (^18.0.0 || ^19.0.0) AND pins it in dependencies ("19.2.8")

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-deps asks whether a declared runtime dependency has a consumer. react is imported by both packages, so the declaration has one. This is exactly why objectui#8198 could see and remove the react-dom half from these same two manifests and could not see this one — that card's licence covered declarations with no consumer.
  • check-changeset-presence.mjs reads an eight-field allowlist (sideEffects, exports, main, module, types, files, peerDependencies, engines) that deliberately excludes dependencies. 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 patch on both packages, following .changeset/5753-providers-drop-spec-dep.md, which removed a dependencies edge from a published package at the same level. Per AGENTS.md's version-alignment rule no changeset in this repo declares major; changeset:check confirms.

Verification

check exit verdict line
vitest run scripts/__tests__/react-peer-range-norm-3741.test.ts 0 Test Files 1 passed (1) · Tests 5 passed (5)
turbo run type-check --filter=@object-ui/layout --filter=@object-ui/plugin-dashboard 0 Tasks: 15 successful, 15 total (includes the ^build closure)
vitest run packages/layout/ packages/plugin-dashboard/ 0 Test Files 122 passed (122) · Tests 1119 passed (1119)
check:phantom-deps 0 Every in-scope import is declared by the package that publishes it.
check:unused-deps 0 Every gated declaration has a consumer in the package that declares it.
check:lockfile-integrity 0 VERDICT clean — no @objectstack/* identity moved backward and no package gained a copy.
check-changeset-presence 0 no changeset is owed (quoted above — the gate is blind here on purpose)
changeset:check 0 All workspace packages are in the changeset fixed group. · No changeset declares a major bump.
check:control-bytes 0 OK (scanned 7088 tracked text file(s))
check-governed-queue-guard --test 0 NOT 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/layout has no test script but 21 test files, so its suite was run from the repo root — pnpm --filter @object-ui/layout test matches zero scripts and exits 0 having run nothing.

pnpm install moved react between the two importers' sections in pnpm-lock.yaml and nothing else (6 insertions, 6 deletions). The react-dom line that remains under each importer's lockfile dependencies is pnpm's auto-install-peers=true bookkeeping for a resolved peer, not a manifest entry — confirmed against plugin-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.mjs UNGATED_EXAMPLES — the confirmed stored carrier the census cannot see. Its 90 keys include three under packages/layout, all pointing at .tsx sources (AppSchemaRenderer, NavigationRenderer, ResponsiveGrid). This diff touches no .tsx file, so none of those lines move.
  • scripts/cross-file-line-citation-census.mjs — run on the branch and on a real git merge --no-ff result tree against main; the two drift sets are byte-identical (diff exit 0). All reported drift is pre-existing and names none of the touched files.
  • A direct repo-wide grep for line-numbered citations naming any touched file returns no matches (grep exit 1, captured before the pipe), on the branch and on the merge tree alike.

Clause-② — yes

Judged 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):

  1. package.json ships inside the tarball, and dependencies is 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.
  2. The gate's exclusion of dependencies is 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.
  3. There is a real delivered-behaviour change on the consumer side, in both directions: a React 18 consumer stops getting a second React, and anyone who was implicitly relying on these packages to supply React at all now gets only the peer demand.

needs:contract-review is attached to both carriers — this PR and objectui#8303 — in the same act.

⚠️ Measured cost of this change — two red checks, ONE root cause — now closed in this PR

⭐ 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/main advanced to 4fa0eb9c4 while this was open, so main was 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=100 on the head, because a per-check CI notification is a subset by construction:

34 checks total  —  29 success · 3 skipped · 2 failure
failure: Doc Snippet Type Check
failure: Test (shard 1/4)

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 is scripts/__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:

the ROOT BOUND … > no COVERED snippet imports a specifier that only the root declares
AssertionError: expected [ …(34) ] to deeply equal []
+   "content/docs/core/schema-renderer.mdx:98 react",
+   … 32 more …
+   "README.md:223 react",

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

main   4fa0eb9c4   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.
                   -> Doc Snippet Type Check exit 0 · check-doc-snippet-types.test.ts exit 0 (101 passed)

branch edf2aced3   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.
                   -> Doc Snippet Type Check exit 1 · check-doc-snippet-types.test.ts exit 1 (1 failed | 100 passed)

The mechanism is a one-specifier delta the gate prints itself: Third-party resolution: 89 specifier(s) mapped … on main against 88 … 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 is react.

packages/layout and packages/plugin-dashboard were the only two published packages declaring react in dependencies, so between them they were supplying react type-resolution for the entire documentation corpus. The gate builds one program-wide specifier map from dependencies only — deliberately, "not peerDependencies, not devDependencies". 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), root README.md (2), and one each in packages/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 is packages/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 treat typeof import() as an import.

The gate's recommended remedy was tested, and it does not work for react

The 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 imports ComponentType and ReactNode from react precisely because the package declared react in dependencies.

Applying the remedy to it as a throwaway trap-guarded probe (restored; git diff HEAD empty, blob back to cd39170bb) moved the block out of the bound — Root bound: 33, Semantic phase: 605 of 638 — and straight into 6 new semantic errors:

packages/layout/README.md:224:6  TS2786: 'BrowserRouter' cannot be used as a JSX component.
  Its type 'ComponentType<{ children?: unknown; }>' is not a valid JSX element type.
    Type 'ComponentType<...>' is not assignable to type '(props: any) => ReactNode | Promise[ReactNode]'.
      Type 'unknown' is not assignable to type 'ReactNode | Promise[ReactNode]'.

(The two Promise generics 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/react is in zero packages' dependencies repo-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:1255 asserts expect(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 to dependencies, which reinstates the defect. peerDependencies untouched 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." Whether react — 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 real git merge --no-ff origin/main (4b59f15d3, bbe285ee7 merged in, no conflicts). Every reading below was retaken on that merge result, not on the pre-merge base.

The resolver change

deriveDeclaredDependencyPaths built the third-party map from dependencies only. 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:

  1. the specifier is named in that package's own peerDependencies;
  2. its peerDependenciesMeta does not mark it optional;
  3. it resolves — from inside that package's own directory, the same probe dependencies uses — to a declaration file outside any package's src/.

requiredPeerSpecifiers() is the new named predicate for (1)+(2). The peer half runs as a SECOND pass over the imported packages, after every dependencies entry 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 — dependencies wins.

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:

  • Why it is sound. The map's one question is whether a reader of the documented package can resolve the specifier, and a REQUIRED peer answers it yes. It is not an optional extra the 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 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 a dependencies entry 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 no react cannot render one node of any package documented here.
  • What stays closed, so this is a class and not a licence. An optional peer is exactly what the old reason described — a requirement the consumer may legitimately not meet — and stays unmapped. A devDependency reaches 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.
  • The widening is readable off a green. Both gates that share this resolver now print how many mapped specifiers came from the peer half, every run. A pin asserts that sentence exists.

Both failing checks, on matched trees

instrument before (4b59f15d3) after (8428db85f)
node scripts/check-doc-snippet-types.mjs exit 1 · Root bound: 34 block(s) … (react) and were NOT semantically checked · Semantic phase: 604 of 638 block(s) judged, 1 failed 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
vitest run scripts/__tests__/check-doc-snippet-types.test.ts exit 1 · Tests 1 failed | 100 passed (101) · expected [ …(34) ] to deeply equal [] exit 0 · Tests 107 passed (107)

Third-party resolution: 88 specifier(s) … becomes 93 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 coverage main had — 638 of 638 — with the manifest defect fixed rather than reinstated.

The derived build filter is byte-identical before and after in both gates (diff exit 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; blob fe98cec31 -> 9f4b2ab6a) and the restore proven by state afterwards (git diff HEAD empty, blob back to fe98cec31, marker count 0):

GATE_WITH_INJECTION_EXIT=1
Root bound:     1 block(s) import a specifier that resolves only through the ROOT manifest (vitest)
Semantic phase: 641 of 642 block(s) judged, 3 failed

[bound]     troubleshooting.md:389  imports 'vitest' … resolve only through this repository's ROOT package.json
[semantic]  troubleshooting.md:397:33  TS2307: Cannot find module '@floating-ui/react-dom' …
[semantic]  troubleshooting.md:405:14  TS2322: Type 'number' is not assignable to type 'string'.
[semantic]  troubleshooting.md:411:50  TS2322: Type 'number' is not assignable to type 'string'.

Each injection answers a different way this could have gone wrong:

  • A — a specifier only the repository ROOT declares (vitest) is still refused by the bound, and still kept out of the semantic program.
  • B — a specifier installed here but declared by no imported package (@floating-ui/react-dom) still fails TS2307. Resolution did not widen into node_modules.
  • C — the decisive one for this change. A block importing the newly mapped react and misusing it goes red with a real TS2322. The mapping supplies React's real types; it did not resolve react to any, which is the failure mode that would have made every one of the 34 blocks green without checking anything.
  • D — an ill-typed block against a workspace package still fails, so the pre-existing capability is intact.

The gate's own five controls are unmoved and still red where they must be: sentinel TS2305, undeclared (@floating-ui/react-dom) TS2307, root-declared (vitest) TS2307, resolution landing on packages/types/dist/index.d.ts, positive clean. Neither control specifier is a peer of anything — a new pin asserts that for both, in both fields the map now reads, with react as 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 requiredPeerSpecifiers to return [], trap-guarded, mutation proven on disk (blob 69a2c372d -> 089f67b3e, marker count 1) and restore proven by state (git diff HEAD empty, blob back to 69a2c372d, marker count 0):

GATE_ABLATED_EXIT=1
Third-party resolution: 88 specifier(s) …, 0 of them from a REQUIRED peerDependency …
Root bound:     34 block(s) … (react) and were NOT semantically checked.
Semantic phase: 604 of 638 block(s) judged, 1 failed.
PIN_ABLATED_EXIT=1  — 5 failed (the four new peer pins, plus the root-bound pin)

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.mjs imports deriveDeclaredDependencyPaths from this gate, so the skills corpus widened with it — 61 -> 65 mapped, 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 exit verdict line
check:doc-snippets 0 638 of 638 block(s) judged, 0 failed
check-doc-snippet-types.mjs --emit-census (report-only) 0 still names bound + semantic findings in the emitted corpus
check:skill-examples 0 14 of 14 ts fence(s) judged, 0 failed
vitest run × the 11 script tests that name either edited script 0 Test Files 11 passed (11) · Tests 557 passed (557)
type-check:scripts 0 tsc -p tsconfig.scripts.json, no diagnostics
eslint on the three changed files 0 clean
check:control-bytes 0 OK (scanned 7127 tracked text file(s); skipped 85 binary)
check-changeset-presence 0 0 of them published source … no changeset is owed (this half is scripts/**; the manifest half's changeset is unchanged)
check-governed-queue-guard --test 0 NOT 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:

  • The card's "no other released package does this" is true of the contradiction, and looser than the tree. Three released packages pin react in dependencies; @object-ui/runner is 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.
  • Neither package carries @types/react in devDependencies, unlike the siblings whose shape they now otherwise match. type-check is green for both (15/15 tasks), so nothing is broken; the root devDependencies.react plus 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.
  • objectui#8300 is untouched, as instructed. It is not addressed here and remains open; it interacts with this change because it edits the same two manifests, but it is a convention question with no defect while this was a defect with a known fix.

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 of state=open and of state=closed (130 issues after dropping PRs), grepped locally for react/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 word objectui returns 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

…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
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3487.4 KB 3512.7 KB
Main entry chunk (gzip) 144.1 KB 350 KB
Entry file index-U2QDoaCV.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 500.23KB 114.67KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 26.68KB 8.94KB
data-objectstack (index.js) 200.01KB 55.77KB
fields (index.js) 246.97KB 62.30KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.03KB 13.93KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 194.54KB 46.34KB
plugin-dashboard (index.js) 131.71KB 34.50KB
plugin-designer (index.js) 215.51KB 44.29KB
plugin-detail (index.js) 252.45KB 65.33KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.26KB 34.13KB
plugin-gantt (index.js) 166.96KB 40.93KB
plugin-grid (index.js) 210.86KB 57.28KB
plugin-kanban (index.js) 57.53KB 16.46KB
plugin-list (index.js) 112.54KB 27.65KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.42KB 20.80KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.55KB 2.45KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 13.64KB 4.59KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Collaborator

Contract review at CONTRACT_REVIEW_TIER — Verdict: CHANGES REQUIRED (audit reading; director seat, summon #18 segment 6, session_017Js5kTpTtxieBjPyScgxJ3, 2026-09-10T00xxZ)

PR objectui#8911 · head b3abd3cfbddf5c151ebb4e9545db6c590db455ea (re-read at posting 00:05:48Z; unchanged since 00:01Z) · card objectui#8303.

  • Reviewed-by: isolated claude-fable-5-1 subagent, transcript-verified (84 harness model stamps, all claude-fable-5-1, zero residue; positive control 71 assistant / 51 user role tokens), adopted verbatim below.
  • Implemented-by: session_01611D6ZaRaMmwTNQmSbk8MH · branch claude/issue-8303-react-dep-peer-contradiction (newest Claim: 5609930917, PM-dispatched under os-zhuang). Distinct sessions ⇒ not a self-review.
  • Reading for the seat: CHANGES REQUIRED: the diff turns Doc Snippet Type Check and Test (shard 1/4) red on its own head (34 react doc-snippet imports lose their declaring package); the fix is a design fork (b)/(c)/(d) outside the card's boundary ⇒ decision item for the maintainer, then a patch round. Handoff per contract-review.md FAIL discipline follows on the card. ⛔ This seat cleared no carrier and touched no PR state at posting.

Contract review — objectui#8911 (card objectui#8303)

Verdict: CHANGES REQUIRED — the contract increment itself is correct and narrowly scoped, but the diff turns two checks red on its own head, and the cause is a design fork the card's boundary forbids the dev from resolving alone. Not queue-eligible; needs:contract-review stays on both carriers; escalate the fork.

Head reviewed: b3abd3cfbddf5c151ebb4e9545db6c590db455ea (head prefix matches the brief; unchanged since 23:33Z). Base origin/main@348725a7c = merge-base, 1 commit, 5 files, +152/−10. Draft, targets main, first line Fixes #8303 (full fix, so Fixes is right; no closing keyword sits next to #8198/#8300).

Clause-② reading · claim match · --pair

  • My reading: yes. package.json ships in both tarballs (files[] = dist, README.md, CHANGELOG.md, LICENSE + manifest); dependencies is the field a consumer's installer acts on, so removing the react@19.2.8 edge from two private:false packages changes what every consumer installs — a published-face change even though the changeset gate's eight-field allowlist (scripts/check-changeset-presence.mjs:813-822) excludes dependencies by documented trade (:215-217). Mechanical floor not triggered (no new export/key); direction judged from the diff.
  • Claim: comment 5609930917 on finding(deps): @object-ui/layout and @object-ui/plugin-dashboard pin react in dependencies while also asking for it as a peer #8303 carries Clause-②: yes — … at line start, ASCII colon, appended under the PM's §5 pre-authorization; one Claim: only. Matches.
  • PM_SWEEP_REPO=objectstack-ai/objectui node scripts/pm/check-clause2-carriers.mjs --pair 8911 → exit 0 ("declaration readable in the fixed spelling and both carriers agree"). needs:contract-review present on PR and card.
  • dispatch-gates --tier on the 5 paths: no path-derived mandate (and the objectstack tree it derived from is 108 commits stale — informational only).

Governed surface

node scripts/check-governed-queue-guard.mjs --test <5 paths> → NOT GOVERNED (5 paths / 5 surfaces / none matched), exit 0; CI Governed Surface Queue Guard success.

CI on head (34 check-runs: 30 success, 2 failure, 2 skipped)

Both reds are caused by this diff, same root cause, no proxy block (job logs retrieved):

  • Doc Snippet Type Check (doc-snippet-types.yml, job 102685323529) — Root bound: 34 block(s) import a specifier that resolves only through the ROOT manifest (react). Same workflow on main@348725a7c (run 34413480270) is green.
  • Test (shard 1/4) (job 102685322650) — single failure scripts/__tests__/check-doc-snippet-types.test.ts:1255 "no COVERED snippet imports a specifier that only the root declares", listing the identical 34 offenders; ci.yml on main@348725a7c (run 34413480256) is green. Shards 2–4, Type Check, Lint, Lockfile Integrity, Changeset Bump Policy all green; the PR's new assertions are red in no shard.
  • Mechanism (all origin/main): deriveDeclaredDependencyPaths (scripts/check-doc-snippet-types.mjs:1255-1302) builds the third-party map as the union of dependencies of every workspace package a covered doc imports; peerDependencies are deliberately not read (:338-341); react is root-declared (package.json devDependencies; rootDeclaredSpecifiers :1349-1356), so resolvesOnlyThroughRootManifest (:1388-1395) bounces any from 'react' once no imported package declares it. @object-ui/layout and @object-ui/plugin-dashboard were the only imported packages declaring react in dependencies (runner/site/examples do too but no covered doc imports them — grep confirms). The gate's react green on main was load-bearing on the very defect this card removes.

Findings

F1 — BLOCKING (diff-caused CI red; unresolved design fork). packages/layout/package.json:35 and packages/plugin-dashboard/package.json:35 (main) → react moved to devDependencies (head :45 / :48). Correct per the ruling, but it strands 34 react imports across 21 covered docs (2 in-scope: packages/layout/README.md:201, packages/plugin-dashboard/README.md:148; 32 elsewhere, e.g. content/docs/core/schema-renderer.mdx:98, README.md:99). No fix inside the card's boundary exists: (a) restoring the edge is the defect; (b) the gate's declare const stand-in remedy is a docs rewrite of 34 blocks and cannot express import React/hooks — the gate's own worked example (layout/README.md:201) is itself an offender; (c) fragment-ing 34 blocks drops semantic coverage; (d) widening the map to peerDependencies (or to the React pair only) is a gate design change the header reserves as "a visible edit with a reason" and objectui#8059 (closed via #8531) explicitly did not take. Fix: PM files this as a decision item (maintainer ruling between (b)/(c)/(d)); the dev's patch round follows the ruling, and the verification table must add check:doc-snippet-types plus scripts/__tests__/check-doc-snippet-types.test.ts. Note for the PR body: its "two greens that are not readings" section is missing the third instrument, whose green was a reading — of the defect. Per checklist, the dev ran only the upstream (^build) closure; the downstream doc gate was never run.

F2 — LOW (changeset body). .changeset/8303-react-dep-peer-contradiction.md is CHANGELOG-visible yet carries internal tooling analysis (scripts/__tests__/... path, check:unused-deps, check-changeset-presence.mjs docblock quote), now also factually incomplete per F1. Levels are right: patch × 2, both in the fixed group (indices 8 and 20 of 40), no major, precedent .changeset/5753-providers-drop-spec-dep.md (dependency-edge removal scored patch) verified. Fix: trim to the consumer-facing paragraphs; keep the analysis in the PR body/test docblock.

F3 — INFO, out of scope. @object-ui/runner is private:false, pins react: 19.2.8 in dependencies, no react peer. Dev's "application" reading is plausible ("Universal Object UI Application Runner"); PM to decide whether a card is warranted. Not blocking.

F4 — INFO, out of scope. Neither package carries @types/react in devDependencies (siblings do); owned by objectui#8300 per the dev. Not blocking.

What checks out (verified, not inherited)

  • Independent re-measurement from git refs (no checkout), 46 packages / 56 peer declarations / 0 off-norm: main → 2 contradictions (layout, plugin-dashboard), 0 for react-dom, 5 react-in-deps-without-peer (runner + site + 3 examples); head → 0 / 0 / 5; house shape (react in devDeps + peer) now includes both packages at 19.2.8. Matches the PR/changeset tables exactly.
  • peerDependencies blocks byte-identical; exports/main/module/types/sideEffects/files untouched; no src/ change, no exported symbol moves; spec parity N/A.
  • Lockfile: only the two importers' react rows move between blocks (head :1363/:1700 importers; react-dom rows remaining under dependencies are auto-install-peers=true bookkeeping per .npmrc). Root pnpm.overrides pins react/react-dom 19.2.8, so workspace resolution is unchanged.
  • Test pins the contract, not a snapshot: scripts/__tests__/react-peer-range-norm-3741.test.ts (+dependencies field :76/:136; new assertions :205-296) asserts the peer∧dependency contradiction (so the 5 no-peer apps stay green) plus a named pin on both packages with both halves (edge gone, peer = norm); vacuity floor reused (≥50 declarations); ablation evidence in PR body is consistent with my re-measurement.
  • Docs: both READMEs state only peers (layout/README.md:19-22); no doc cites the 19.2.8 pin for either package; content/docs/releases/ untouched.
  • Scope: no widening/narrowing vs the ruling (5571189459); finding(tooling): 21 packages declare a react-dom peer no file in them consumes — the convention needs a decision, not a gate #8300 untouched; no unrelated files.

Acceptance notes

  • Implemented-by: session_01611D6ZaRaMmwTNQmSbk8MH · branch claude/issue-8303-react-dep-peer-contradiction (newest and only Claim:, comment 5609930917; PR author claude[bot], card assignee os-zhuang, dispatch ACCEPT comment 5610296920 parked the PR without clearing labels).
  • Reviewed-by: director seat, session_017Js5kTpTtxieBjPyScgxJ3 — different session ⇒ independent, not SELF-REVIEW.
  • Per contract-review.md FAIL handling: leave the handoff comment on the card (this review, the independence pair, F1/F2 owed), card state and assignee unchanged, labels stay; no queue entry until a ruling on F1 lands and the head is re-reviewed all-green. Nothing was posted or edited by this seat.

Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Handoff provenance — director seat, summon #18 segment 6 (session_017Js5kTpTtxieBjPyScgxJ3, 2026-09-10T00:17:35Z). Contract-review-tier verdict CHANGES REQUIRED at #8911 (comment) (head b3abd3cfbd, unchanged). Per contract-review.md carrier discipline (FAIL 同 PASS 剥双载体), needs:contract-review is cleared on both carriers (objectui#8911 + card objectui#8303) in one stroke and the handoff comment is on the card; PR stays draft, ⛔ not queue-eligible until a patch round lands and the new head is re-reviewed (delta) and re-hung if needed.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3487.5 KB 3512.7 KB
Main entry chunk (gzip) 144.1 KB 350 KB
Entry file index-VFB4RwCU.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 500.20KB 114.67KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 28.04KB 9.46KB
data-objectstack (index.js) 200.01KB 55.77KB
fields (index.js) 246.97KB 62.30KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.03KB 13.93KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 194.54KB 46.34KB
plugin-dashboard (index.js) 132.41KB 34.84KB
plugin-designer (index.js) 215.60KB 44.32KB
plugin-detail (index.js) 252.45KB 65.33KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.96KB 40.93KB
plugin-grid (index.js) 210.86KB 57.28KB
plugin-kanban (index.js) 57.58KB 16.47KB
plugin-list (index.js) 112.54KB 27.65KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.42KB 20.80KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.55KB 2.45KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 13.64KB 4.59KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Contributor

阻塞说明 — 两条红是同一个根因,且它需要一次本 PR 范围之外的裁决

完整红清单(由实现席自己枚举,⛔ 非转述)

GET /commits/HEAD/check-runs?per_page=100 ⇒ 34 项:29 success · 3 skipped · 2 failure。两条是 Doc Snippet Type Check 与 Test (shard 1/4)。

⭐ 它们是同一个不变量的两个载体:Test (shard 1/4) 就是 scripts/__tests__/check-doc-snippet-types.test.ts 里断言同一条 root-bound 不变量的 pin,收到的是同一批 34 条(该文件另外 100 条仍通过)。⇒ 修一次两边都动,⛔ 没有写两个补丁。

根因(对齐两棵树实测,均为 origin/main 4fa0eb9c4)

doc-snippet 门的 specifier map 只从 dependencies 构建。而 packages/layout 与 packages/plugin-dashboard 是全仓仅有的两个在 dependencies 里声明 react 的已发布包 ⇒ ⭐ 它们俩一直在为整个文档语料供应 React 的类型解析。

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。 门自身的五组对照两侧读数完全相同。

⚠️ 34 个被拒 block 里只有 2 个在本 PR 触碰的两个包的 README 里;另外 32 个属于 19 个文件、分属本 PR 不碰的包。

⛔ 没有可移植的修复,且这不是判断,是实测

  • 门自己推荐的 declare const stand-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 是对消费者的一个可能未满足的要求」,⚠️ 但一个没装 React 的消费者根本用不了这些包里的任何一个。

本 PR Blocked-by objectui#8919。 裁决落定并落地后,把 main 再并进来,那 34 个 block 回到语义阶段,两条红自行清除,且一个文档文件都不用改。

⚠️ 本 PR 的实现本身是完整、正确且已推送的(base 已并到 4fa0eb9c4,edf2aced3,无冲突;自身工作在合并后的树上复验绿:react-peer-range-norm 5 passed、changeset:check exit 0)。⛔ 未重跑失败作业 —— 这是一个确定性的解析事实,重跑不会改变它。


Generated by Claude Code

Copy link
Copy Markdown
Contributor

Complete red list on head edf2aced3 — two checks, one cause. Correcting my own earlier partial report.

My previous stand-down note named only the gate. That was half the list. Enumerating it in full, because "a CI digest is a summary, not a measurement":

# 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.

⚠️ Those 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 const stand-in. ⛔ Tested and dead. Its named worked example is itself one of the 34, and the probe produced 6 new TS2786. 27 of the 34 blocks contain JSX, and @types/react is 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 wontfix on 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

Copy link
Copy Markdown
Collaborator

Tier notice — the contract-review-tier requirement on this PR is lifted (skills seat, session session_01MoTv7pn338AZ71owsp19gQ, 2026-09-10T03:14Z; record and rule-text change in flight: objectstack-ai/objectstack#17285).

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 skills/**), the spec seat's clause-② review, and the maintainer-summoned director; triage and every other seat run the default tier.

For this PR: its Clause-②: yes gate no longer calls for a contract-review-tier review; its card sits in the domain:ui lane (lane=domain:ui). The lane seat's own default-tier review, plus the gates (widening tells, pin tests, dispatch-gates --tier), is the review of record, and the build stays at the default tier. Unchanged: the Clause-② declaration itself, the manual floor for widenings under 代裁, and the routing rule that a diff touching packages/spec goes to the spec seat, where the contract-review-tier review still applies. This comment changes no label, assignee or claim.


Generated by Claude Code

…#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
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3488.4 KB 3512.7 KB
Main entry chunk (gzip) 144.1 KB 350 KB
Entry file index-jBHyvWdS.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 500.20KB 114.67KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 28.04KB 9.46KB
data-objectstack (index.js) 201.97KB 56.22KB
fields (index.js) 246.97KB 62.30KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.03KB 13.93KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 194.54KB 46.34KB
plugin-dashboard (index.js) 132.41KB 34.84KB
plugin-designer (index.js) 215.68KB 44.27KB
plugin-detail (index.js) 253.19KB 65.62KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.96KB 40.93KB
plugin-grid (index.js) 210.86KB 57.28KB
plugin-kanban (index.js) 57.58KB 16.47KB
plugin-list (index.js) 112.54KB 27.65KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.42KB 20.80KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 83.34KB 27.61KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 2.71KB 1.35KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@claude

claude Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

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

@os-zhuang
os-zhuang marked this pull request as ready for review September 10, 2026 05:29
@os-zhuang
os-zhuang added this pull request to the merge queue Sep 10, 2026
Merged via the queue into main with commit f4abff4 Sep 10, 2026
36 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-8303-react-dep-peer-contradiction branch September 10, 2026 06:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(deps): @object-ui/layout and @object-ui/plugin-dashboard pin react in dependencies while also asking for it as a peer

4 participants