Repository navigation
chore(deps): run pnpm dedupe --lockfile-only on an untouched main — the measurement (objectui#9215) - #9316
Conversation
`pnpm dedupe --lockfile-only` on `d7f0601ee7` with nothing else edited. Pure collapse of pre-existing duplicate resolutions: 1822 -> 1773 snapshot keys, 1814 -> 1767 identities, 47 removed and 0 added. Every dropped copy has a strictly higher surviving sibling of the same name, so no consumer moves backward. Zero declarations, zero ranges and zero overrides move: the lockfile's `overrides`, `settings` and `patchedDependencies` sections are byte-identical, and 0 of the `importers:` `specifier:` lines change (114 `version:` lines do). No `package.json` and no `pnpm-workspace.yaml` is touched. The measurement, the identity-sensitive collapses and the `Bundle Analysis` delta are reported on the pull request. Lockfile only; empty-frontmatter changeset declares no release. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
ACCEPT the measurement — and ⛔ LEG 3 FIRED, so the merge is the maintainer's ruling, not this seat's. PR stays DRAFT.
Verified by this seat against the diff, ⛔ not taken from the report
⭐ Every figure the card predicted reproduced exactly on today's tip, with nothing to explain — and the committed lockfile's sha256 is still ⭐ The control ran both directions in the same run: ⛔⛔ LEG 3 FIRES — all eight identity-sensitive packages are hit
⇒ per triage's ruling — 「⇒ that reading is what turns this into a maintainer decision, and it belongs on the PR with numbers attached, ⛔ not asked in advance without them」 — this seat ⛔ does not flip ready, ⛔ does not enqueue, ⛔ does not arm auto-merge, and ⛔ does not rule the merge. Routed to the maintainer with the numbers attached, which is exactly the shape triage asked for. ⭐ The card's question 1 is ANSWERED: accretion, not intent⛔ No declaration, range or override selects any dropped copy — verified structurally:
|
|
| Step | Outcome |
|---|---|
| Build packages | success |
| Check console performance budget | failure |
Which half objected:
| Eager-closure half | Verdict |
|---|---|
| Aggregate closure ceiling | ✅ pass |
| Per-chunk ceilings | ✅ pass |
| Ceiling sensitivity (headroom) | |
| Ceiling freshness (checkout vs. base branch) | ✅ pass |
⚠️ A broken gauge half is a verdict about the ceiling, not about the bundle: that line has drifted out of range of the regression it exists to catch, or the report behind it cannot be trusted. It does not say anything grew. TheCheck console performance budgetstep log carries the ceiling and the number it was compared against.
Reason: The entry chunk measured 144.3 KB, but the eager-closure half of this gate returned no trustworthy VERDICT: the report could not be read, a ceiling has drifted out of range of the regression it must catch, or (objectui#6245) a ceiling was replaced on the base branch after this checkout was made. The step log says which. This is not a passing budget — and it is not a size regression either.
See the workflow run for details.
📦 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) | 502.04KB | 115.16KB |
| core (index.js) | 8.52KB | 3.41KB |
| create-plugin (index.js) | 27.94KB | 9.51KB |
| data-objectstack (index.js) | 211.58KB | 58.68KB |
| fields (index.js) | 247.88KB | 62.51KB |
| 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) | 8.87KB | 3.64KB |
| i18n (index.js) | 5.21KB | 2.26KB |
| i18n (pickLocalized.js) | 7.62KB | 3.26KB |
| i18n (provider.js) | 32.15KB | 10.49KB |
| 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.83KB | 10.95KB |
| 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.02KB | 13.93KB |
| plugin-charts (index.js) | 71.51KB | 19.97KB |
| plugin-chatbot (index.js) | 195.34KB | 46.51KB |
| plugin-dashboard (index.js) | 131.22KB | 34.59KB |
| plugin-designer (index.js) | 215.94KB | 44.33KB |
| plugin-detail (index.js) | 253.48KB | 65.86KB |
| plugin-editor (index.js) | 2.23KB | 1.05KB |
| plugin-form (index.js) | 136.77KB | 34.17KB |
| plugin-gantt (index.js) | 166.94KB | 41.03KB |
| plugin-grid (index.js) | 211.59KB | 57.48KB |
| plugin-kanban (index.js) | 45.99KB | 14.29KB |
| plugin-list (index.js) | 112.58KB | 27.65KB |
| plugin-map (index.js) | 20.43KB | 6.81KB |
| plugin-markdown (index.js) | 13.88KB | 4.80KB |
| plugin-report (index.js) | 43.41KB | 11.93KB |
| plugin-timeline (index.js) | 30.07KB | 8.74KB |
| plugin-tree (index.js) | 9.55KB | 3.32KB |
| plugin-view (index.js) | 84.42KB | 20.79KB |
| 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) | 94.03KB | 31.02KB |
| react (data-invalidation.js) | 5.05KB | 2.08KB |
| react (index.js) | 4.63KB | 2.18KB |
| react (schema-input.js) | 4.25KB | 2.04KB |
| 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) | 3.89KB | 1.87KB |
| sdui-parser (parse.js) | 25.28KB | 7.80KB |
| 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
Brings the branch up to main at 21d34d5 (294 commits since the merge base 078f2e4). One textual conflict. packages/plugin-kanban/package.json Union. main deleted `@tanstack/react-virtual` from `dependencies` along with the module that used it (objectui#8932); this branch bumps `lucide-react`. Both kept. pnpm-lock.yaml Regenerated by pnpm from main's lockfile, not hand-merged: main's file taken whole, then `pnpm install --lockfile-only` with the 22 manifests briefly at an exact `1.43.0` and again with the caret restored, which pnpm keeps. A plain re-lock follows `^1.43.0` to the newest 1.x (1.48.0 today), outside what this branch measures. The delta against main is lucide-react alone: 22 importer specifiers `^1.31.0` to `^1.43.0`, 22 resolutions and the fumadocs peer suffixes 1.35.0 to 1.43.0. Note main's resolution was already 1.35.0, not 1.31.0, since the lockfile dedupe of #9316; the declared range is what said 1.31.0. `node scripts/check-lockfile-integrity.mjs --base <main's lockfile> --head pnpm-lock.yaml` reads `VERDICT clean` (1773 resolutions on both sides). Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude <noreply@anthropic.com>
…s tree The entry was written before objectui#9251 landed and before main's lockfile moved to 1.35.0. Three of its claims stopped being true: the double lucide install it credited to this bump was already collapsed by the #9316 dedupe; its class note called the per-alias classes purely additive, when 1.43.0 also drops the key-derived class on component imports; and it said nothing about string-resolved icons, which under ruling C keep the seam's classes and do not get lucide's alias classes. It now declares every released package whose `lucide-react` range moves (20 in `dependencies`, the console in `devDependencies`) plus `cli` and `types`, whose source changed, all `patch`. It names the invisible-glyph repair (`__iconData`) as a user-visible fix, and dates the `Trash2` class example in the pending objectui#9251 entry, without editing that entry. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude <noreply@anthropic.com>
…urce` expression root (objectstack-ai#9378) Fixes objectstack-ai#9370 ⛔ **GOVERNED SURFACE** (`skills/**`, `GOVERNED_SURFACES` `skills-catalog`). This PR parks as a **draft by design** and needs an APPROVED review from an authorized approver (`os-zhuang`, `hotlong`). ⛔ Not flipped ready, not enqueued, no auto-merge. The precedent on this exact surface is objectstack-ai#9352 / card objectstack-ai#9311, merged as `28be0786d`.⚠️ **Merge after objectstack-ai#9369, and this PR states which world it measured in.** Re-derived from my own tree at base `69aa9c017`, not assumed: `SchemaRenderer.tsx` still carries the `data: dataSource` binding, and `SchemaRendererContext.tsx` still reads `const dataSource = context?.dataSource` inside `useDataScope`. **Both halves are still LIVE on this base** — objectstack-ai#9369 is open, not merged. So these pages become true the moment objectstack-ai#9369 lands and are ahead of the code until then. Every evaluator measurement below is from that same pre-objectstack-ai#9369 tree, which is the right place to take it: the evaluator itself is what objectstack-ai#9369 stops feeding, and the evaluator's own behaviour does not move. Sequencing is a maintainer decision, not something this PR can enforce. ## What moved The maintainer ruled objectui#9308 option B on 2026-09-13: `SchemaRenderer` stops publishing the injected `DataSource` adapter as the expression root `data` (b1), and `useDataScope` — what a node's `bind` resolves through — reads the ambient scope a host publishes via `PredicateScopeProvider` instead of walking that adapter (b2). The published skill package still taught both halves. Three files, matching the shape objectstack-ai#9369 used for `content/docs/guide/schema-rendering.md` and `packages/react/README.md`: publish the host values under real names through `PredicateScopeProvider`, read them by those names, and state that `dataSource` is the **adapter** and not a root. | file | what changed | |---|---| | `guides/schema-expressions.md` | the scope table, a new COMPILED wiring example, the migration callout, the `bind` resolution prose, the bound-list comment, debugging-checklist item 3 | | `guides/data-integration.md` | the architecture diagram's channel split, "Static data" rewritten onto the scope channel as a COMPILED example, the `useDataScope` resolution sentence, the `data` root claim | | `rules/protocol.md` | both measured tables' WIRING restated, the `bind` comment and nested-path sentence, plus a callout carrying the retirement and pointing at the verdict flip | ⛔ Untouched, as the card requires: the `object-*` reader-list paragraph and the `data-table` `bind` pothole (objectui#6575 still rules `data-table` does not read `bind`). ## ⭐ The part that is not a renaming A migration note that only swaps provider names is wrong here, because the **verdict moved**. Measured by me on the built evaluator (`packages/core/dist`), with firing controls — ⛔ and the `true` on a missing root belongs to the **predicate layer**, not to the evaluator: | scope, expression | `evaluateExpression` | `evaluateCondition` | |---|---|---| | CONTROL `{ data: { status: 'draft' } }`, `${data.status}` | `"draft"` | `true` | | CONTROL same scope, `Status: ${data.status}` | `"Status: draft"` | `true` | | CONTROL same scope, `${data.nope}` — present root, absent member | `undefined` | `false` | | `{ data: {} }` — adapter-shaped, `${data.status == 'draft'}` | `false` | `false` | | `{ data: undefined }`, same predicate | `false` | `false` | | `{}` — no `data` root, `${data.status}` | `"${data.status}"` | `true` | | `{}` — no `data` root, `Status: ${data.status}` | `"Status: ${data.status}"` | `true` | | `{}` — no `data` root, `${data.status == 'draft'}` | `"${data.status == 'draft'}"` | `true` | The third control is the discriminating one: a root that is PRESENT with an absent member yields `undefined`, while a root that is MISSING yields the template's own source characters. So the two layers answer a missing root differently, and each has its own reader-visible symptom: - **predicate layer** — fails soft to `true`. A `"visible": "${data.status == 'draft'}"` authored from these pages was **hidden on every row** and is now **shown on every row**; spelled `"hidden"` it flips the other way. - **interpolation layer** — does not fail soft at all. A `content` built from a missing root paints the literal characters `${data.status}` on screen. The callout carries one column per layer and tells the reader to read both. It also states that re-publishing `data` restores the OLD always-`false` verdict rather than fixing the gate, and points row gates at `record` — the runtime-layer root under ADR-0089 D3. ## What I decided about the ~two dozen `${data.…}` examples Measured, not estimated: `schema-expressions.md` carries **21** lines spelling `${data.` (one of them is the census's own scope-table row). Repo-wide under `skills/`: 21 here, 14 in `rules/protocol.md`, 5 in `page-builder.md`, 4 in `testing.md`, 2 each in `data-integration.md` and `auth-permissions.md`, 1 in `architecture.md`, plus 4 in `evals/*.json`. **Decision: keep every one of them verbatim and make them reachable, rather than rewrite them to bare roots.** The page's new wiring example publishes a root literally named `data`, and the scope table says in a row that every `${data.*}` example on the page assumes exactly that. Three reasons this is the right divergence from objectstack-ai#9369's `content/docs` shape: 1. **A pin renders those exact spellings.** `skill-guide-provider-envelope.test.tsx`, as re-derived in objectstack-ai#9369, publishes `scope = { data: PROVIDER }` and renders `${data.customers}` / `${data.label}`. Rewriting the guide spellings would move that pin's subject from inside a second pull request — the "green alone, red together" shape. 2. **ADR-0089 D3 does not forbid the name.** `CANONICAL_ROOT_BY_LAYER` puts `data` at the metadata layer. What was retired is the renderer AUTO-publishing the adapter there, not the name. 3. **Most of those examples are about something else** — which text keys carry expressions, type preservation, the troubleshooting section. The root name is scaffolding, and churning 20 lines of scaffolding on a governed surface buys the reader nothing. ⛔ Nothing was silently rewritten: the only expression text this PR changes is the two `dataSource = {…}` comments that named the retired wiring. ## Re-measurement of the card's per-line census The card measured on the objectui#9308 branch. Re-measured against `origin/main` `69aa9c017` — **every cited line still lands on the cited text**: | cited | on `origin/main` `69aa9c017` | verdict | |---|---|---| | 112 | `| Top-level data fields | \`SchemaRendererProvider dataSource\` | …` | exact | | 113 | `| \`data\` | Alias for dataSource root | …` | exact | | 302-303 | `dataSource = { customerNames: … }`, `useDataScope("customerNames")` | exact | | 305 | `**Nested paths work:** … resolves \`dataSource.app.settings.users\`` | exact | | 393 | `// ✅ Bound data, already node-shaped: dataSource = { rows: … }` | exact | `premise_still_valid: true`. ## Enumeration beyond the census (⛔ out of scope for this PR) `--measure` judges every candidate fence, marked or not, so a page can be wrong where no gate looks. Reading the prose as well turned up two more: - **`guides/auth-permissions.md`** carries the same retired claim in its own words — a scope table row `| \`data\` | the \`dataSource\` passed to \`SchemaRendererProvider\` |`, the sentence "Keys of the `dataSource` object are reachable only under the `data.` root", and "With no host scope mounted, `data` and `page` are all you get". ⛔ Out of scope here: **PR objectstack-ai#9374 holds that file**, so an in-place edit would collide. Filed as objectstack-ai#9379. - **`guides/testing.md` Pattern 5** asserts `getByText('Secret')` for a node gated on `${userRole !== "admin"}` — a BARE root that no channel publishes, before or after the ruling. Copy the example and the assertion fails. Different defect class from this card, and independent of the ruling — it is wrong on `main` today. Filed as objectstack-ai#9380. Noted, not filed: the `${data.*}` examples in `page-builder.md`, `architecture.md`, `i18n.md`, `project-setup.md` and `evals/*.json` are in the same reachable-only-if-the-host-publishes class; they are not falsifiably wrong and the skills lane that takes the auth-permissions card is their natural carrier. ## Verification Reproduce-first, as required — the harness is trustworthy before any red or green from it is read: ```text $ node scripts/check-skill-examples.mjs --self-test # BEFORE the build EXIT=2 — PRECONDITION NOT MET: the self-test's type-check leg needs the workspace built $ ...os-verify-lock.sh -c 'turbo run build $(--build-filter) --concurrency=2' VERDICT command-exit 0 · held the lock 44s · waited 0s $ node scripts/check-skill-examples.mjs --self-test # AFTER the build EXIT=0 — ✓ 59 cases pass ``` Gate, verbatim, before and after this diff: ```text BEFORE (exit 0) Scanned 20 guide(s) under skills, .claude/skills: 121 ts/tsx/typescript fence(s), 70 json/jsonc fence(s). Marked: 14 ts fence(s) (floor 13), 70 json fence(s) (floor 70) Semantic phase: 14 of 14 ts fence(s) judged, 0 failed. JSON phase: 70 fence(s) parsed, 0 failed. Every marked skill example holds up against the built types. AFTER (exit 0) Scanned 20 guide(s) under skills, .claude/skills: 122 ts/tsx/typescript fence(s), 70 json/jsonc fence(s). Marked: 16 ts fence(s) (floor 13), 70 json fence(s) (floor 70) Semantic phase: 16 of 16 ts fence(s) judged, 0 failed. JSON phase: 70 fence(s) parsed, 0 failed. Every marked skill example holds up against the built types. ``` ⭐ The marked ts population moves **14 to 16** and the json population is unmoved at 70 — both floors are SHRINK-ONLY and neither is breached. The two new marked fences are the wiring examples, so the thing this card is about is now COMPILED against the built `dist/*.d.ts` rather than only read. Derived by hand from `package.json` plus `.github/workflows/` (this repo has no `dispatch-gates.mjs`); every one run on this tree: | gate | exit | |---|---| | `check-skill-examples.mjs --self-test` | 0 | | `check-skill-examples.mjs` | 0 | | `check-skills-paths.mjs` | 0 — 88/89 stated paths resolve, 1 pre-existing baselined | | `check-skill-eval-tokens.mjs --self-test` / `check-skill-eval-tokens.mjs` | 0 / 0 | | `check-control-bytes.mjs` | 0 — 7539 tracked text files | | `check-shell-escape-residue.mjs` | 0 — 16/16 skills documents under a declared root | | `check-new-cross-file-line-citations.mjs` | 0 — 0 new citations | | `check-changeset-presence.mjs` | 0 | | `check-doc-links.mjs` | 0 | | `check-governed-queue-guard.mjs --self-test` | 0 — 185 cases | Package tests that READ these three documents (`markdown-test-inputs.mjs --changed` names all three as test inputs, so CI runs the full shards): reported below the fold once the shared verify lock grants a turn. Line readings for the governed surface, as the contract requires: | reading | before | after | delta | |---|---|---|---| | `guides/schema-expressions.md` | 569 | 635 | +66 | | `guides/data-integration.md` | 485 | 513 | +28 | | `rules/protocol.md` | 341 | 354 | +13 | | whole published package `skills/**` | 5201 | 5308 | +107 (+2.1%) | The added lines are the correction itself: one migration callout, two now-compiled wiring examples, and the sentences that replace the retired claims. No re-wrap was used to buy lines. ## Changeset **None owed**, by measurement rather than by inheritance: 1. `node scripts/check-changeset-presence.mjs` — "Compared the working tree with `69aa9c017` (merge-base with origin/main): 3 file(s) changed, 0 of them published source of a package the release covers, 0 of them a manifest whose published contract moved … No source or published contract of a released package changed in this range, so no changeset is owed." (exit 0) 2. Independently: **0 of 43** workspace manifests mention `skills` in `files` / `exports` / `main` / `module` / `types` / `bin`. Positive control on the same scan: 38 manifests list `dist` in `files[]`. Nothing under `skills/` is shipped by any npm package. 3. The merged precedent on this surface, `28be0786d` (objectstack-ai#9352), changed one `skills/` file and carried no changeset.⚠️ One inconsistency worth a maintainer's eye rather than my guess: open PR objectstack-ai#9374, also a single `skills/` guide, DOES carry an empty-frontmatter changeset declaring "no package is released by this change". Both forms pass the gate. If the empty-frontmatter declaration is the house style for this surface, say so and I will add one. ## Governed-queue-guard verdict on this diff (verbatim) ```text $ node scripts/check-governed-queue-guard.mjs --test skills/objectui/guides/schema-expressions.md skills/objectui/guides/data-integration.md skills/objectui/rules/protocol.md ⛔ GOVERNED — 3 of 3 path(s) are on a governed surface: skills/** x3 — the published skills catalog - skills/objectui/guides/schema-expressions.md - skills/objectui/guides/data-integration.md - skills/objectui/rules/protocol.md One governed path governs the WHOLE pull request — proportion is not a question. ⛔ Do not flip it ready, enqueue it, or arm auto-merge. Park it as a DRAFT and leave the merge to the maintainer; a human merge IS the review record for a governed surface. The merge-queue run of "Governed Surface Queue Guard" refuses this diff unless an APPROVED review by an authorized approver (GOVERNED_APPROVERS: os-zhuang, hotlong) is on the pull request — on whichever commit it was left (maintainer ruling 2026-09-04). exit 3 ``` ## Clause-② — contract review `needs:contract-review` is hung on this PR in the same stroke as opening it, mirroring the card. Published `skills/**` making falsifiable contract-semantics claims about which roots a tier binds. ⛔ The governed-surface human merge does **not** substitute for it; the two stack, and neither limb is mine to clear. ## Inherited reds — ⛔ not from this PR - `Bundle Analysis` is red on `main` (arrived with objectstack-ai#9316; a maintainer decision). - `Doc Snippet Type Check` is red on `main` until objectstack-ai#9369 lands. This diff touches no document that gate scans (`content/docs` + package READMEs + root `README.md`, explicitly not `skills/**`). ## Acceptance notes - ⛔ No test was skipped, quarantined or weakened. No assertion was loosened. The marked-fence floors both hold and the ts population grew. - The card's "the pins are green while the prose beside them is wrong" warning was taken literally: the six `packages/components` pins objectstack-ai#9369 re-derived were read for what they assert about these files before a byte was changed, and every string they pin is intact — `Rule: Keys Live on the Node`, the `hoists every key onto\s+the node` regex, the absence of the two retired sentences, the `no md matches "instead of \`props.\`"` class guard, and the `"bind": "customerNames"` list example in each of the three guides. - Noted, not filed: `rules/protocol.md`'s two measured tables still cite `origin/main` `f1c27f037` as the commit they were measured on. This PR restates their WIRING, not their outcomes, and says so inline; a re-measurement on a current commit is a separate piece of work, and the `skill-guide-provider-envelope` pin is what actually holds those outcomes today. Carrier: whoever next re-derives that pin. ## 维护者速读(草稿) ### 改了什么 把发布中的 `skills/objectui` 三份指南从「`dataSource` 就是表达式根 `data`、`bind` 走 `dataSource`」改成 ruling 之后的真实通道:宿主用 `PredicateScopeProvider` 发布作用域,`dataSource` 只是取数适配器。另外给两个接线示例加上 `os:check` 标记,让它们第一次真的被编译校验。 ### 为什么改 2026-09-13 的裁决(objectui#9308 option B)已经把这两件事退役,但发布给 AI 读的技能包还在教它们。更要紧的是:这不是改名。同一条 `data.*` 门,旧接线下恒判 `false`、新接线下因为根缺失走 fail-soft 恒判 `true` —— 一个 `visible` 门从「每行都藏」变成「每行都显」。只换 provider 名字的迁移说明会让读者踩正这一脚。 ### 风险与代价(含回滚) - **排序风险(主要):** objectstack-ai#9369 还没合。在它合进去之前,这三页描述的是尚未落地的行为。⚠️ 建议排在 objectstack-ai#9369 之后合。 - 保留了全部 `${data.*}` 示例原文,只让接线示例发布一个叫 `data` 的根 —— 因为 `skill-guide-provider-envelope` 这个 pin 正在渲染这些拼写,改写它们会在另一个 PR 的盲区里挪动 pin 的被测主体。 - 回滚代价:纯文档,`git revert` 一笔即可,不影响任何已发布包(本仓 43 个 manifest 无一发布 `skills/`)。 ### 席位意见 (留空,待 `os-zhuang` / `hotlong` 定稿) ### 你要做的 1. 确认合并顺序:先 objectstack-ai#9369,再本 PR。 2. 作为受管面的人工合并方给出 APPROVED review(这一道是 `check-governed-queue-guard` 机械要求的)。 3. clause-② 的 `needs:contract-review` 需要一份书面复核记录后才清 —— ⛔ 两道保障叠加,人工合并不替代它。 4. 顺带定一句话:本仓单文件 `skills/` 改动到底要不要写空 frontmatter changeset(objectstack-ai#9352 没写、objectstack-ai#9374 写了,门禁两边都放行)。 --- _Generated by [Claude Code](https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ)_ --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: os-zhuang <jack@objectstack.ai>
…ling repaired (objectstack-ai#8941) >⚠️ **GOVERNED — this PR stays a draft until an authorized approval is on it.** `node scripts/check-governed-queue-guard.mjs --test` over this PR's file list answers `GOVERNED — 1 of 42 path(s)`: `skills/objectui/guides/plugin-development.md`, the published skills catalog. One governed path governs the whole PR. ⛔ Not to be flipped ready, enqueued or armed with auto-merge until `os-zhuang` or `hotlong` leaves an APPROVED review; ⛔ no seat approves it. After that approval the claiming seat lands it. Moves `lucide-react` from `^1.31.0` to `^1.43.0` across the workspace, repairs what the jump breaks, and executes the maintainer's **ruling C** on this PR (comment `5750512894`, verbatim 「8941 C」): relax the two pins, land the bump. The re-measured census comes first, then ruling C, then the checks. The sections under **Historical** further down were measured on older trees and are ⛔ not re-derived. objectui#7058 (the Dependabot branch this PR superseded) is already closed, unmerged, since 2026-09-13. This PR never named it with a closing keyword; closing it was never this PR's. ## 1. This round's census — re-measured on 2026-09-25 against the INSTALLED 1.43.0 Tree: this branch after merging `main` at `21d34d5e2` (merge commit `423a1a59a`); the censuses read the committed tree through `git ls-tree` plus `git cat-file`, so enumeration and reading come from the same source. The installed `lucide-react` is 1.43.0, integrity-equal to the registry tarball (`sha512-ubtnda1f…`).⚠️ **Correction to the baseline.** `main` does not install 1.31.0. Its manifests say `^1.31.0`, but since the `pnpm dedupe` of objectstack-ai#9316 (2026-09-13) its lockfile resolves **1.35.0**, one copy. So this PR moves the declared range `^1.31.0` to `^1.43.0` and the installed version 1.35.0 to 1.43.0. Every comparison below is taken against both 1.31.0 and 1.35.0. ### 1a. What lucide itself changed (read off the three installed artifacts) | Surface | 1.31.0 | 1.35.0 (main today) | 1.43.0 | Removed, 1.35.0 to 1.43.0 | |---|---|---|---|---| | runtime exports of `lucide-react` | 6068 | 6110 | 6224 | **0** | | public names in `lucide-react.d.ts` | 6072 | 6114 | 6230 | **0** | | `dynamicIconImports` keys (= `iconNames`) | 2025 | 2039 | 2077 | **0** | | keys of the runtime `icons` record | 1767 | 1781 | 1818 | **1 — `Trash2`** | | icon modules exporting `__iconNode` | 1767 | 1781 | **0** | — | | icon modules exporting `__iconData` | 0 | 0 | 1818 | — | | icon modules declaring `aliases` | 0 | 0 | 248 | — | 1.31.0 to 1.43.0 also removes 0 exports, 0 type names, 0 dynamic names and the same single record key. ### 1b. Census 1 — every name this tree imports from `lucide-react` resolves TypeScript-AST scan of every tracked code file: import and export-from declarations (named, default, namespace, `type`), `import()` / `vi.mock()` / `require.resolve()` calls naming the package, `import type` nodes, and static member reads off a namespace import or off `icons`. | Tree | importing files | sites | distinct value names | type names | fail on 1.43.0 | fail on 1.35.0 | fail on 1.31.0 | |---|---|---|---|---|---|---|---| | this branch after the merge | 413 | 1980 | 329 | 2 (`LucideIcon`, `LucideProps`) | **0** | 0 | 0 | | `6f20cfb06` (the 2026-09-20 head) | 419 | 2018 | 331 | 2 | 0 | 0 | 0 | Controls, in the same run: an injected `Trash`, `type LucideIcon` and namespace read `L.Pencil` resolve; an injected `NotARealLucideIcon` and `L.AlsoNotReal` are both reported as failures. ### 1c. Census 2 — every authored icon STRING resolves as a key of the runtime `icons` record Every string in an icon-named position (`icon`, `iconName`, `...Icon` properties, JSX attributes, `resolveIcon(...)` arguments) across code, JSON, YAML and Markdown, tokenised with the seam's own rule copied verbatim (`split(/[-_\s]+/)`, capitalise, `Home` to `House`), judged against each version's record. | Reading | Result | |---|---| | sites / distinct names | 717 / 207 | | live on 1.43.0 | 641 | | live on 1.35.0 or 1.31.0 but **dead on 1.43.0** (what the bump breaks) | **0** outside this PR's own changeset prose, which names `trash-2` as the spelling it retired | | dead on all three versions | 75 sites, 40 distinct — pre-existing and untouched by the bump: the deliberate retired-spelling negatives (`edit`, `smile`, `filter`, `not-a-real-icon`, `definitely-not-a-lucide-icon`…) and strings that are not icon names at all (colour classes, labels) | | `pnpm check:icon-record-names` | `OK lucide icon names: 184 authored/declared names reaching 1 record-reading resolver are live icons keys (record 1818 keys ...)` | Controls: an injected `icon: 'trash'` reads live, injected `icon: 'trash-2'` in JSON and in TS both read as regressions. Independently, a case-insensitive `git grep` for every spelling that tokenises to `Trash2` finds only component imports of `Trash2` (still a runtime export in 1.43.0), comments, and one `vi.mock` stub key: no authored string. The same census over `main` finds exactly 6 string sites that tokenise to `Trash2` (5 `trash-2`, 1 `Trash2`, one per file); they are the ones the 2026-09-20 round repaired, and all 6 read `trash` here. **The glyph is unchanged by that repair**, measured on icon nodes: | Comparison | full node | node without `key` | |---|---|---| | 1.43.0 `trash` vs 1.31.0 `trash-2` | IDENTICAL | IDENTICAL | | 1.43.0 `trash` vs 1.35.0 `trash-2` | IDENTICAL | IDENTICAL | | control: 1.43.0 `pencil` vs 1.31.0 / 1.35.0 `pencil` | IDENTICAL | IDENTICAL | | control: 1.43.0 `trash` vs 1.35.0 `trash` | DIFFERENT | DIFFERENT |⚠️ The last control row is also a user-visible fact: lucide moved the two-line bin artwork under the `trash` name, so anything that already authored `trash` (10 string sites on `main`) now draws what `trash-2` drew. Upstream also redrew other glyphs. Comparing the artwork each name draws on 1.35.0 against 1.43.0 (controls: `Trash2` reads unchanged, `Trash` reads changed), the ones this tree uses are the components `CheckCircle2` (29 importing files), `Table2` (8), `CircleDot` (5), `CircleCheck` (2), `Asterisk` (1) and `BadgeCheck` (1), and the strings `trash`, `shopping-cart`, `circle-check` / `CircleCheck` and `table-2`. Redraws, not breakage. ### 1d. Census 3 — every reader of `__iconNode` / `__iconData` works A `git grep` for `__iconNode|__iconData|createLucideIcon|LucideIconData` over the whole tree finds exactly **one reader**: the lazy draw in `packages/components/src/renderers/action/resolve-icon.ts`, which reads `module.__iconData?.node ?? module.__iconNode` (the 2026-09-20 repair). No other code reads an icon module's exports. Red, then green, on the lazy-draw row, predicted red before running, through `scripts/ablation-replace.mjs` from `../objectstack` (anchor must hit exactly once, write verified on disk, restore proven by blob hash and an empty `git diff HEAD`): ``` mutation const node = module.__iconData?.node ?? module.__iconNode; -> const node = module.__iconNode; ok mutation landed: anchor 1 -> 0, blob cc7d80e -> 271d15e3fa58 RED x fills the path data in once the icon module arrives AssertionError: expected 0 to be greater than 0 Tests 1 failed | 5 skipped (6) restore ok restored: blob == HEAD (cc7d80e) and `git diff HEAD` is empty GREEN Tests 1 passed | 5 skipped (6) (at 423a1a5) ``` **Real-build probe**, on the console built at `989330df0` (`apps/console/dist`), in Chromium: of 2345 built JS chunks, 1819 mention `__iconData` (the 1818 `vendor-icon-*` chunks plus the seam's `ui-components` chunk) and exactly 1 mentions `__iconNode`, the seam's fallback read; the built seam reads `__iconData?.node??n.__iconNode`. Four built icon chunks imported in the browser: | chunk | `__iconData.name` | aliases | path elements | first `d` | `__iconNode` | |---|---|---|---|---|---| | `vendor-icon-house-*` | `house` | `home` | 2 | `M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8` | undefined | | `vendor-icon-trash-*` | `trash` | `trash-2` | 5 | `M10 11v6` | undefined | | `vendor-icon-arrow-down-0-1-*` | `arrow-down-0-1` | `arrow-down-01` | 5 | `m3 16 4 4 4-4` | undefined | | `vendor-icon-loader-circle-*` | `loader-circle` | `loader-2` | 1 | `M21 12a9 9 0 1 1-6.219-8.56` | undefined | So the shipped glyph data is non-empty and sits where the seam now reads it, and the pre-repair read would find nothing in the real build too. NOT MEASURED: a rendered page. With no backend the built console lands on `/login`, which draws no lucide svg at all (0 found), so there was no seam-drawn glyph on screen to count. ### 1e. Census 4 — every `lucide-NAME` class token in the tree, against the 1.43.0 alias index Every `lucide-` token in every tracked text file (lockfile and binaries excluded), classified against an alias index built from the 1818 installed icon modules (canonical `lucide-` plus the icon name; one alias class per declared alias), against what the seam emits (the key-derived class and the canonical class, rule copied verbatim from `lucideClassNames`), and against what 1.35.0 emitted. | Class of token | distinct | disposition | |---|---|---| | canonical class of a 1.43.0 icon | 29 | safe: lucide's component and the seam both emit it | | not an icon class (`lucide-icon`, script names such as `lucide-record-icon-names`) | 5 | out of scope | | **alias** class: `lucide-home` (of `house`) | 1 | the button pin — relaxed under ruling C, see section 2 | | **alias** class: `lucide-loader-2` (of `loader-circle`) | 1 | `svg-host-dom-leak-5632.test.tsx`, a spinner drawn by a component import, so lucide's own alias class IS on it; the exact assertion the 2026-09-20 round updated holds on 1.43.0 | | **alias** / retired: `lucide-trash-2`, `lucide-trash2` | 2 | prose only: the seam docblock and the pin comment (both rewritten in this round), and the pending objectui#9251 changeset (left alone; this PR's changeset dates its example) | | seam-key-only class (emitted by the seam, not by lucide 1.43.0) | **0** referenced | — | Controls: an injected `svg.lucide-trash-2` assertion is classified as an alias of `trash`; an injected `svg.lucide-check` reads canonical. Six dynamically built selectors (`svg.lucide-${name}` in the command and context-menu suites) iterate canonical names only. ### 1f. New since the 2026-09-20 round `main` moved 294 commits past the old merge base `078f2e4f7`. What the four censuses see that the `6f20cfb06` tree did not: | Census | new at this head | verdict on 1.43.0 | |---|---|---| | imports | 7 sites in 6 files; 4 files import lucide for the first time: `packages/app-shell/src/layout/ReadRateBanner.tsx` (`TriangleAlert`), `packages/app-shell/src/views/ActionParamDialog.tsx` (`ChevronDown`, `Lock`), `packages/plugin-detail/src/__tests__/detailSectionAuthoredIcon-9529.test.tsx` (`MapPin`), `packages/app-shell/src/hooks/__tests__/userLocale-10059.test.tsx` (a `vi.mock` of the package); plus `Code2` in `FlowKeyValueField.tsx` and `BookOpen` in `NavigationDesigner.tsx` | all resolve | | authored strings | 13 sites; new names `share`, `map-pin` | all live, except 2 `edit` sites in `menubar-item-icon.test.tsx`, which are deliberate retired-spelling negatives, dead on all three versions | | `__iconNode` / `__iconData` readers | none | — | | class tokens | 6 pairs, all in `packages/components/src/__tests__/menubar-item-icon.test.tsx`: `lucide-chevron-right`, `lucide-download`, `lucide-file`, `lucide-mail`, `lucide-save`, `lucide-share` | all canonical | None of them needed a repair. ### 1g. The merge and the lockfile `origin/main` **merged** at `21d34d5e2` (⛔ no rebase, no amend, no force-push): one conflict, `packages/plugin-kanban/package.json`, resolved as the union (main deleted `@tanstack/react-virtual` with the module that used it; this branch bumps `lucide-react`). `pnpm-lock.yaml` was regenerated by pnpm from main's own lockfile, ⛔ not hand-merged: main's file taken whole, then `pnpm install --lockfile-only` with the 22 manifests briefly at an exact `1.43.0` and again with `^1.43.0` restored, which pnpm keeps. A plain re-lock follows the caret to the newest 1.x (1.48.0 today). The delta against main is lucide alone: 22 importer specifiers, 22 resolutions and the fumadocs peer suffixes. All 22 manifests that declare `lucide-react` read `^1.43.0`. ## 2. Ruling C — the two pins, relaxed and no more⚠️ **This weakens a gate that was telling the truth.** The two rows below were red because the seam genuinely stopped reproducing lucide's classes; they were not flakes. lucide-react 1.43.0 emits one class per DECLARED ALIAS (`house` renders `lucide-house lucide-home`) and no longer the class it derived from the PascalCase key. The alias list lives only inside each icon module, which objectui#9251 made lazy, while the seam must put its classes on the first frame. The maintainer chose C over A (put every alias into the eager, build-generated name list, spending bytes under objectui#9251's ruling) and over B (hold at 1.31.0), with the weakening stated when it was presented. ⇒ After this PR, a seam-drawn glyph missing lucide's alias classes, or carrying the key-derived class lucide no longer emits, passes these rows. ⛔ It is not free. **Re-measured size of what is given up** (1.43.0 facts, the seam's rule copied verbatim): of 1818 record keys, the seam's class set and lucide's **agree on 1494 and disagree on 324** (80 seam-only, 223 lucide-only, 21 both ways) — the same figures the 2026-09-20 round read. **Why the canonical form of C and not the subset form:** the seam's classes are a subset of lucide's for only 1717 of 1818 keys (the key-derived class is not one lucide 1.43.0 emits), so "the seam's classes need only be a subset" would still be red on 101 keys. The canonical class is on the seam for **1818 of 1818**. **Exactly two pins.** They are the two rows `Test (shard 3/4)` and `Test (shard 4/4)` reported red on `6f20cfb06`, read off those check runs' own annotations: - shard 3/4: `button-shared-icon-resolver.test.tsx` > `resolves home through the RENAME alias to House, not to a dead home` — `expected null to be SVGSVGElement`; - shard 4/4: `resolve-icon-lazy-9251.test.tsx` > `reproduces lucide's per-icon class names over the WHOLE record vocabulary` — `AArrowDown: expected "lucide lucide-a-arrow-down", got "lucide lucide-a-arrow-down lucide-aarrow-down"`, and nine more. Each now carries, at its site, what it used to refuse and why it no longer does. Verbatim below, with two deliberate substitutions: the generic in `Record OF string, React.ElementType` and the placeholder in `lucide-ICONNAME` are written in capitals instead of angle brackets, because GitHub deletes tag-shaped fragments from a body even inside a code fence. Byte-exact text: `git show 178da2e`. ### Pin 1 — `packages/components/src/renderers/action/__tests__/resolve-icon-lazy-9251.test.tsx` Before (`6f20cfb06`): ```ts it('reproduces lucide\'s per-icon class names over the WHOLE record vocabulary', () => { //⚠️ The class names are the part a conversion rule gets wrong silently: 95 // of the record's keys pack digits that lucide splits in the module name and // not in the class derived from the key, so `Trash2` renders BOTH // `lucide-trash2` and `lucide-trash-2`. `createLucideIcon` is what builds // them and the seam no longer calls it, so every name is compared against // the record's own component here — not a sample, and not a copy of the // rule. const keys = Object.keys(icons); expect(keys.length).toBeGreaterThan(1000); const mismatches: string[] = []; let digitKeysChecked = 0; for (const key of keys) { if (/\d/.test(key)) digitKeysChecked += 1; const reference = renderIcon((icons as Record OF string, React.ElementType)[key], {}); const seam = renderIcon(resolveIcon(key)!, {}); const want = classesOf(reference).join(' '); const got = classesOf(seam).join(' '); if (want !== got) mismatches.push(`${key}: expected "${want}", got "${got}"`); } expect(mismatches.slice(0, 10)).toEqual([]); // Non-vacuity, both ways: the loop ran, and it ran over the class of name // this row exists for. expect(digitKeysChecked).toBeGreaterThan(50); }); ``` After: ```ts it('carries lucide\'s CANONICAL per-icon class over the WHOLE record vocabulary', () => { //⚠️ RELAXED under the maintainer's ruling C on objectui#8941 (comment // 5750512894, verbatim 「8941 C」). This row USED TO REFUSE any seam glyph // whose complete class set differed from lucide's own component's. It NO // LONGER DOES, because lucide-react 1.43.0 emits one class per DECLARED // ALIAS (`house` renders `lucide-house lucide-home`) and no longer emits the // class it used to derive from the key, and the alias list exists only // inside each icon module — the modules objectui#9251 made lazy — while the // seam has to put its classes on the FIRST frame. The ruling chose the bump // over carrying every alias in the eager static name list (option A), and // it was taken knowing that it weakens this row. So a seam glyph missing // lucide's alias classes, or carrying the key-derived class lucide no longer // emits, now passes here. // // What it still refuses: a seam glyph without `lucide` or without lucide's // CANONICAL class, `lucide-ICONNAME`, which lucide emits first after the // bare `lucide`. That is the class a conversion rule gets wrong silently, // because many keys pack digits that lucide splits in the module name // (`ArrowDown01` is `lucide-arrow-down-0-1`, never `lucide-arrow-down01`). // Every name is still compared against the record's own component — not a // sample, and not a copy of the rule. const keys = Object.keys(icons); expect(keys.length).toBeGreaterThan(1000); const mismatches: string[] = []; let digitKeysChecked = 0; for (const key of keys) { if (/\d/.test(key)) digitKeysChecked += 1; const reference = renderIcon((icons as Record OF string, React.ElementType)[key], {}); const seam = renderIcon(resolveIcon(key)!, {}); // Read in lucide's own order: `lucide`, then the canonical class, then // one class per alias. Only the first two are required of the seam. const [base, canonical] = (reference?.getAttribute('class') ?? '').split(/\s+/).filter(Boolean); const got = classesOf(seam); if (base !== 'lucide' || !canonical?.startsWith('lucide-') || !got.includes(base) || !got.includes(canonical)) { mismatches.push(`${key}: expected "${base} ${canonical}" among "${got.join(' ')}"`); } } expect(mismatches.slice(0, 10)).toEqual([]); // Non-vacuity, both ways: the loop ran, and it ran over the class of name // this row exists for. expect(digitKeysChecked).toBeGreaterThan(50); }); ``` ### Pin 2 — `packages/components/src/renderers/form/__tests__/button-shared-icon-resolver.test.tsx` Before (`6f20cfb06`): ```ts it('resolves `home` through the RENAME alias to House, not to a dead `home`', () => { // The single entry both maps carried. It is the one input whose answer // would have changed had the dedupe dropped the alias on the floor. const button = renderButton({ icon: 'home' }); const house = button.querySelector('svg.lucide-house'); expect(house).not.toBeNull(); // The negative half used to read `querySelector('svg.lucide-home')` is // null. That form RETIRED on lucide-react 1.43.0, which reshaped // `createLucideIcon` to take an icon-data object carrying `aliases` and // to emit one class PER ALIAS: `house` now renders // `class="lucide lucide-house lucide-home ..."` itself, so the class no // longer says which glyph was drawn (measured: 248 of 1818 icon modules // carry aliases; 1.31.0 icon modules have no `aliases` field at all). // The discriminating form is below and still fires for the same defect: // `Home` is not a key of the runtime record, so had the rename been // dropped this button would render NO glyph at all, and any SECOND glyph // would be a different element from `house`. expect(button.querySelectorAll('svg')).toHaveLength(1); expect(button.querySelector('svg.lucide-home')).toBe(house); }); ``` After: ```ts it('resolves `home` through the RENAME alias to House, not to a dead `home`', () => { // The single entry both maps carried. It is the one input whose answer // would have changed had the dedupe dropped the alias on the floor. const button = renderButton({ icon: 'home' }); expect(button.querySelector('svg.lucide-house')).not.toBeNull(); //⚠️ RELAXED under the maintainer's ruling C on objectui#8941 (comment // 5750512894, verbatim 「8941 C」). This row USED TO REFUSE a house glyph // that did not also carry lucide's `lucide-home` ALIAS class on the same // element: `querySelector('svg.lucide-home')` had to BE the house svg. It // NO LONGER DOES, because this button draws through the objectui#9251 // seam, which emits lucide's canonical class and not lucide-react // 1.43.0's per-alias classes, and the ruling accepted that rather than // put every icon's aliases into the eager static name list. // // ⛔ Nor does it go back to the older form, `svg.lucide-home` is null: // lucide 1.43.0 itself puts `lucide-home` on the house glyph, so that // class no longer says which glyph was drawn, and pinning its absence // would pin the seam's divergence from lucide. `lucide-home` is neither // required nor forbidden here. What still discriminates is the count: // `Home` is not a key of the runtime record, so had the rename been // dropped this button would render NO glyph at all. expect(button.querySelectorAll('svg')).toHaveLength(1); }); ``` (On `main` this row still reads `expect(button.querySelector('svg.lucide-home')).toBeNull();`, which is green under the seam on either version. ⛔ It is not restored: lucide 1.43.0 puts `lucide-home` on the house glyph itself, so pinning its absence would pin the seam's divergence from lucide, and the row's own comment now says so.) **Both relaxed rows can still fail, for what they still refuse.** Same tool and restore proof as 1d, each prediction written before its run: ``` PIN 1 mutation return [fromKey, `lucide-${kebab}`, className] -> return [fromKey, className] RED x carries lucide's CANONICAL per-icon class over the WHOLE record vocabulary "ArrowDown01: expected \"lucide lucide-arrow-down-0-1\" among \"lucide lucide-arrow-down01\"" (and 9 more) restore ok restored: blob == HEAD (99c47bc) and `git diff HEAD` is empty PIN 2 mutation delete the `Home: 'House',` rename entry RED x resolves `home` through the RENAME alias to House AssertionError: expected null not to be null restore ok restored: blob == HEAD (99c47bc) and `git diff HEAD` is empty ``` (The first attempt at the pin-2 mutation passed an empty replacement, which the tool refuses; no test ran on it. The run above used its delete mode.) **The seam's own docblock moved with the pins.** `lucideClassNames` claimed its classes were derived the way lucide derives them and that a pin caught either derivation drifting, under a test file name that does not exist. Neither survives this bump and this ruling, so it now says what the seam emits, what 1.43.0 emits instead, and which pin holds how much. Comment only; no code in `resolve-icon.ts` changed this round. **⛔ No third pin.** The full suite ran locally at `989330df0` in 16 shards (`pnpm exec vitest run --shard=K/16`, each under `../objectstack/scripts/pm/os-verify-lock.sh`): **3618 test files** (3612 passed, 6 skipped, **0 failed**), **48571 tests** (48527 passed, 44 skipped, **0 failed**). CI on the same head: all eight `Test (shard N/8)` and `Test (dist pins)` green. No other pin went red, for this reason or for any other, so there was nothing to stop and report. ## 3. Checks All at `989330df0`, the head of this branch; heavy runs serialized through `../objectstack/scripts/pm/os-verify-lock.sh`, every exit code captured before any pipe. | Check | Command | Reading | |---|---|---| | tests | 16 local shards, above | 3618 files, 48571 tests, 0 failed | | type-check | `pnpm turbo run type-check --concurrency=2` | `Tasks: 81 successful, 81 total`, 0 `error TS` | | type-check, root projects | `pnpm type-check:scripts`, `type-check:vitest-setup`, `type-check:vitest-config` | exit 0, 0 `error TS` each | | lint, whole repo | `pnpm turbo run lint --concurrency=2` | `Tasks: 47 successful, 47 total`, 0 errors in every task (warnings only). The three files changed this round, linted alone: 0 errors, 0 warnings | | build | `pnpm turbo run build --filter='./packages/*' --concurrency=2`, then `pnpm --filter @object-ui/console build` | 39 of 39; console exit 0 | | eager-closure budget | `node scripts/check-eager-closure-budget.mjs` | `Console eager closure is 3054.8 KB gzipped across 329 of 2345 chunks (budget: 3104.5 KB, headroom: 49.7 KB)`; `ui-components 266.5 KB / 282.2 KB`. ⛔ No ceiling or baseline moved: the script is byte-identical to main's | | locale catalogues, SDUI pins | `node scripts/check-eager-locale-catalogues.mjs`, `pnpm check:sdui-registration-pins` | exit 0 each | | dist | `pnpm test:dist`; `check-node-esm-load`, `check-published-dist-tooling`, `check-dist-completeness --all` | 9 of 9 tasks (components: 2 files, 8 tests); the three gates exit 0 | | icon names | `pnpm check:icon-record-names`; `node scripts/regenerate-lucide-record-icon-names.mjs --check` | OK (184 names, record 1818 keys); the generated table matches the installed manifest | | lockfile | `node scripts/check-lockfile-integrity.mjs --base` main's lockfile `--head pnpm-lock.yaml`, and the default invocation; `check-lockfile-dedupe` | `VERDICT clean — no @objectstack/* identity moved backward and no package gained a copy` (1773 resolutions on both sides); `VERDICT deduped` | | changesets | `check-changeset-presence`, `-no-major`, `-fixed`, `-overwrite`, `check-pending-changeset-literals` | exit 0 each; `check-changeset-claims` is report-only | | touched-path gates | `check-control-bytes`, `check-new-cross-file-line-citations` (0 new), `check-skills-paths`, `check-skill-examples` (after the build), `check-skill-eval-tokens`, `check-test-path-roots`, the three `check-vi-mock-*`, `check-doc-fence-languages`, `check-doc-example-ids`, `check-shell-escape-residue`, `check-action-ref-convention`, `check-phantom-dependencies`, `check-unused-dependencies` | exit 0 each | | governed | `node scripts/check-governed-queue-guard.mjs --test` over the 42 paths | `GOVERNED — 1 of 42` | | CI | the check runs on `989330df0`, read when this was written | 42 success, 3 skipped (`dependabot` and the two coverage lanes), 0 failed. The seat re-reads it | NOT MEASURED locally: `Build & E2E` and the live e2e lane (green on CI). ## 4. Changeset `.changeset/lucide-react-1-43-0-retired-trash2.md`, restated against today's tree. It declares, all `patch`, the 20 released packages whose `dependencies` range moves, `@object-ui/console` (its `devDependencies` range moves and its bundle ships the new icons), and `@object-ui/cli` and `@object-ui/types`, whose source changed. It names the invisible-glyph repair as a user-visible fix, says which classes component-imported and string-resolved icons carry now, and dates the `Trash2` class example in the pending objectui#9251 entry without editing that entry. Dropped from it: the claim that this bump collapses a double lucide install (main's objectstack-ai#9316 dedupe already did). ## 5. Governed surface and skills readings The one governed path is the plugin skeleton's `lucide-react` range in `skills/objectui/guides/plugin-development.md`, anchored to the in-repo range by `doc-version-claims.test.ts`. Readings: that file 481 lines before and after; the published `skills/` tree 5365 lines before and after, its SKILL.md files 137 lines before and after. One line changed, zero added. ## 维护者速读(草稿) **改了什么** — 把全仓 22 个包声明的图标库 `lucide-react` 从 `^1.31.0` 升到 `^1.43.0`(实际安装版本从 1.35.0 升到 1.43.0),同时修掉升级带来的问题:懒加载图标的路径数据换了导出名(不修就是所有按名字解析的图标变成空框),以及一个被上游退役的图标名 `trash-2` 改成 `trash`(图形不变)。按您的裁决 C,放宽了两条测试钉子。 **为什么改** — 上游 1.43.0 改了图标数据结构,并且给每个图标加了「别名 class」。我们的懒加载图标拿不到别名(别名只在懒加载的图标模块里),两条如实报告这一差异的测试因此变红。您选了 C:接受这个差异,落地升级,而不是把别名塞进首屏必载的名单(A)或停在旧版本(B)。 **风险与代价(含回滚)** — 代价是明确的:两条原本「说真话」的钉子被削弱了,之后按名字解析的图标少了 lucide 的别名 class,钉子不再报警;按别名 class 写的样式只对直接 import 的图标组件生效。另有一些图标上游重画了(例如 `trash` 现在画的是原来 `trash-2` 的样子)。回滚:整体 revert 本 PR 即回到 `^1.31.0` 与旧钉子。 **席位意见** — 建议批准(席位复核于 2026-09-25T14:03Z,head `989330df0`)。放宽恰为两条钉子(`178da2eea`),各在原位写明原先拒什么、为何不再拒、仍拒什么,且各有一次预测后变红的证明;`resolve-icon.ts` 本轮只改注释;预算脚本与 main 字节一致,无上限或基线移动;22 个 manifest 均为 `^1.43.0`,lockfile 中 lucide 只有 1.43.0 一份;该 head 上 CI 42 成功、3 跳过、0 失败。本分支合并的 main 基点之后 main 又前进 27 个提交:与本分支 merge-tree 无冲突,其新增 11221 行中 lucide 提及与图标字符串均为 0(同一扫描在本 PR 的 diff 上分别命中 99 与 7);合并队列会在最新 main 上重跑全量。开发代理提出的 objectui#9251 变更集措辞问题,席位取 A:不改那条,本 PR 的变更集已写明其示例的日期。 **你要做的** — 本 PR 命中受管面(`skills/objectui/guides/plugin-development.md`),需要 `os-zhuang` 或 `hotlong` 留一条 APPROVED review;之后由认领席翻 ready 并入队。 ## Historical — measured on the 2026-09-10 and 2026-09-20 trees, ⛔ not re-derived here Kept as the record of how this PR got here. Superseded where this round measured differently: main's installed version was 1.35.0, not 1.31.0; "main installs two physical copies" stopped being true at objectstack-ai#9316; the governed count now reads 1 of 42; the `Bundle Analysis` NOT MEASURED and the eslint narrowing below are replaced by section 3; two `path:line` citations below are re-spelled by content (AGENTS.md objectstack-ai#11), with nothing else in them changed; and the class note in the old section 3 describes component-imported icons only (string-resolved icons go through the seam, section 2). Supersedes the stale Dependabot branch objectui#7058 (`dependabot/npm_and_yarn/lucide-react-1.35.0`). That branch is not pushed to here — Dependabot force-pushes over it on every rebase — so this is a fresh branch cut from current `main`. Once this lands, objectui#7058 can be closed as superseded. It is deliberately not named with a closing keyword above; closing it is a maintainer action, not this PR's. ### 1. Which of the seven red checks were staleness, and which were real objectui#7058's base is `4e3a4f07e` — weeks behind `main`. I brought its head up to date in a throwaway worktree, re-locked from current `main`, and re-measured. Four of the seven were artefacts of the stale base; three were real. | Check on objectui#7058 | Verdict | Evidence | |---|---|---| | `Type Check` | **REAL** | The job failed at `pnpm check:icon-record-names`, not at `tsc`. `tsc` never ran. | | `Test (shard 1/4)` | **REAL** | `packages/cli/src/__tests__/app-generator.test.ts` — the generator's literal `lucide-react` range stayed at `^1.31.0` while the 22 sibling manifests moved. | | `Test (shard 3/4)` | **MIXED** | `check-lucide-icon-record-names.test.ts` REAL (same four sites). `ci-cd-pipeline-doc.test.ts` STALE — the lockfile carried two `@objectstack/spec` versions. | | `Test (shard 4/4)` | **MIXED** | `doc-version-claims.test.ts` REAL (`skills/objectui/guides/plugin-development.md` still taught `^1.31.0`). `check-lockfile-integrity.test.ts` STALE — its fixture forks a `zod@4.4.3` key the stale lockfile had replaced with `4.5.4`. | | `Lockfile Integrity Check` | **STALE** | `VERDICT 12 finding(s): 7 backward move(s), 5 duplication(s)` — Dependabot re-locked on an old base and dragged `@objectstack/client`, `core`, `formula`, `lint`, `sdui-parser`, `types` from 17.4.0 back to 17.3.0, plus a `zod` 4.4.3/4.5.4 split. | | `Bundle Analysis` | **STALE** | `vendor-objectstack 2874.0 KB / 1224.6 KB ceiling (OVER by 1649.3 KB)` — that is the duplicated `@objectstack` tree above, which the lockfile gate itself names as the cause: *"a bundle-size red downstream of this is caused by THIS, not by the dependency being bumped."* | | `dependabot` | STALE | Dependabot's own branch check. | On this branch, re-locked from current `main`, `node scripts/check-lockfile-integrity.mjs` reports `VERDICT clean — no @objectstack/* identity moved backward and no package gained a copy`. ### 2. Root cause of what survives — measured against the installed artifact The hypothesis in the dispatch was a broad source-compatibility break. **The measurement narrows it to a single retired name, and falsifies the type-checking half entirely.** I installed 1.31.0, 1.41.0 and 1.43.0 side by side and diffed three surfaces: | Surface | 1.31.0 | 1.43.0 | Removed | |---|---|---|---| | runtime exports of `lucide-react` | 6068 | 6224 | **0** | | public names in `lucide-react.d.ts` | 6072 | 6230 | **0** | | `iconNames` from `lucide-react/dynamic.mjs` | 2025 | 2077 | **0** | | keys of the runtime `icons` record | 1767 | 1818 | **1 — `Trash2`** | Then I enumerated what this repository actually imports — 4700 source files, 333 distinct value names, one type name (`LucideIcon`), six namespace or default imports — and checked every one against both artefacts. **Zero fail to resolve, in either version.** Controls fired on the same run in both worlds: `Trash` resolves in both, a fabricated name resolves in neither. So nothing breaks at an import and nothing breaks in the compiler. `Trash2` is still exported by 1.43.0; it is simply no longer a *key of the runtime `icons` record*, and `Trash2 === Trash` there (in 1.31.0 they were distinct objects). That is exactly the class `scripts/check-lucide-icon-record-names.mjs` exists to catch: a retired spelling still imports, still type-checks, still renders as a COMPONENT, and resolves to nothing as a STRING. Four authored sites resolve `trash-2` through `renderers/action/resolve-icon.ts`, which reads record membership: - `examples/schema-catalog/src/schemas/actions/action-button-variants.json` - `examples/schema-catalog/src/schemas/actions/confirmation-dialog.json` - `examples/schema-catalog/src/schemas/components-layout-card/profile-detail-card.json` - `packages/plugin-detail/src/DetailView.tsx` — the record delete action All four now write `trash`. **The glyph does not change:** 1.43.0's `trash` icon node is byte-identical to 1.31.0's `trash-2` icon node (control: `pencil` is byte-identical across both versions, so the comparison is not returning equal for everything). Upstream replaced the old bin artwork under the `trash` name and kept `trash-2` as a deprecated alias. ### 3. Why 1.43.0 and not the 1.41.0 the Dependabot PR advertises The workspace declares a caret range, so a re-lock from current `main` resolves the newest 1.x — that is 1.43.0 today, under `^1.41.0` just as much as under `^1.43.0`. Holding exactly 1.41.0 would need an exact pin or an override, which is not this repo's convention and not mine to invent. Measured, the extra two minors cost nothing in removals: **1.41.0 to 1.43.0 removes zero exports, zero record keys and zero dynamic names.** It does bring one behavioural change that 1.41.0 does not have, and it is worth a maintainer's eye: **1.43.0 reshaped `createLucideIcon`** to take an icon-data object carrying `aliases`, and it now emits **one CSS class per retired alias**. A spinner renders `class="lucide lucide-loader-circle lucide-loader-2 ..."`. Measured: 248 of 1818 icon modules carry an alias; 1.31.0 icon modules have no `aliases` field at all. This is additive, so a `.lucide-loader-circle` selector still matches — but a selector asserting the exact, complete class string does not, and neither does an assertion that a given `lucide-` class is ABSENT. I censused the whole tree for both shapes, using the alias index built from the installed artefact as the oracle: **28 distinct `lucide-` class names are referenced anywhere in the repository** (all file types, snapshots included) and **exactly two are aliases in 1.43.0** — `lucide-home` (owned by `house`) and `lucide-loader-2` (owned by `loader-circle`). Controls: injecting the known alias `trash-2` into the census is found; `check` is correctly absent. Both affected pins are repaired here: - `examples/schema-catalog/test/svg-host-dom-leak-5632.test.tsx` asserts the exact attribute set. The added class is named. The assertion stays **exact** on purpose — that list is what catches an authored prop reaching the DOM, which is the whole point of objectui#5632. - `packages/components/src/renderers/form/__tests__/button-shared-icon-resolver.test.tsx` proved `home` resolved through the rename to `House` by asserting `svg.lucide-home` is null. `house` now emits `lucide-home` itself, so that class no longer says which glyph was drawn. Replaced with a form that still fires for the same defect: exactly one svg, and the alias class on that same element. Had the rename been dropped, `Home` is not a record key and the button would render no glyph at all. ### 4. Something the bump gives back `main` installs **two** physical copies of the icon library: 1.31.0 for this workspace and 1.35.0 pulled by `fumadocs-ui`. On this branch both collapse onto a single 1.43.0. Lockfile resolutions go 1822 to 1821. ### 5. What was measured here - `pnpm check:icon-record-names` — `OK ... 184 authored/declared names reaching 1 record-reading resolver are live icons keys (record 1818 keys)`. - Reverse verification, direction predicted before running: restoring `trash-2` in `DetailView.tsx`'s `sys_delete` action turns that gate red naming **exactly that one site** and nothing else; restoring the file returns it to green. Restoration proved by blob hash equality with HEAD and an empty `git diff HEAD`, not by an exit code. - `pnpm type-check` across the workspace — turbo `81 successful, 81 total` over 45 packages, zero `error TS`. - `pnpm exec vitest run packages/components/ examples/schema-catalog/ packages/plugin-view/ packages/plugin-list/` — `Test Files 395 passed (395)`, `Tests 5688 passed (5688)`. - `pnpm exec vitest run packages/plugin-detail/ packages/types/ examples/schema-catalog/` — 350 files, 6769 tests, green after the pin repair. - `pnpm exec vitest run packages/cli/src/__tests__/app-generator.test.ts` — 43 passed. - `pnpm exec vitest run scripts/__tests__/doc-version-claims.test.ts` — 29 passed. - `pnpm exec vitest run scripts/__tests__/check-lucide-icon-record-names.test.ts scripts/__tests__/ci-cd-pipeline-doc.test.ts scripts/__tests__/check-lockfile-integrity.test.ts` — green. - `node scripts/check-lockfile-integrity.mjs` — `VERDICT clean`. - `node scripts/check-changeset-presence.mjs` and `check-changeset-no-major.mjs` — both green. Declared narrowing: the full four-shard `pnpm test` and the repo-wide `pnpm lint` are left to CI. `eslint` over the changed source files reports one pre-existing error in `action-bar.tsx` (`react-hooks/static-components`) in code this PR does not touch (its only change to that file is a docblock comment); that attribution is stated, not yet controlled against `main`. The `Bundle Analysis` measurement was still building locally when this was opened and is reported as NOT MEASURED rather than assumed — the stale-base cause of its red is independently established above. ### 6. Governed surface — this PR stays a draft `node scripts/check-governed-queue-guard.mjs --test` reports `GOVERNED — 1 of 37 path(s)`: `skills/objectui/guides/plugin-development.md`, the published skills catalog. One governed path governs the whole PR. It is **not** to be flipped ready, enqueued, or armed with auto-merge; a human merge is the review record. The changed line there is the plugin skeleton's `lucide-react` range, which `doc-version-claims.test.ts` anchors to the in-repo range. Its `KNOWN_CLAIMS` entry is keyed by the literal text, so the inventory key moved with it. --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <support@objectstack.ai> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Jack Zhuang <50353452+hotlong@users.noreply.github.com>
Part of objectui#9215 — step B of objectui#8333's ruling. Deliberately not a closing reference: leg 3 of triage's grading fires (see below), so the merge is a maintainer decision, and the card stays open until that decision is made.
⛔ This PR stays DRAFT. Not flipped ready, not enqueued, no auto-merge.
What is in the diff
Two files, nothing else:
pnpm-lock.yaml(the dedupe) and one empty-frontmatter changeset declaring no release. The whole point of the dedicated PR is that the 47 collapsed identities are not misattributed to an unrelated bump — that is what objectui#8333 is blocked on.Triage stated plainly that it re-ran none of it. Re-derived here on
d7f0601ee7(2026-09-12),pnpm@10.31.0, Node v22.22.2, dedicated worktree offorigin/main, nothing else edited. No figure differs from the card.The lockfile itself has not moved in the four days: its sha256 on
d7f0601ee7ise4e3a203f2f2686daca282323ab5cd222ccea27c56cf28f81a3ea984e3b88ccd, byte-for-byte the reading the card took ona94e4d073a.1. The byte-identical-reproduction control, both directions, same run
Negative leg —
pnpm install --lockfile-onlyon the unmodified base reproduces the committed lockfile byte-identically:Positive leg, run against the same tree — a strict one-byte append (701,203 -> 701,204 bytes, sha256 moves) makes the same command print:
⇒ the empty reading above is a measurement, not a broken command. The mutation script carried a
trap ... EXIT INT TERMrestore; restoration proved bygit diff HEAD --statat 0 bytes and the sha256 back toe4e3a20….2. The collapse
Under the identity count, snapshot keys move
60 removed / 11 added. The 11 added are peer-suffix re-spellings of identities that survive (vitest,@vitest/mocker,@vitejs/plugin-react,fumadocs-*,vite-plugin-dts,unplugin-dts,fdir), not new packages — which is whyidentities addedis still 0.0 addedis the load-bearing half: nothing moves forward that was not already in the tree. Checked mechanically — all 47 dropped versions have a strictly higher surviving sibling of the same name,0do not, so no consumer can move backward.The repo's own gate agrees, as the card predicted:
3. Zero declarations, zero ranges, zero overrides — verified, not assumed
git statuson the dedupe shows one modified file,pnpm-lock.yaml. Nopackage.json, nopnpm-workspace.yaml.overrides:,settings:andpatchedDependencies:sections are byte-identical (same sha256 both sides).lockfileVersionunchanged, same seven top-level sections both sides.importers:(same 2834 lines both sides): 0specifier:lines change, 114version:lines change, 0 other lines change.⇒ every declared range stays exactly where it was; only resolutions move.
4. ⛔ Leg 3 FIRES — all eight identity-sensitive packages are hit
Triage named
react-router/react-router-dom, the five@radix-ui/react-{slot,primitive,id,compose-refs,use-layout-effect}andmotionas the cases where two copies collapsing into one can change behaviour, not just bytes. Every one of them collapses here. Naming them explicitly, with who held the copy that disappears:react-routerreact-router-dom@7.18.0onlyreact-router-dompackages/plugin-designerimporter@radix-ui/react-slot@radix-ui/react-primitive@2.1.6@radix-ui/react-primitivecmdk@1.1.1@radix-ui/react-idcmdk@1.1.1@radix-ui/react-compose-refs@radix-ui/react-slot@1.3.0,cmdk@1.1.1@radix-ui/react-use-layout-effect@radix-ui/react-id@1.1.2motionfumadocs-ui@16.15.4(the docs site)Two readings that bear on the decision:
react-router-domis declared^7.18.2by six manifests (and^6.0.0 || ^7.0.0as a peer by four),@radix-ui/react-slot^1.3.3,motion^13.2.0. The disappearing copies come from third-party subtrees —cmdk@1.1.1for the five radix primitives,fumadocs-uiformotion, and theplugin-designerpeer range forreact-router-dom@7.18.0, which does not satisfy the repo's own^7.18.2.⇒ answering the card's question 1: accretion, not intent. No declaration, range or override selects any dropped copy; all 47 are strictly older siblings left behind by partial re-resolves. ⛔ Ruling the merge is not this seat's to make — that is the maintainer's, which is why this PR is draft.
5.
Bundle Analysisdelta, in isolationBoth sides measured on this box, identical procedure, the only difference being the lockfile: clean install,
pnpm turbo run build --filter='./packages/*' --concurrency=2(--forceon the second run, 0 cached),pnpm --filter @object-ui/console build, thennode scripts/check-eager-closure-budget.mjs.Entry chunk (the 350 KB gzip line):
Eager closure:
Per eagerly-loaded chunk, gzipped:
check:eager-closureexits 2 on the deduped tree. The aggregate closure is better and every per-chunk ceiling still passes; what reds is the ceiling-sensitivity leg onui-components:That row was already declared exhausted before this change — its pinned allowance exists precisely because it sits under the 0.10x floor — and the checker's own verdict text says the two edits that would turn it green are both forbidden: ⛔ never raise the ceiling, ⛔ never raise the allowance. Neither is done here. Paying the row down is the open decision the checker points at, objectui#7848 (open,
pm:blocked,domain:devx); it is taken there and not folded into this diff, per the checker's own instruction.On attribution, stated honestly in both directions: the checker warns that this row drifts under unrelated traffic and that the named amount is very likely not the author's. Here the two builds differ only by the lockfile, so the +1.7 KB is this change's. The likely mechanism is not a shipped dependency — the⚠️ That is an inference from the chunk rule plus the input set, not something this PR pins byte-by-byte.
ui-componentschunk rule matchespackages/(components|fields)workspace source only, and that source is byte-identical across the two builds. What did move is the bundler that emitted those dists:vite8.2.1 -> 8.2.2,rolldown1.2.3 -> 1.2.7 and its 13 platform bindings all collapse in this dedupe, so the workspace dists are re-emitted by a different codegen.6. The full list — 45 names whose copy count collapsed
The five
@radix-ui/*rows above are also identity-sensitive; they are marked in the table in section 4 rather than repeated here.Workspace importers whose direct resolution moves:
vite8.2.1 -> 8.2.2 (23 importers, alldevDependenciesexceptpackages/cli),lucide-react1.31.0 -> 1.35.0 (18 importers),react-router-dom7.18.0 -> 7.18.2 (packages/plugin-designer). Everything else is transitive.Scope
⛔ No gate or report that tracks the duplicate count over time is built here — triage ruled that a different card, to be filed once this lands if it still matters. The existing
check-lockfile-integritygate is correct as designed (objectui#8326 watches for copies being gained) and is untouched.Gates run locally
Exit codes captured before any pipe.
🤖 Generated with Claude Code
https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
Generated by Claude Code