Skip to content

docs(types): correct the UniquenessValidation deprecation prescription - #5614

Merged
os-sales merged 2 commits into
mainfrom
claude/issue-4765-uniqueness-deprecation-jsdoc
Aug 21, 2026
Merged

docs(types): correct the UniquenessValidation deprecation prescription#5614
os-sales merged 2 commits into
mainfrom
claude/issue-4765-uniqueness-deprecation-jsdoc

Conversation

@os-sales

Copy link
Copy Markdown
Collaborator

Fixes#4765

Corrects the @deprecated prescription on UniquenessValidation in packages/types/src/data-protocol.ts. Comment-only; no runtime behaviour.

Re-measured against the installed spec, not the rc

The card was written against @objectstack/spec@17.0.0-rc.6; GA 17.0.0 is pinned now, so every claim below was re-run against the installed package. Each probe is paired with a control, because a probe that can only come out one way is not a measurement.

indexes[].partial — retired, rejected at any value. Confirmed:

IndexSchema.safeParse({ fields: ['a'], partial: "s = 'open'" })
success = false
issue.code = invalid_type issue.path = ["partial"]
"`indexes[].partial` was removed in @objectstack/spec 17.0.0 (#5248, #4943,
ADR-0049) — no driver ever emitted the `WHERE` clause, so a declared partial
index was materialized as a FULL index and the predicate silently did
nothing. Delete the key. Partial indexes are built at the database layer,
not the declaration surface: issue CREATE [UNIQUE] INDEX ... WHERE (the
predicate placeholder is elided here — GitHub's write path destroys
angle-bracketed text) from a runtime migration"

Same rejection at partial: true, so it is a tombstone rather than a type error on one value. Control: { fields: ['a'], zzz_not_a_key: 1 } parses green — the shape is .strip(), not strict, so the rejection is a targeted tombstone (partial is typed z.never() in the emitted shape) and not generic unknown-key behaviour.

unique: true — deprecated on the index surface. Confirmed from the schema's own description text:

