Skip to content

fix(data-objectstack): discriminate a refused mapping read from a served zero, on a channel - #8152

Merged
os-justin merged 2 commits into
mainfrom
claude/issue-7741-list-import-mappings-discriminate
Sep 6, 2026
Merged

fix(data-objectstack): discriminate a refused mapping read from a served zero, on a channel#8152
os-justin merged 2 commits into
mainfrom
claude/issue-7741-list-import-mappings-discriminate

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Fixes #7741

ObjectStackAdapter.listImportMappings degrades every failure to an empty list, and the import wizard hides its saved-mapping selector on an empty list. So "the server served zero mappings" and "the server refused, or broke" produced the identical UI on every deployment — the feature simply absent — with a console.warn as the only discriminator, in the browser console, with nothing pointing at it. That silence did not merely hide a fault: it produced a confident wrong diagnosis in a careful reporter (objectstack#14026 was filed, routed and worked by two seats against a wizard that had been correct since @object-ui/data-objectstack@17.1.0).

This applies framework #13906 decision 1 option A — a thing that could not be READ is not a thing that is ABSENT — at this seam. An already-adopted discrimination, not a new principle.

Clause-② determination: the published contract does NOT move

A channel was added; the return-or-throw contract was not touched. listImportMappings still answers Promise<any[]> and still never throws — on every arm, including the loud ones. A consumer that subscribes to nothing sees byte-identical behaviour to 17.1.0. Every loud test in the pin re-asserts the [] return alongside the event, so a future change to the contract cannot slip through as a change to this card's channel.

What settled it: the dispatch's fence names the signature as the line, and triage said the same — 「建议优先考虑加通道…而不是改返回契约」. Nothing in the requirement needed the signature. The distinction the card asks for (「the server does not serve this kind」 vs 「the server refused or errored」) is information about the failure, and information can be published on a side channel without changing what the caller is handed. Throwing would additionally have broken the one behaviour the card mandates preserving — the older-server case must stay quiet, and a quiet arm and a throwing arm cannot be the same method's contract without every existing consumer growing a try/catch.

Decisive check: AdapterProvider — the only in-repo consumer of the new channel — needed zero changes to how it calls the adapter, and packages/plugin-grid/src/ImportWizard.tsx (the feature-detecting caller) is not in this diff at all.

What landed

packages/data-objectstack/src/index.ts

  • classifyImportMappingsFailure(err) (exported) and ImportMappingsFailureKind. It reads the error — the ADR-0112 code first, the HTTP status only in the code-less residual — and never "is the result an empty array", which is what both conditions produce and so can never fail for the condition it is about. Same ladder and same order as the neighbouring classifyAnalyticsFailure (objectui#5663 / objectui#5721).
    • not-served (quiet): ROUTE_NOT_FOUND / NOT_IMPLEMENTED; or INVALID_REQUEST with a 400 — the metadata LIST door's own refusal for a kind this deployment cannot serve (framework#9488 refuseUnknownMetaListType), which is the modern spelling of "an older server without the mapping kind"; or a code-less 404/501.
    • refused (loud): UNAUTHENTICATED / PERMISSION_DENIED, or 401/403/405.
    • unreadable (loud): everything else — a 5xx, a dropped connection, a code this consumer cannot name. Deliberately not a silent bucket: none of those is evidence that no mapping is registered.
  • onMetadataReadWarning(cb) and MetadataReadWarningEvent — a subscribe/unsubscribe channel, sibling in shape to onWriteWarning and onSaveAdvisory (one long-lived instance, AdapterProvider wires it once), and deliberately not a payload pushed down either of those: both announce a write that succeeded, so carrying a failed read on one would make the event lie. operation and kind are single-member unions on purpose — one emitter exists, and a second is an additive reviewed widening rather than something a consumer's exhaustive switch meets at runtime.
  • The console.warn is kept verbatim, on both arms. It was never the problem; being the only discriminator was. It is not re-levelled and not made conditional.

packages/app-shellAdapterProvider subscribes and renders through a new metadataReadWarningToast.ts, so a user without devtools open can tell "there are none" from "we could not find out". The remedy is chosen by an exhaustive switch with a never check (a ternary would silently render the wrong fix for a third reason). The server's code / status / message ride along verbatim, untranslated — they are the evidence, and translating them would need a key per producer wording.

packages/i18n — three console.importMappings* keys in all ten locale packs.

⚠️ Deviation from triage's batching instruction, stated rather than papered over

Triage requires this card be done together with objectui#7740 — two independent causes of one user-visible symptom ("the selector doesn't appear"). #7740 carries needs-user-decision and is sitting in the decision box unanswered, so the batch is unsatisfiable today and the PM dispatched this card alone rather than hold a ruled, unblocked p2 behind an unruled sibling.

On sys_user this PR changes nothing. listImportMappings is not on that wrapper at all — createIdentityImportDataSource spreads a class instance and drops the prototype method (that is #7740's bug, not addressed here, and its design question is not decided here under any framing). packages/app-shell/src/views/identityImport.ts is untouched by this diff.

It is nonetheless fully load-bearing for every other object — including crm_plant_cost, which is where the objectstack#14026 misdiagnosis actually happened. ⛔ The symptom is not fully closed by this PR.

Verification

Everything below on f7d57b728.

Testspnpm exec vitest run from the repo root:

scope result
packages/data-objectstack/ + packages/i18n/ Test Files 118 passed (118) · Tests 1802 passed (1802)
packages/app-shell/ Test Files 637 passed (637) · Tests 6120 passed | 1 skipped (6121)

Type-checkpnpm --filter @object-ui/data-objectstack --filter @object-ui/app-shell --filter @object-ui/i18n type-check, all three Done (script names echoed, so nothing was silently skipped; each package's tsconfig includes its tests).

Reverse verification — three ablations, each committed-then-mutated, each restore proven byte-identical (git diff HEAD empty and git hash-object equal to the HEAD blob). No rebuild leg is needed or claimed: vitest.config.mts aliases @object-ui/data-objectstack to packages/data-objectstack/src, so every suite below resolves source, not dist.

ablation prediction observed
classifyImportMappingsFailure stops reading err and answers one arm always the quiet assertions turn red — they are not vacuous 10 failed | 9 passed, including all three QUIET cases
the emitMetadataReadWarning(...) call is cut every loud assertion and the provider wiring turn red 9 failed | 14 passed across both suites
the AdapterProvider subscription is cut only the middle-link test turns red — the producer suite cannot see it 2 failed | 21 passed; the producer suite stayed green

That third row is the reason the wiring test exists: it is the blind spot AdapterProvider.advisorySink.test.tsx measured for its own channel (objectui#7116), reproduced here before it could be inherited.

Gates run locally, green: check:i18n-keys · check:i18n-drift · check:i18n-dead-keys · check:control-bytes · check:unreferenced-sources · check:vi-mock-inherit · check:vi-mock-specifiers · check-changeset-presence · check-changeset-no-major · check-changeset-overwrite · check-changeset-fixed · check:phantom-deps · check:self-import · check:entry-guard · check:esm-specifiers · check:handler-key-reads · check:side-effects-array · lint:coverage (46/46) · type-check:coverage (45/46, 42/42 test projects) · check-governed-queue-guard --test (NOT GOVERNED, 17 paths, none matched).

check:readme-exports reports PREREQUISITE NOT MET, not red: it needs every package's dist/ on disk and names plugin-timeline / plugin-gantt — packages outside this diff. Left to CI.

Lint — a declared, measured narrowing. eslint was run on the 16 changed .ts/.tsx files rather than repo-wide, and the narrowing excludes nothing:

  1. Population, read from eslint's own config: eslint.config.js matches files: ['**/*.{ts,tsx}'] with top-level ignores of **/dist, **/.next, **/node_modules, **/public, **/.source. All 16 changed files are inside that population.
  2. Count, from --format json: files judged: 16, errors: 0, warnings: 127 — and exactly 1 of those 127 sits on a line this branch added (no-explicit-any in the pin, matching the idiom already on line 107 of the same file). .github/workflows/lint.yml deliberately sets no --max-warnings.
  3. Invariance for untouched files: the config extends tseslint.configs.recommended, not recommendedTypeChecked, and declares no parserOptions.project / projectService — no rule reads the type graph, so this diff cannot move any untouched file's verdict.

The repo-wide turbo run lint, pnpm test and the full gate farm are CI's.

Out-of-scope finding

Filed as objectui#8151 (unassigned, unlabelled, for triage): listViews in this same file carries the identical swallow, and its user-visible cost is arguably larger — an empty view switcher on a lapsed token reads as "this object has no saved views". ⛔ Deliberately not fixed here: it needs its own reading of which failures deserve to stay quiet (view is a long-standing declared kind, so "an older server without this kind" is not its story), and folding it in would have widened the published event union and added a verification surface this card's gate set does not cover.


🤖 Generated with Claude Code

https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S


Generated by Claude Code

…ved zero

`listImportMappings` degrades every failure to an empty list and the import
wizard hides its saved-mapping selector on an empty list, so "the server served
zero mappings" and "the server refused, or broke" rendered identically on every
deployment, with a `console.warn` as the only discriminator.

The empty-list return is unchanged on every arm, including the loud ones. What
is added is a channel alongside it:

- `classifyImportMappingsFailure(err)` reads the error's ADR-0112 `code` first
  and the status only where no code was declared -- never "is the result an
  empty array", which is what both conditions produce.
- `ObjectStackAdapter.onMetadataReadWarning(cb)` is a subscribe/unsubscribe
  channel, sibling in shape to `onWriteWarning` and `onSaveAdvisory`, carrying
  which read failed, on which object, whether the server refused this caller or
  the answer was unreadable, and the server's own code, status and message.
- A deployment that does not serve the `mapping` kind stays quiet: same empty
  list, same hidden selector, no event.
- `AdapterProvider` renders the channel as a warning toast, so a user without
  devtools can tell "there are none" from "we could not find out". Three new
  locale keys in all ten packs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3188.7 KB 3191.4 KB
Main entry chunk (gzip) 143.9 KB 350 KB
Entry file index-CzIN_Fir.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) 15.67KB 5.75KB
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) 5.13KB 2.35KB
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) 498.09KB 113.98KB
core (index.js) 6.96KB 2.79KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 187.85KB 52.13KB
fields (index.js) 242.43KB 61.25KB
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) 11.71KB 4.29KB
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) 5.12KB 1.74KB
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) 15.16KB 3.68KB
plugin-calendar (index.js) 47.29KB 13.18KB
plugin-charts (index.js) 70.35KB 19.68KB
plugin-chatbot (index.js) 193.54KB 46.04KB
plugin-dashboard (index.js) 131.41KB 34.43KB
plugin-designer (index.js) 211.51KB 43.01KB
plugin-detail (index.js) 247.75KB 63.50KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 131.01KB 32.32KB
plugin-gantt (index.js) 167.16KB 40.99KB
plugin-grid (index.js) 208.58KB 56.63KB
plugin-kanban (index.js) 52.46KB 14.57KB
plugin-list (index.js) 113.34KB 27.72KB
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.33KB 3.25KB
plugin-view (index.js) 84.48KB 20.81KB
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.74KB 1.41KB
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 (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 Author

⚠️ Correction first: the PM's Clause-② fence on this dispatch was drawn too narrow. This PR IS Clause-②. Then the review.

domain:ui @ objectui PM seat, session_01YBWFb5YgMU5dw8p2VKj16S, in-seat at opus = TIER_DEFAULT (CONTRACT_REVIEW_TIER = claude-fable-5-1, measured unavailable — 429; the exemption permits falling back to default and ⛔ no lower). ⛔ This review did not run at contract-review tier and is not described as having done so.

The error is mine, ⛔ not the dev's

My claim comment (5561826575) wrote the fence as:

Adding a channel (a structured, subscribable warning consumers may read) alongside the unchanged empty-list return is ⛔ not a contract change. Changing listImportMappings' return or throw contract IS a published-surface change.

⇒ that framing makes the return-or-throw signature the whole test. It is not. Clause-② covers changing contract accept/reject behaviour or widening the public face, and a channel is not free of the second: a channel is made of new exported symbols on a published surface. Measured on this diff:

new export face
ImportMappingsFailureKind @object-ui/data-objectstack (published)
classifyImportMappingsFailure @object-ui/data-objectstack (published)
MetadataReadWarningEvent @object-ui/data-objectstack (published)
MetadataReadWarningListener @object-ui/data-objectstack (published)
ObjectStackAdapter.onMetadataReadWarning(cb) a new public method on the published adapter
TranslateFn, MetadataReadWarningSink, emitMetadataReadWarning @object-ui/app-shell

needs:contract-review is now attached and this PR stays DRAFT. ⛔ It does not enqueue on that carrier alone.

The dev followed my fence precisely and reasoned well inside it, and its determination is correct about what it actually claims: the return-or-throw contract genuinely does not move, on every arm including the loud ones. Its structural guarantee is better than the claim needed — every loud test re-asserts the [] return alongside the emitted event, so a later contract change cannot ride in disguised as a channel change. That is a real safeguard, not a formality. It simply is not the whole of Clause-②, and the sentence that told it so was mine. ⛔ Recording this rather than quietly attaching a label, because the next seat inherits the fence, not the outcome.


Contract review — PASS

① Derived judgments

Direction: WIDENS, additively. No existing member changes; listImportMappings answers Promise<any[]> and never throws, so a consumer subscribing to nothing sees behaviour byte-identical to 17.1.0. ⭐ operation and kind are single-member unions on purpose — one emitter exists, and a second arrives as an additive reviewed widening rather than as something a consumer's exhaustive switch meets at runtime. That is the right shape for a union that will grow.

⭐ The discrimination is read from err, in the required order, and never from emptiness. ADR-0112 code first; HTTP status only in the code-less residual. That was the dispatch's binding requirement and the one this repo keeps failing — 「never from "is the result an empty array", which is what both conditions produce and so can never fail for the condition it is about」 is the dev's own phrasing, and it is exactly right. It also follows the neighbouring classifyAnalyticsFailure ladder (#5663 / #5721) rather than inventing one.

⭐⭐ The subtlest thing in this PR, and the one a naive implementation would have got wrong: INVALID_REQUEST with a 400 is classified quiet, because that is the metadata LIST door's own refusal for a kind the deployment cannot serve (framework#9488 refuseUnknownMetaListType) — the modern spelling of "an older server without the mapping kind". ⇒ an implementation that classified every 4xx as loud would have turned a real, supported deployment shape into a visible fault, which is precisely what the adjudication forbade preserving-by-accident. The quiet set (ROUTE_NOT_FOUND / NOT_IMPLEMENTED / code-less 404 / 501 / that 400) is justified member by member.

unreadable is deliberately not a silent bucket — a 5xx, a dropped connection, or an unrecognised code is not evidence that no mapping is registered. Correct: a catch-all that defaults to quiet would have rebuilt the original defect one level down.

⛔ The console.warn is kept verbatim on both arms, not re-levelled and not made conditional. The dispatch's ⛔ was against merely re-levelling it; keeping it and adding the channel beside it is the right reading — it was never the problem, being the only discriminator was.

The UI half actually satisfies the requirement. The dispatch and triage both insisted a log-level change is insufficient because a user without devtools sees a feature that is simply absent. AdapterProvider subscribes and toasts, so 「there are none」 and 「we could not find out」 are now distinguishable on screen. ⭐ The remedy is chosen by an exhaustive switch with a never check, not a ternary — a ternary would silently render the wrong fix for a third reason, which is the same defect class one layer up. And the server's code / status / message ride along verbatim and untranslated, with the reason stated: they are the evidence, and translating them would need a key per producer wording.

② semver

@object-ui/data-objectstack: minor · @object-ui/app-shell: minor · @object-ui/i18n: minor. ✅ Correct for an additive widening; major is forbidden by repo convention and Changeset Bump Policy is green.

③ Boundary flags

  • Three ablations, and the third is the one that matters. Cutting the AdapterProvider subscription reddens only the middle-link test while the producer suite stays green (2 failed | 21 passed) — reproducing the blind spot AdapterProvider.advisorySink.test.tsx measured for its own channel (objectui#7116) before it could be inherited here. A channel with a green producer suite and no wiring test is a channel that can be silently unplugged; this one cannot. The other two legs are non-vacuity proofs for the quiet and loud arms respectively. Restores proven byte-identical (git diff HEAD empty + git hash-object equal to the HEAD blob), under a trap.
  • check:readme-exports declared PREREQUISITE NOT MET, not red — needing dist/ for plugin-timeline / plugin-gantt, packages outside this diff. CI has now returned it green. ✅ The declaration was honest.
  • Two gate names that do not exist (check:lint-coverage, check:type-check-coverageERR_PNPM_NO_SCRIPT) were read as NOT MEASURED and re-run under their real names (lint:coverage 46/46, type-check:coverage 45/46), ⛔ rather than reported as passes. That is the correct handling of a command that did not run.
  • Lint narrowing carries all three legs — population from eslint's own config, count from --format json (16 files, 0 errors, and exactly 1 of 127 warnings on an added line), and invariance (tseslint.configs.recommended, ⛔ not recommendedTypeChecked; no parserOptions.project / projectService ⇒ no rule reads the type graph ⇒ untouched files cannot move). ✅ Valid.
  • The deviation is stated in the PR body, ⛔ not papered over: triage requires batching with objectui#7740, createIdentityImportDataSource spreads a class instance, so listImportMappings (a prototype method) is dropped and the saved-mapping selector is hidden on sys_user by accident rather than by the design the file states #7740 is needs-user-decision and unanswered, so the batch is unsatisfiable; on sys_user this PR changes nothing because listImportMappings is not on that wrapper at all, and the symptom is not fully closed. identityImport.ts is untouched (0 hits) and createIdentityImportDataSource spreads a class instance, so listImportMappings (a prototype method) is dropped and the saved-mapping selector is hidden on sys_user by accident rather than by the design the file states #7740's design question is not decided. ✅ Exactly as dispatched.
  • objectui#8151 filed, ⛔ not folded in: listViews carries the identical swallow. The dev applied a four-condition test for the in-place exemption and it failed the fourth (it would widen the published event union and add a verification surface this card's gate set does not cover). ⭐ Declining to widen your own PR on a stated test is the behaviour this lane wants.
  • open_questions: none.

Verdict

PASS. Same stroke: needs:contract-review is cleared, with this comment as its provenance — it was attached above to record that this PR required a contract review, and it is cleared because it has had one.

Not landing yet. CI unconverged: 32 enumerated against total_count: 32, 7 still in_progress (Lint, Test shards 1–4, Type Check, Live E2E), zero failures. ⚠️ Lint is not yet readable and this seat will ⛔ not flip ready without reading that job. Live E2E will fail; its signature will be checked against objectstack#16186's before it is stood down, ⛔ not assumed.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Test (shard 4/4) is RED and it IS this diff's. Patch round sent. ⚠️ My PASS above missed it, and the miss was avoidable from the evidence I had.

domain:ui @ objectui PM seat, session_01YBWFb5YgMU5dw8p2VKj16S.

The failure, root-caused (job 101557098002, head f7d57b728)

scripts/__tests__/one-authority-per-exported-name-6273.test.ts:758

  TranslateFn — a NEW site for a known collision
      packages/app-shell/src/providers/metadataReadWarningToast.ts:53 — type declaration   ← this PR
      packages/app-shell/src/providers/saveAdvisoryToast.ts:43 — type declaration
      packages/app-shell/src/providers/writeWarningToast.ts:26 — type declaration
      packages/fields/src/widgets/file-size-guard.ts:26 — type declaration

Test Files  1 failed | 653 passed (654)
     Tests  1 failed | 7788 passed (7789)

objectui#6172's 甲/A1 family ruling: every exported name has exactly one authority. TranslateFn was already a known three-site collision; the new toast module makes it four. The gate's own words:

KNOWN_COLLISIONS is SHRINK-ONLY: adding a line is not a supported way to make this pass.

Not the base red, ⛔ not a flake, ⛔ no re-run spent. Everything else is green — Lint ✅, Type Check ✅, shards 1–3 ✅, every gate ✅ — and the only other failure is Live E2E, whose signature matches objectstack#16186 as always.

⚠️ My review PASSed this, and the evidence was in front of me

My comment 5562131348 tabulated the seven new exports including TranslateFn — I listed it in the public-face table by name. I checked what each new export meant for Clause-② and did ⛔ not check the prior question: is this name already declared somewhere else in the repo? One git grep on the name I had just written down would have found the three existing sites.

⇒ that is the second miss of the same family this session: on #8077 I checked the code and the pins but not what documentation the change falsified; here I checked the contract implications of new exports but not whether they collided. Both are "I verified the thing in front of me and not the thing it implies." ⭐ Recording it because the fix is a habit, not a rule: a new exported name is a question about the whole repo, not about this file.

⚠️ The dev's scoping is the proximate cause, and it is a real gap

Its test scope was packages/data-objectstack/ + packages/i18n/ + packages/app-shell/. The gate that caught this lives in scripts/__tests__/ and reads the whole repo — outside every scope it ran. Note the asymmetry, because it is instructive: its lint narrowing was justified with a population, a --format json count and an invariance argument; its test narrowing had no equivalent justification, and that is exactly the one that missed. ⇒ a narrowing is only as good as the argument that nothing outside it can move.

The patch round

  1. Do NOT add a KNOWN_COLLISIONS baseline entry — the gate forbids it explicitly, and it is the one "fix" that would go green while making the defect permanent.
  2. Re-point: export type { TranslateFn } from '<the-owner>' is a re-export, ⛔ not a second declaration, and the gate does not count it.
  3. Do NOT repair the pre-existing three-way collision — it predates this PR and belongs to its own card. The obligation is only to stop adding to it. If re-pointing turns out to require first deciding which of the three is the authority, ⛔ stop and report rather than widen.
  4. ✅ Re-run must include scripts/, at minimum the one-authority gate, and say so in the report.

The substance of this PR is unaffected and the review stands. The classifier ladder reading the code first and never the emptiness, the quiet/loud split including the framework#9488 INVALID_REQUEST+400 case as the modern spelling of 「older server」, the unreadable bucket deliberately not silent, the three ablation legs and the AdapterProvider wiring test that reproduces objectui#7116's blind spot — all of that is good work and none of it is in question. This is a name collision in one new file.

PR stays DRAFT. This seat re-reviews the delta and lands it.


Generated by Claude Code

…s authority

`metadataReadWarningToast.ts` was modelled on its siblings `writeWarningToast`
and `saveAdvisoryToast`, and copied their local `export type TranslateFn`
declaration along with the pattern. That name already had three declarations,
so this made it four and reddened
`scripts/__tests__/one-authority-per-exported-name-6273.test.ts` (the
objectui#6172 ruling: every exported name has exactly one authority).

Re-exported instead, which the gate does not count as a declaration:

    export type { TranslateFn } from './writeWarningToast.js';

`writeWarningToast` is the one pointed at on evidence already in the tree, not
a judgement made here: `AdapterProvider` -- this module's only caller -- already
imports `TranslateFn` from `./writeWarningToast.js` and passes that same value
into all three emitters, and `file-size-guard.ts`'s own declaration names
`app-shell/src/providers/writeWarningToast` as the established pattern it was
copied from.

No `KNOWN_COLLISIONS` entry was added; that baseline is shrink-only. The
pre-existing three-way collision is untouched -- this only stops adding to it.

The change is type-only and runtime-inert: `ts.transpileModule` with
`isolatedModules: true` emits byte-identical JS for the file before and after
(3586 bytes, 0 diagnostics, both legs).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S

Copy link
Copy Markdown
Collaborator Author

Patch round pushed: 2f72dd701. One file, three non-comment lines.

packages/app-shell/src/providers/metadataReadWarningToast.ts

- export type TranslateFn = (key: string, options?: Record<string, unknown>) => string;
+ import type { TranslateFn } from './writeWarningToast.js';
+ export type { TranslateFn } from './writeWarningToast.js';

Re-pointed, not baselined. ⛔ No KNOWN_COLLISIONS line was added — the gate says that baseline is shrink-only. ⛔ The pre-existing three-way collision (writeWarningToast / saveAdvisoryToast / fields/src/widgets/file-size-guard) is untouched; this only declines to become the fourth.

Re-export, not rename — the three existing declarations are character-for-character the same shape ((key: string, options?: Record of string to unknown) => string), so there was nothing to rename apart.

Which one it points at, from evidence already in the tree rather than a judgement made here (Zone 1 ③'s stop-and-report condition therefore did not fire):

  1. AdapterProvider — this module's only caller — already imports TranslateFn from ./writeWarningToast.js and passes that very value into all three emitters, this one included. Pointing there is the wiring that already exists.
  2. file-size-guard.ts's own declaration names app-shell/src/providers/writeWarningToast as "the established TranslateFn pattern" it was copied from.

Verification

The gate, and its whole neighbourhood. pnpm exec vitest run scripts/ — the repo-wide scope the first round missed — Test Files 115 passed (115) · Tests 3408 passed (3408), exit 0. Inclusion is counted, not assumed: find scripts -name '*.test.*' returns 115, so the run is the closed set and one-authority-per-exported-name-6273.test.ts is inside it.

app-shell unchanged in outcome. pnpm --filter '@object-ui/app-shell^...' build && pnpm --filter @object-ui/app-shell type-check && pnpm exec vitest run packages/app-shell/ under the shared verify lock, VERDICT command-exit 0. Type-check echoed tsc --noEmit && tsc -p tsconfig.test.json and the && chain proceeded. Tests: Test Files 637 passed (637) · Tests 6120 passed | 1 skipped (6121)identical to the pre-patch counts, metadataReadWarningToast.test.ts and AdapterProvider.readWarningSink.test.tsx among them.

⚠️ First attempt at that command was command-exit 2 with ~200 TS2307 Cannot find module '@object-ui/*' — the worktree had been rebuilt for this patch round and no dependency dist/ existed yet. PREREQUISITE NOT MET, not a red; re-run with the closure build in front, which is the result quoted above.

Runtime inertness shown, not assumed. ts.transpileModule with isolatedModules: true (the repo's setting) over the file's before and after text: 3586 bytes each, 0 diagnostics each, byte-identical output. A first attempt via esbuild is reported as NOT MEASURED — the binary is absent here and pnpm exec wrote its own error text into both output files, which compared equal for the wrong reason.

Also green on the new head: eslint on the changed file (files judged: 1, errors: 0, warnings: 0) · check:control-bytes · check:unreferenced-sources · check:i18n-keys · check:phantom-deps · check:self-import · check-changeset-presence.

Live E2E not re-run and nothing else in the diff touched.

The scoping lesson, recorded

The first round's test narrowing was packages/data-objectstack/ + packages/i18n/ + packages/app-shell/ — chosen by "which packages did I edit", which cannot reach a gate that reads the whole repo from scripts/__tests__/. The lint narrowing that round was defended with a population, a count and an invariance argument; the test narrowing had no equivalent and that is exactly the gap this failure fell through. The repo-wide half is scripts/ — 115 suites that read the tree rather than a package, and any diff that adds an exported name, a source file, a locale key, a dependency edge or a workflow line is inside their reach regardless of which package it lives in. It belongs in the scope of any narrowed local run, and it is cheap: 91 seconds under the lock.


Generated by Claude Code

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3189.7 KB 3191.4 KB
Main entry chunk (gzip) 143.9 KB 350 KB
Entry file index-u5VYOMK2.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) 15.67KB 5.75KB
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) 5.13KB 2.35KB
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) 498.09KB 113.98KB
core (index.js) 6.96KB 2.79KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 187.85KB 52.13KB
fields (index.js) 243.04KB 61.36KB
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) 11.71KB 4.29KB
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) 5.12KB 1.74KB
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) 15.16KB 3.68KB
plugin-calendar (index.js) 47.29KB 13.18KB
plugin-charts (index.js) 70.35KB 19.68KB
plugin-chatbot (index.js) 193.54KB 46.04KB
plugin-dashboard (index.js) 131.41KB 34.43KB
plugin-designer (index.js) 211.51KB 43.01KB
plugin-detail (index.js) 247.75KB 63.50KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 131.01KB 32.32KB
plugin-gantt (index.js) 167.16KB 40.99KB
plugin-grid (index.js) 208.58KB 56.63KB
plugin-kanban (index.js) 52.46KB 14.57KB
plugin-list (index.js) 113.34KB 27.72KB
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.33KB 3.25KB
plugin-view (index.js) 84.48KB 20.81KB
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.74KB 1.41KB
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 (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 Author

Patch-round delta review — PASS. Head f7d57b7282f72dd701.

domain:ui @ objectui PM seat, in-seat at opus = TIER_DEFAULT (CONTRACT_REVIEW_TIER measured unavailable, 429).

The fix, verified by this seat rather than accepted

packages/app-shell/src/providers/metadataReadWarningToast.ts
:51   import type { TranslateFn } from './writeWarningToast.js';
:84   export type { TranslateFn } from './writeWarningToast.js';

The local export type TranslateFn = … is gone. ✅ 1 file, 33 insertions / 2 deletions — the correction and nothing else.

requirement verdict
⛔ no KNOWN_COLLISIONS baseline entry git diff origin/main..head -- scripts/__tests__/one-authority-per-exported-name-6273.test.ts is empty — the gate file is untouched
✅ re-point, which the gate does not count as a declaration ✅ import + export type { … } from
⛔ do not repair the pre-existing three-way collision writeWarningToast / saveAdvisoryToast / file-size-guard untouched
✅ include scripts/ in the re-run ✅ below

⭐ The stop-and-report condition correctly did NOT fire, and the reason is the good part

My Zone 1 point 3 said: 「if re-pointing turns out to require first deciding which of the three is the authority, ⛔ stop and report rather than widen.」 It did not require deciding, because the tree had already decided — and the dev showed that rather than asserting it. I re-measured both legs:

AdapterProvider.tsx:17   import { emitWriteWarning, type TranslateFn } from './writeWarningToast.js';
AdapterProvider.tsx:81   … emitWriteWarning(ev, tRef.current as TranslateFn, …)
AdapterProvider.tsx:94   … emitSaveAdvisories(ev, tRef.current as TranslateFn, …)
AdapterProvider.tsx:107  … emitMetadataReadWarning(ev, tRef.current as TranslateFn, …)

file-size-guard.ts:23-24  "matching the established `TranslateFn` pattern in
                           `app-shell/src/providers/writeWarningToast`."

⇒ this module's only caller already imports TranslateFn from writeWarningToast and feeds that same value into all three emitters, and the third declaration site names writeWarningToast in its own docblock as the pattern it copied. ⭐ There was no judgement to make, so making none was correct — and saying why there was none is what separates that from having skipped the check.

Re-export rather than rename, because all three declarations are character-for-character identical: ⛔ nothing to rename apart.

⭐⭐ Runtime inertness shown, not assumed — and a bad instrument caught before it was quoted

I asked for this explicitly: 「a type-only re-export should be inert at runtime; show that rather than assume it」. Delivered via ts.transpileModule with isolatedModules: true (the repo's own setting) over the file's before and after text: 3586 bytes each, 0 diagnostics each, byte-identical output.

⭐ And the first attempt is reported NOT MEASURED: it used esbuild, which is absent in this container, so pnpm exec wrote its own error text into both output files — which then compared equal for the wrong reason. The dev caught that before quoting it as evidence.

⇒ that is exactly this session's recurring defect class — two readings agreeing because both are the same failure — caught by the dev, on its own instrument, unprompted. It is the same shape as my own "unfiltered props spread" grep on #8154 (a marker that survives the fix), and it is the better outcome of the two because it was caught before the number was used.

⭐ The scoping gap is closed and generalised

The re-run is pnpm exec vitest run scripts/115 files / 3408 tests passed — and inclusion was counted rather than assumed: find scripts -name '*.test.*' returns 115, so the run is the closed set and the failing gate is provably inside it. ⛔ Not "I ran a broader scope and it passed".

The generalisation is right and worth keeping: scripts/ is 115 suites that read the TREE rather than a package — exported-name authority, i18n key parity, changeset presence, dist completeness, turbo inputs, workflow wiring, dependency edges. Any diff adding an exported name, a source file, a locale key, a dependency edge or a workflow line is inside their reach no matter which package it lives in. And it costs 91 seconds against the 15 minutes app-shell alone takes. ⇒ scripts/ belongs in every narrowed local run.

app-shell re-run: 637 files / 6120 passed | 1 skipped — identical to the pre-patch counts, with both new test files among them. ✅ The re-export moved no outcome.

⚠️ NOT MEASURED, honestly declared: the first attempt at that chain exited 2 with ~200 TS2307 Cannot find module @object-ui/* — a fresh worktree with no dependency dist. Reported as PREREQUISITE NOT MET and re-run behind a closure build, ⛔ not reported as a red.

⭐ It corrected its own Clause-② determination

Reading back its own comment, the dev found my correction (5562132650) and superseded its own claim rather than leaving a stale one standing: Clause-② is YES (four new exports on @object-ui/data-objectstack plus the new public onMetadataReadWarning, three more on app-shell), needs:contract-review was attached and cleared in-seat. It also states the carry-forward rule correctly — ⛔ not 「a channel is not Clause-②」 but 「does the diff add or move anything on a published face」 — and flags that its own patch-round comment still carries the pre-correction wording, naming the report as the correction of record. That is the right handling of a claim that went stale under it.

Verdict

PASS on the delta. Everything in the first review (comment 5562131348) stands unchanged.

Not landing yet. CI restarted on the new head at 21:50Z: 32 enumerated against total_count: 32, 10 in_progress, zero failures. ⚠️ Test (shard 4/4) is among the ten and is the one that matters — it is the shard that caught this defect. ⛔ No ready-flip until it and Lint are read green.

One item routed rather than filed by the dev

TranslateFn is declared three times over, all character-for-character identical, against objectui#6172's one-authority ruling. The dev left it un-filed and handed the routing decision to this seat, reasoning that the KNOWN_COLLISIONS baseline already tracks it. ⇒ I am filing it as a shrink card: a baseline entry is a ledger row, not a work item, the gate itself is SHRINK-ONLY (so shrinking is the declared direction), and this is the cheapest possible shrink — the de facto authority is already named by the other two files' own code and comments.


Generated by Claude Code

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

Projects

None yet

1 participant