Skip to content

The I18nLabel inverted pin now watches the premise it claims to, and ui-action.ts drops the unused import - #5634

Merged
os-sales merged 2 commits into
mainfrom
claude/issue-5612-i18nlabel-inverted-pin
Aug 21, 2026
Merged

The I18nLabel inverted pin now watches the premise it claims to, and ui-action.ts drops the unused import#5634
os-sales merged 2 commits into
mainfrom
claude/issue-5612-i18nlabel-inverted-pin

Conversation

@os-sales

@os-salesos-sales commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Fixes#5612
Fixes#5613

Folded per the claim on #5612: both cards are residue of the same removed local label / options[].label override in packages/types, each is independently checkable, and one changeset covers both. Precedent: #4559 + #4966 landed as one PR in round 7.

Generic spellings below are flattened to parentheses to survive GitHub's body sanitizer, which strips tag-shaped <...> text — the same convention #5612's own body used. Real syntax is at the cited file and line.

Part 1 (#5612) — the inverted pin that could not fire

The it(...) case in packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts called itself an inverted pin on the spec's I18nLabel and rested on one assertion:

constlabel: SpecI18nLabel='Priority';

A plain string is assignable under the narrow shape and under the wide one, so that assignment could only ever fail if the plain-string form were removed — the opposite of the event the comment said it was watching. Re-verified against the installed pin rather than taken from the card: @objectstack/spec@17.0.0, node_modules/@objectstack/spec/dist/ui/index.d.ts:614

declare const I18nLabelSchema: z.ZodUnion( readonly [ z.ZodString, z.ZodRecord( z.ZodString, z.ZodString ) ] );
type I18nLabel = z.input( typeof I18nLabelSchema ); // line 615

i.e. the widening had already landed and the pin stayed green through it. Doc block at dist/ui/index.d.ts:571-613 — both card line numbers confirmed accurate.

Retargeted at the premise that actually holds the decision up. Not which single form the spec has, but that both authorized forms stay assignable — on SpecI18nLabel and on the inherited ActionParam['label'] / options[].label. It now fails when either form is withdrawn, and deliberately does not fail on a further widening, since inheriting by reference is precisely what stays correct as the authorized set moves. Retitled, and the comment rewritten against the schema's own doc block ("Both are real; neither is deprecated by this schema", objectstack#5728, maintainer ruling 2026-08-06) instead of the false premise.

Evidence the new pin can fail

⛔ A rewritten pin still green under both shapes would have fixed nothing, so this is shown, not asserted.

Standalone 2×2 truth table — the real spec type vs. a locally constructed type I18nLabel = string:

assertionwide (real @objectstack/spec@17.0.0)narrow (string)
oldconst label: SpecI18nLabel = 'Priority'tsc exit 0tsc exit 0 — cannot discriminate
new map assert + fixturetsc exit 0tsc exit 2 — fires
narrow-new.ts(5,27): error TS2344: Type 'false' does not satisfy the constraint 'true'.
narrow-new.ts(7,14): error TS2322: Type '{ en: string; 'zh-CN': string; }' is not assignable to type 'string'.

The wide column carries its own negative control, so it is not vacuously green — assigning a number there resolves the real union and fails, and the error text names the union, proving the leg did not silently fall back to any:

wide-control.ts(4,14): error TS2322: Type 'number' is not assignable to
type 'string | Record( string, string )'.

In-repo reverse verification, both legs mutation-confirmed on disk and restored to a byte-identical tree (git diff --quiet HEAD → 0):

  • Leg A — narrow SpecI18nLabel inside the real test file → tsc -p tsconfig.test.json exit 2, TS2344 + TS2322.
  • Leg B — reintroduce the exact defect the pin guards (a local override narrower than the contract: label?: string) → exit 2, four errors, including both ActionParam legs:
page-nav-misc-spec-parity.test.ts(400,34): error TS2344: Type 'false' does not satisfy the constraint 'true'.
page-nav-misc-spec-parity.test.ts(401,35): error TS2344: Type 'false' does not satisfy the constraint 'true'.
page-nav-misc-spec-parity.test.ts(405,7): error TS2322: Type '{ en: string; 'zh-CN': string; }' is not assignable to type 'string'.
page-nav-misc-spec-parity.test.ts(408,11): error TS2322: Type '{ en: string; 'zh-CN': string; }' is not assignable to type 'string'.

Sibling pins in the same file — probed, none defective

The mechanical test for this defect class: a pin that cannot fail is one whose asserted value can be flipped without turning the compile red. Every type-level pin in the file was flipped one at a time, each flip confirmed on disk and restored; baseline green:

pinflipresult
IsAny( SpecNavigationArea['navigation'][number] ) = false :251to trueRED TS2322
SpecThemeInput = authored (mutual assignability) :293to a bad literalRED TS2353
ModeOfParsed = 'required' :309to 'optional'RED TS2322
ModeOfLocal = 'optional' :313to 'required'RED TS2322
RequiredOf = 'optional' :346to 'required'RED TS2322
new _MapForm / _StringForm :389-390to a narrowed targetRED TS2344 x2
new _ParamLabelMap / _OptionLabelMap :400-401to a narrowed targetRED TS2344 x2

The file's other 15 compile-time guards are @ts-expect-error directives, which cannot be blind by construction: a directive suppressing nothing is itself an error. Measured rather than assumed — injecting a directive over an error-free line gives

page-nav-misc-spec-parity.test.ts(69,1): error TS2578: Unused '@ts-expect-error' directive.

so with the baseline green, all 15 are live. No sibling pin carries the #5612 defect.

Part 2 (#5613) — the unused import

packages/types/src/ui-action.ts:27 imported the type I18nLabel with no type position using it. Re-verified on current origin/main: the import line number :27 is accurate, but the card's occurrence count is stale — it recorded 4 (import + 3 in JSDoc), and the file now holds 6 word-boundary occurrences (import at :27 plus five in JSDoc prose at :317, :323, :335, :336, :350), because #5617 landed the prose rewrite after the card was filed. None is a type position; the symbol is not re-exported. Deleted.

One bounded in-place fix, named here rather than left silent

The same JSDoc block carried a paragraph opening NOT guarded: that recorded this very pin as absent coverage and pointed at #5612. Landing the guard in the same PR would have shipped a published comment asserting the absence of the thing the PR adds, so the paragraph is corrected to say what is now true. Same defect class (stale prose asserting a false premise about this exact pin), same file this card already claims, same gate family, no new verification surface.

Changeset — patch, not empty frontmatter

Let the gate decide and measure dist/ rather than reasoning from an assumed zero delta — and the delta is not zero.

$ node scripts/check-changeset-presence.mjs
✅ 2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/i18nlabel-inverted-pin-5612.md.
$ pnpm changeset:check
✅ All workspace packages are in the changeset fixed group.
✅ No changeset declares a `major` bump.

Measured with the package's real tsc, both legs built from a cleared dist/ and cleared composite build info (tsconfig.tsbuildinfo, so neither leg skipped emit), comparing sha256 per file rather than byte counts:

  • 108 emitted files on both sides; exactly one differs.
  • dist/ui-action.d.ts — 31,026 to 31,117 bytes. Content diff is JSDoc prose only; no declaration changed.
  • dist/ui-action.js byte-identical (1b24e8f5…, 3,480 bytes) — the comment documents an interface, erased at emit with its declaration.
  • The deleted type import contributes no emitted delta at all, and the rewritten test file is not part of the build (exclude: **/*.test.ts).

So the published surface really does move, in the declaration file. That is the same shape and the same scoring as the precedent on this exact block (#5617 used '@object-ui/types': patch), which is why the empty-frontmatter "publishes nothing" form would have been wrong here.

Gates — run at eca724225, the final commit

$ pnpm --filter @object-ui/types type-check # tsc --noEmit, tsconfig.examples.json, tsconfig.test.json
os-verify-lock: VERDICT command-exit 0 · held the lock 27s
$ npx vitest run --maxWorkers=2 packages/types/
Test Files 40 passed (40)
Tests 460 passed (460)
$ pnpm lint # turbo run lint, repo-wide
Tasks: 47 successful, 47 total
✅ check-control-bytes: OK (scanned 4679 tracked text file(s); skipped 85 binary).
✅ spec symbol derivation: 1290 files scanned against 4912 spec export names
✅ No package names itself inside its own src/.
✅ lint coverage: 46/46 packages linted, 0 with outstanding errors (0 total).

The repo-wide pnpm lint ran in full — no narrowing is being declared. Both changed files lint clean of new findings: the test file reports 0 errors / 0 warnings, and ui-action.ts's 7 no-explicit-any warnings are pre-existing, at :600-642, far from either edit.

Not run locally, and left to CI by the standing scope rule: pnpm check (the CLI self-check, which needs the CLI build closure). Known-broken gauges noted, not touched: check-eager-closure-budget exits 2, check-doc-snippet-types exits 1.

Out-of-scope finding — already filed, no new card

#5613's body also observes that tsconfig.base.json sets noUnusedLocals: true while packages/types/tsconfig.json extends the root config, which sets it false. Confirmed on current origin/main, and extended: nothing under packages/ or apps/ extends tsconfig.base.json at all — the only consumer is examples/byo-backend-console/tsconfig.json, plus tsconfig.node.json and tsconfig.react.json, which nothing in turn extends.

Searching before filing, per the file-first discipline, this is already open as #4806, which states both halves — the noUnusedLocals gap and the uncapped no-unused-vars warning. No duplicate card was created; today's re-measurement was added there as a comment instead.


Generated by Claude Code

… forms
The `it(...)` case that called itself an inverted pin on the spec's
`I18nLabel` asserted `const label: SpecI18nLabel = 'Priority'` — type-correct
under both the narrow and the wide shape, so it could only fire if the
plain-string form were removed, the opposite of the widening it was written to
catch. The widening had already landed (`@objectstack/spec@17.0.0` declares
`z.ZodUnion<[z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>`) and the pin
stayed green.
Retarget it at the premise that holds the decision up: BOTH authorized forms
stay assignable, on `SpecI18nLabel` and on the inherited `ActionParam['label']`
/ `options[].label`. It now fails when either form is withdrawn. Retitled, and
the comment rewritten against the spec's own two-forms doc block.
Also drop the unused `I18nLabel` type import from `ui-action.ts` — residue of
the removed local `label` override, with no type position left — and update the
`NOT guarded` paragraph it sits under, which asserted the absence of the guard
this commit adds.
Fixes#5612Fixes#5613
…ed import
Scored `patch`, not empty frontmatter: the published surface really does move.
Measured with the package's real `tsc`, both legs from a cleared `dist/` and
cleared composite build info — 108 emitted files on both sides, exactly one
differing (`dist/ui-action.d.ts`, 31,026 -> 31,117 bytes, JSDoc prose only),
`dist/ui-action.js` byte-identical. Same shape as the precedent on this block.
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3785.8 KB3867.2 KB
Main entry chunk (gzip)151.6 KB350 KB
Entry fileindex-L9e7ztdq.js
StatusPASS

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

PackageSizeGzipped
app-shell (index.js)10.04KB3.72KB
app-shell (runtime-config.js)8.91KB2.99KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)29.34KB7.05KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)6.35KB2.43KB
auth (index.js)2.77KB1.22KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.02KB0.89KB
auth (useIsWorkspaceAdmin.js)3.04KB1.45KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)506.99KB113.73KB
core (index.js)4.51KB1.80KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)159.80KB44.33KB
fields (index.js)238.85KB60.13KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.22KB3.08KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.35KB3.31KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.42KB1.42KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.81KB0.83KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.65KB18.32KB
plugin-chatbot (index.js)181.41KB43.22KB
plugin-dashboard (index.js)128.36KB32.95KB
plugin-designer (index.js)212.30KB42.80KB
plugin-detail (index.js)242.15KB60.89KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)125.07KB30.43KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)200.79KB54.26KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.70KB27.17KB
plugin-map (index.js)20.06KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.50KB20.68KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)43.66KB14.77KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.33KB0.69KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (index.js)4.77KB2.16KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)10.76KB3.17KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)6.92KB2.40KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.08KB1.53KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-sales
os-sales marked this pull request as ready for review August 21, 2026 21:02
@os-sales
os-sales added this pull request to the merge queueAug 21, 2026
Merged via the queue into main with commit dffd752Aug 21, 2026
23 checks passed
@os-sales
os-sales deleted the claude/issue-5612-i18nlabel-inverted-pin branch August 21, 2026 21:02
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

1 participant

@os-sales