'global' = materialized over exactly `fields`, no organization column injected
'organization' = the driver prepends the NULL-safe organization key part
bare true = deprecated positional spelling of 'global' (warned in 17.x by lint
unique/unscoped-declared-index, rejected at protocol 18, #5082)
— state the scope

Controls: unique: 'global' and unique: 'organization' both parse green; unique: 'nonsense_scope' is rejected, so the union is genuinely discriminating.

The measurement refined the card, and that changed the fix

The report treats unique: true as one uniform defect. It is not — unique is scope vocabulary shared by two surfaces on which the same bare true means different things, and the original sentence prescribed it on both in a single breath:

surfacebare true meansstatus
indexes[].uniquestays verbatim over fields — same materialization as 'global'deprecated; lint warns in 17.x, protocol 18 rejects
field-level uniqueper-organization (the positional default)valid; spec says it "stays valid indefinitely … no trap"

Measured via the spec's own driver-facing helpers, which are the single source of truth the drivers read:

unique=true isUniqueDeclared=true isGlobalUnique=false isOrganizationUnique=false
unique="global" isUniqueDeclared=true isGlobalUnique=true isOrganizationUnique=false
unique="organization" isUniqueDeclared=true isGlobalUnique=false isOrganizationUnique=true

true is neither flag, i.e. no organization key part is prepended — which is exactly why it is the positional spelling of 'global' on the index surface. At field level the same value resolves through isUniqueDeclared && !isGlobalUnique, i.e. per-organization. FieldSchema.safeParse({ type: 'text', unique: true }) parses green.

So two of the three prescribed spellings were wrong, and the third — field-level unique: true — was correct all along. Rewriting both unique: true occurrences the same way would have swapped one piece of false guidance for another. The replacement names the per-surface difference instead.

The replacement

Prescribes { fields, unique: 'global' | 'organization' } for the index, keeps field-level unique valid while flagging that the token is surface-dependent, and sends predicated constraints to the database layer (a runtime migration issuing CREATE UNIQUE INDEX … WHERE) rather than at a declarable key. It closes by naming what would falsify it — UniqueScopeSchema and IndexSchema in @objectstack/spec — so the next reader checks the schemas rather than trusting the paragraph. This is the fourth piece of false guidance found in this campaign (strictness ledger finding 18); prose that cannot be checked is how the first three survived.

The interface's own deprecation is untouched and still correct. Verified: ValidationRuleSchema rejects type: 'unique' at the discriminator (accepted: script, state_machine, format, cross_field, json_schema, conditional). Control — a fully green parse, so the probe was capable of passing: { type: 'format', field: 'email', format: 'email', message: 'bad', name: 'r1' } parses success = true.

Comment-only, verified rather than asserted

Every one of the 38 changed lines in the .ts file was classified; lines not inside the block comment: 0.

git diff -U0 | classify each +/- line as comment vs code
comment lines: 38 non-comment lines: 0

Changeset form: patch, decided by the .d.ts

Measured with the package's real build (tsc; tsconfig.base.json sets removeComments: false deliberately), building both sides from committed states:

artifactbeforeafterverdict
dist/data-protocol.d.ts40218 bytes41781 bytesdiffers — new prose present, stale prose gone
dist/data-protocol.js207 bytes207 bytesidentical, sha256 a3de34c5…

The JSDoc sits on an exported declaration and reaches the declaration file, so consumer-visible API documentation changes and it is declared as patch.

Worth recording, since it cuts against the usual reasoning about removeComments: false: the emitted JS is byte-identical here. data-protocol.ts is a types-only module whose entire JS output is the license banner plus export {} — a comment attached to an erased interface reaches the .d.ts and nothing else. The .d.ts is what decided the bump; the .js is reported as measured, not as the general rule.

Gates

Union re-run after the final commit, at 531561746. Exit codes captured before any pipe; each line is the gate's own verdict.

gateexitverdict
check-control-bytes0OK, scanned 4666 tracked text files, skipped 85 binary
check-changeset-presence01 source file of 1 released package changed, 1 changeset declares it
check-changeset-no-major0No changeset declares a major bump
check-changeset-fixed0All workspace packages are in the changeset fixed group
check-type-check-coverage045/46 via type-check, 0 errors outstanding
check-lint-coverage046/46 packages linted, 0 with outstanding errors
check-spec-symbol-derivation01290 files scanned against 4912 spec export names
pnpm --filter @object-ui/types type-check0tsc --noEmit plus the examples and test projects
pnpm --filter @object-ui/types lint0255 problems, 0 errors, all pre-existing no-explicit-any
pnpm exec vitest run packages/types/ (repo root)040 files, 460 tests, all passed
check-phantom-dependencies0Every in-scope import is declared by its publishing package
check-package-self-import0No package names itself inside its own src/
check-skills-paths095/96 stated paths resolve, 1 baselined

The control-bytes gate was deliberately re-run after the changeset was committed: it scans tracked files, and on the first run the changeset was still untracked, so that run had not seen it. The count moves 4665 to 4666 across the two runs.

No test and no ablation is possible on this change. The diff is a comment; nothing executes it, so there is no assertion that could distinguish the old prose from the new one, and mutating it could not turn any suite red. The 460 passing tests are context, not evidence for this change. What stands in for a test is the parse measurement above — run against the installed spec, each probe paired with a control.

Declared narrowing. Repo-wide pnpm lint (turbo run lint, 46 packages) was not run locally; @object-ui/types was linted in full instead. Why that cannot hide a failure: the population is read from eslint's own JSON output — 99 files, 0 errors, 255 warnings, with the changed file present in it and contributing 44 pre-existing no-explicit-any warnings on any types, none of which a comment can create. The only channel by which this diff could reach another package's lint verdict is the emitted .d.ts, and type-aware linting is off: eslint.config.js has zero hits for projectService, project:, or parserOptions (controls on that same file: rules 10 hits, files 10 hits, so the search works). With no type information consulted, no untouched file's verdict can move. CI runs the full farm regardless.

Generated by Claude Code


Generated by Claude Code

The `@deprecated` tag on `UniquenessValidation` pointed authors at two
spellings the platform no longer accepts, at exactly the moment they are
migrating off the old shape:
- `indexes[].partial` was retired in `@objectstack/spec` 17.0.0 (ADR-0049).
It is a tombstone the parse rejects at any value, so the "`partial` for a
scoped constraint" clause prescribed a key that cannot be declared.
- `{ fields, unique: true }` on `ObjectSchema.indexes` is the deprecated
positional spelling of `unique: 'global'` under ADR-0120 — lint
`unique/unscoped-declared-index` warns in 17.x and protocol 18 rejects it.
Measured against the installed `@objectstack/spec@17.0.0` rather than the
rc the report was written against. That measurement also refined the card:
the third spelling, field-level `unique: true`, is NOT deprecated — at field
level bare `true` is the positional spelling of `'organization'` and stays
valid. The two surfaces give the same token different meanings, so the
replacement names that difference instead of harmonizing them, which would
have shipped a fresh piece of false guidance.
Comment-only: all 38 changed lines are inside the block comment; no emitted
JavaScript behaviour changes.
Co-Authored-By: Claude <noreply@anthropic.com>
…atch
`patch` rather than an empty frontmatter, decided by measurement rather than by
the "docs don't publish" heuristic: the JSDoc sits on an exported declaration,
so it ships. Built with the package's own `tsc` (`tsconfig.base.json` sets
`removeComments: false` deliberately), `dist/data-protocol.d.ts` moves
40218 -> 41781 bytes and carries the new prose.
The emitted `dist/data-protocol.js` is byte-identical either way (sha256
a3de34c5..., 207 bytes) — that module is types-only and its whole JS output is
the license banner plus `export {}`, so a comment on an erased `interface`
reaches the declaration file and nothing else. The `.d.ts` is what decides the
bump, and it changed.
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 #4765)

