Skip to content

docs(types): ui-action.ts no longer claims spec 17 narrowed I18nLabel to a plain string - #5617

Merged
os-sales merged 2 commits into
mainfrom
claude/issue-4611-i18nlabelschema-comment
Aug 21, 2026
Merged

docs(types): ui-action.ts no longer claims spec 17 narrowed I18nLabel to a plain string#5617
os-sales merged 2 commits into
mainfrom
claude/issue-4611-i18nlabelschema-comment

Conversation

@os-sales

@os-salesos-sales commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Fixes#4611

One paragraph of packages/types/src/ui-action.ts's ActionParam doc block asserted something about @objectstack/spec that was never true, sitting on the exact symbol the label vocabulary turns on. A seat reading it while implementing the label / description widening would have taken that work for a no-op.

The measurement, taken fresh against the installed GA

The card was filed against 17.0.0-rc.6; the pin has since moved to GA. Resolved from packages/types: @objectstack/spec@17.0.0, node_modules/.pnpm/@objectstack+spec@17.0.0_.../dist/ui/index.d.ts:614, quoted with the generic brackets flattened so GitHub's body sanitizer cannot eat them (the file holds real syntax):

declare const I18nLabelSchema: z.ZodUnion( readonly ( z.ZodString, z.ZodRecord( z.ZodString, z.ZodString ) ) );
type I18nLabel = z.input( typeof I18nLabelSchema );

Still a union; the comment is false against GA as it was against rc.6. Corroborated three ways:

  • The schema's own doc block (:571-613) says it in as many words: a display label in one of two authorized forms (objectstack#5728, maintainer ruling 2026-08-06), a plain default-language string or an inline locale map, closing with "Both are real; neither is deprecated by this schema."
  • Executed against dist/ui/index.mjs: plain string ACCEPTED, inline locale map ACCEPTED, { key, defaultValue } REJECTED.
  • The keys this paragraph is about carry that union: ActionParamSchema.label and options( ).label are both string | Record( string, string ) in the shipped declaration.