Gates. 22 named check runs read individually for completed + success at 531561746: 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 measurement changed the fix, and that is the whole value here

The card treats unique: true as one uniform defect across two prescribed spellings. It is not. Measured against the installed GA:

  • On a declared index, bare true is the deprecated positional spelling of 'global' — the helper readout is unambiguous (trueisUniqueDeclared true, isGlobalUnique false, isOrganizationUnique false; no organization key part prepended).
  • At field level, unique: true is not deprecated at all. The spec's own prose: "stays valid indefinitely (it has exactly one documented meaning here and no trap)".

So a uniform rewrite would have swapped one piece of false guidance for another — on a card whose entire purpose is stopping false guidance from shipping on a @deprecated tag. Leaving field-level true standing, and naming the per-surface difference explicitly, is the correct fix and it is not the fix the card described. Independently corroborated by objectstack#8463 / PR #8512, which corrected the same field-vs-index divergence on a different artifact, and by the maintainer ruling (2026-08-13, #8323 option 1 rejected) that makes the divergence load-bearing rather than an inconsistency to tidy away.

Probe discipline

Every zero paired with a control, and two of the controls are better than standard practice:

  • partial is a targeted tombstone, not generic unknown-key rejection.{ fields: ['a'], zzz_not_a_key: 1 } parses successfully, so IndexSchema is .strip() rather than strict — meaning the partial rejection is specific. Corroborated by partial being typed z.never() in the emitted type. Rejected at any value (true gives the identical message), so it is a tombstone and not a type error on one value.
  • A bad control caught and redone. The first positive control for the retained ValidationRuleSchema claim was { type: 'required' }, which also fails — required is not a listed discriminant, so that probe was red on both sides and proved nothing. It was replaced with a fully-valid format rule that parses. Catching your own always-red control is exactly the discipline that keeps a negative result meaningful.

The retained claim from the original comment (ValidationRuleSchema rejects type: 'unique') was re-verified rather than carried forward on trust. That matters on this card in particular.

Changeset

patch, decided by the artifact that actually carries the change: dist/data-protocol.d.ts 40,218 → 41,781 bytes with the new prose present and the stale prose absent, while dist/data-protocol.js is byte-identical at 207 bytes both sides. My dispatch warned that removeComments: false would move the emitted JS; for a types-only module whose entire JS output is a license banner plus export {}, it does not — the JSDoc on an erased interface reaches the .d.ts and nothing else. Reported as measured rather than bent to fit the dispatch's expectation, which is right.

Note

check-control-bytes was deliberately re-run after the changeset was committed, because it scans tracked files — the first run at 4,665 files had not seen the untracked changeset, the second at 4,666 had. Small thing, but it is the difference between a gate that saw the diff and one that did not.

Cross-repo finding objectstack#10928 raised separately to the maintainer; it is out of this lane and not this PR's business.


Generated by Claude Code

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

UniquenessValidation's deprecation JSDoc points authors at the retired indexes[].partial key

2 participants

@os-sales@claude