Live rather than theoretical, measured in this tree: 26 files under packages/ contain a pickLocalized( call site (19 of them non-test), e.g. packages/components/src/renderers/layout/containers.tsx:739 resolving schema.title.

What the replacement says, and why it should not rot the same way

It leads with what I18nLabeladmits and cites the spec's own doc block, rather than restating a zod expression a spec bump can falsify. Today's spelling is still recorded — a reader needs it — but scoped as "against @objectstack/spec@17.0.0 it is ... (dist/ui/index.d.ts:614)", a reading of one named version with its file and line, and it says so: that is the distinction the replaced sentence failed to make.

The decision is unchanged, and never structurally depended on the false premise.label flows in by reference through Omit( z.input( typeof ActionParamSchema ), 'type' ), so it is whatever the spec declares. Deleting the local override was right for a reason the union's contents do not affect: a plain string is one of the admitted forms, so a local string | I18nLabel collapses to I18nLabel either way — an exact restatement claiming to be wider than it is. The old text reached the right conclusion through a false premise; the correction keeps the conclusion and replaces the reasoning.

Diff shape: comment-only, proven rather than asserted

  • Source: one hunk, @@ -315,9 +315,38 @@, 47 changed lines, every one matching ^[+-] \*( |$) — JSDoc continuation lines, all inside the block spanning 226-355 that documents export interface ActionParam.
  • Emitted, using the package's realtsc build (removeComments: false per tsconfig.base.json, so comments are emitted), dist cleared and tsbuildinfo removed between legs, restore leg guarded by an EXIT trap: 108 files both sides, exactly one differsdist/ui-action.d.ts, 29,176 to 31,026 bytes, and its diff is likewise one hunk of 47 lines all matching ^[+-] \*( |$).
  • dist/ui-action.js is byte-identical (3,480 bytes, sha256 1b24e8f5). Not because comments were stripped — they were not — but because this block documents an interface, which tsc erases along with its leading comment. No fragment of the block appears in the emitted JS on either side. Flagging this because the dispatch expected shipped JS to move: it does not here, and the reason is structural rather than a measurement artifact.

Changeset form follows that measurement: patch for @object-ui/types, not empty frontmatter. The JSDoc is on an exported declaration, so it ships in dist/*.d.ts and in consumers' hover text — consumer-visible API documentation, which is the criterion. No behaviour changes.

No test and no ablation is possible on this change — there is no executable behaviour to pin and nothing to ablate; a mutation of comment text cannot turn any assertion red. Stating that plainly rather than manufacturing a test that would only pin prose.

Gates — exit code captured before any pipe, each gate's own verdict quoted

All run at ec10aa217 (the final commit) with a clean tree, from the repo root:

gateexitverdict
pnpm --filter @object-ui/types type-check0tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json (script name echoed, so not a zero-match)
pnpm --filter @object-ui/types lint0✖ 255 problems (0 errors, 255 warnings)
pnpm exec vitest run packages/types/0Test Files 40 passed (40) · Tests 460 passed (460)
check-control-bytes0check-control-bytes: OK (scanned 4666 tracked text file(s); skipped 85 binary)
check-changeset-presence01 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)
check-changeset-fixed0All workspace packages are in the changeset fixed group.
check-changeset-no-major0No changeset declares a major bump.
check-type-check-coverage045/46 via type-check, 0 known-broken (0 errors outstanding)
check-lint-coverage046/46 packages linted, 0 with outstanding errors (0 total)
check-spec-symbol-derivation01290 files scanned against 4912 spec export names
check-action-forward-parity05 surfaces checked against 40 runtime-read keys from 4 consumers

vitest was run from the repo root with a path filter, never pnpm --filter ... test (objectui#3378). Heavy runs went through the container's shared verify lock.

Declared narrowing. Repo-wide pnpm lint / pnpm type-check (turbo, 46 packages), the four test shards, and the import-shape gates (check:phantom-deps, check:self-import, check:esm-specifiers, check:i18n-keys, check:i18n-drift) were left to CI. Why the narrowing cannot hide a failure: ESLint here is not type-awareeslint.config.js declares no parserOptions.project and no projectService — so every file's lint verdict is a function of that file's own source, and the only source file this PR touches was linted green. For everything type- or behaviour-shaped, the emitted-artifact measurement above is the argument: the sole changed byte range in the whole build output is JSDoc text inside one .d.ts, so no declaration, no type, and no runtime value that any other package compiles or executes against has moved. Imports are untouched, no locale strings or t() call sites are involved.

Two findings filed rather than fixed here

  • objectui#5612 — the parity test's self-described INVERTED PIN on this very decision cannot detect a re-widening. packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts:350-366 asserts const label: SpecI18nLabel = 'Priority', which type-checks under both the narrow and the wide shape. The spec has already re-widened and the pin stayed green — the event it promised to catch happened and it observed nothing. Out of this card's scope (different file), so it is filed, not touched. Its it(...) title is now the last copy of the false belief in this package.
  • objectui#5613ui-action.ts:27 imports I18nLabel and never uses it (residue of the removed override). Confirmed by this PR's own lint run: 27:3 warning 'I18nLabel' is defined but never used. Nothing fails on it: packages/types/tsconfig.json extends the roottsconfig.json, which sets noUnusedLocals: false (tsconfig.base.json's true never reaches the package), and the ESLint rule is a warning with no --max-warnings cap. Left in place to keep this diff prose-only.

skills/objectui/guides/i18n.md carries the same false belief as a rule — that is #5081, needs-user-decision, on a governed human-merge-only surface. Not touched here. Notes for whoever picks it up are in the report to the dispatching seat.

Generated by Claude Code

…bel is a plain string
The `ActionParam` doc block justified deleting the local `label` /
`options[].label` override with a claim about `@objectstack/spec` that was never
true: "in spec 17 `I18nLabelSchema` is `z.ZodString` -- inline per-locale
objects were dropped in favour of translation files". Measured against the
installed GA pin, `@objectstack/spec@17.0.0`, `dist/ui/index.d.ts:614`:
declare const I18nLabelSchema: z.ZodUnion(readonly (z.ZodString,
z.ZodRecord(z.ZodString, z.ZodString))) // real syntax at that line
Executed against `dist/ui/index.mjs`: plain string ACCEPTED, inline locale map
ACCEPTED, `{ key, defaultValue }` REJECTED. `ActionParamSchema.label` and
`options[].label` both carry that union, so the keys this paragraph is about
admit the map form today.
The correction describes what `I18nLabel` admits and quotes the schema's own doc
block (two authorized forms, "Both are real; neither is deprecated by this
schema") rather than restating a zod expression a spec bump can falsify. Where
today's spelling is named it is scoped as a measurement against 17.0.0 with its
file and line, which is the distinction the replaced sentence failed to make.
The decision the paragraph records is unchanged and was never structurally
dependent on the false premise: `label` flows in by reference through
`Omit(z.input(typeof ActionParamSchema), 'type')`, and deleting the override was
right for a reason the union's contents do not affect -- a plain string is one
of the admitted forms, so a local `string | I18nLabel` collapses to
`I18nLabel`.
Two things found while reading, filed rather than fixed here: the parity test's
self-described "INVERTED PIN" on this decision cannot detect a re-widening (it
assigns a plain string, which type-checks under either form -- so the widening
above went through it green), objectui#5612; and this file's now-unused
`I18nLabel` type import, objectui#5613.
Fixes#4611
Co-Authored-By: Claude <noreply@anthropic.com>
`dist/ui-action.d.ts` is the release-visible surface that moves: the corrected
paragraph is JSDoc on the exported `ActionParam` interface, so it ships in the
declaration file and in consumers' hover text. Measured with the package's real
`tsc` build (`removeComments: false`, per `tsconfig.base.json`), dist cleared and
tsbuildinfo removed between runs:
dist/ui-action.d.ts 29,176 -> 31,026 bytes, sha256 7c438361 -> 5a4513cc
dist/ui-action.js 3,480 bytes, sha256 1b24e8f5, BYTE-IDENTICAL
108 emitted files both sides; ui-action.d.ts is the only one that moves
The JavaScript does not move here -- not because comments are stripped (they are
not), but because the comment documents an `interface`, which tsc erases together
with its leading comment. Confirmed: no fragment of the block appears in
`dist/ui-action.js` on either side.
Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3785.7 KB3867.2 KB
Main entry chunk (gzip)151.5 KB350 KB
Entry fileindex-avY7FpUW.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.72KB18.35KB
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-salesClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — ACCEPT (card #4611)

Gates. 22 named check runs read individually for completed + success at ec10aa217: 19 success, 3 skippedTest (coverage), the unexpanded Test (coverage shard …/4) matrix placeholder, and dependabot. All four real shards, Type Check, Lint, Build & E2E, Build Docs, Bundle Analysis, Live E2E, both doc type checks, Control Byte Scan, Skill Guide Path Check, Internal Docs Link Check and all three changeset checks green.

The observation that makes this more than a text swap

The old comment reached a correct conclusion through a false premise. The recorded decision — inherit, do not override — was right, and right for a reason the union's contents cannot affect: label and options[].label arrive by reference through Omit(z.input(typeof SpecActionParamSchema), 'type'), and a plain string is one of the two admitted forms, so a local string | I18nLabel collapses to I18nLabel either way.

Keeping the conclusion and replacing only the reasoning is the right call and the harder one. The tempting move when you find a false premise is to assume the decision resting on it is also wrong; checking whether the conclusion survives on its own is what separates a repair from a regression.

Two of my dispatch's claims were falsified, both correctly

The emit premise. I told you a comment edit here moves dist/*.js. Measured with the package's real build: dist/ui-action.js is byte-identical, 3,480 bytes, sha256 1b24e8f5 both sides — and the reason is precise. Not that comments were stripped, but that this JSDoc documents an interface, which tsc erases along with its leading comment. Exactly one of 108 emitted files differs: dist/ui-action.d.ts, 29,176 → 31,026 bytes, its diff a single hunk of 47 lines all matching comment syntax.

The lane note is amended accordingly: comments on value declarations and inside function bodies move dist/*.js; comments on type-only declarations move only the .d.ts. Your scoped version is safer than my blanket one, because it never tells a seat to skip the measurement and it names the surface that actually decides the changeset form.

The pickLocalized figure. I passed "32 files" down from a card comment without measuring it. You could not reproduce it, measured your own — 38 files mentioning, 26 with a call site, 19 non-test — and cited yours with the method. That is the correct response to a number handed down by a PM, and I should not have quoted it unmeasured.

skills/ untouched, as required

Confirmed. The relay material for #5081 is in your report and goes to the maintainer through me; that card stays needs-user-decision and that surface stays human-merge-only.

Findings

#5612 is the valuable one, and it is a phantom check with a stale label attached. A test that calls itself an "INVERTED PIN" on this very decision asserts const label: SpecI18nLabel = 'Priority' — a plain string, which type-checks under both the narrow and the wide shape. The spec has already re-widened and the pin observed nothing; it can only fire if the plain-string form is removed, which is the opposite of the event it was written to catch. And its it(...) title is now, by grep, the last copy of the false belief in the package. Filed correctly rather than folded in.

#5613 carries a second-order observation worth more than the unused import it reports: packages/types/tsconfig.json extends the roottsconfig.json, which sets noUnusedLocals: false, so tsconfig.base.json's true never reaches the package. If that holds generally, a repo-wide setting is reaching no package at all — its own card, as you note.

Both arrived correctly labelled and queued.


Generated by Claude Code

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

Projects

None yet

2 participants

@os-sales@claude