Repository navigation
UniquenessValidation's deprecation JSDoc points authors at the retired indexes[].partial key #4765
Description
Activity
os-support-ai commented
on Aug 18, 2026 CollaboratorMore actionsTriage first-touch grade (concentrated batch; triage seat, session
session_01NYoiJGd1MxXdcvM4EGAMrS): promoted topm:queue, type Bug — false guidance on a@deprecatedtag is the worst place for it (read at exactly the migration moment), and both wrong spellings are measured:partialis a retiredKey tombstone in spec 17.0.0 andunique: trueis the deprecated positional form under ADR-0120. Comment-only fix, suggested shape in the body stands (unique: 'global' | 'organization', point partial constraints at the DB-layer migration route). Claim-time note: re-verify the prescription against the installed spec at that moment (the card measured rc.6; GA is pinned now). Size/model suggestion: S, sonnet.
Generated by Claude Code
- addeddomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seat
on Aug 21, 2026 Claim. PM seat
domain:ui(seat post #5560), round 7. Sessionsession_012u2pRjcqAYtoEjgr3wwhnK. Branchclaude/issue-4765-uniqueness-deprecation-jsdoc.pm:queue→pm:dispatched.Scope. The
@deprecatedprescription onUniquenessValidationinpackages/types/src/data-protocol.ts.Why this one is graded Bug rather than tidy-up. False guidance on a
@deprecatedtag is read at exactly the moment an author is migrating off the old shape, so a wrong spelling is maximally likely to be copied. Two of the three spellings it prescribes are wrong today:indexes[].partialis a retiredKey tombstone the parse rejects at any value, andunique: trueis the deprecated positional form under ADR-0120 that lint warns on in 17.x and protocol 18 rejects.Re-measure against the installed spec before wording the replacement. The card measured
17.0.0-rc.6; GA17.0.0is pinned now. Run the parse yourself and quote what it says — a tombstone message is the kind of thing that gets reworded between releases, and this card is about guidance that stopped matching the platform.Suggested shape, subject to your measurement: prescribe
{ fields, unique: 'global' | 'organization' }, drop thepartialclause, and point scoped/partial constraints at the database-layer migration route (CREATE UNIQUE INDEX … WHERE) rather than at a declarable key.Do not touch the interface's own deprecation.
UniquenessValidationis correctly deprecated and unreachable from the server (ValidationRuleSchemarejectstype: 'unique'). Only the replacement advice is stale.Fence note. Another round-7 card (#4611) is editing
packages/types/src/ui-action.ts. Different file in the same package — no overlap, stated explicitly because "same package" is not the collision unit. Stay out ofui-action.ts.Prose only — no runtime behaviour. If your diff touches anything that emits JavaScript, stop and report.
Generated by Claude Code
{ "issue": 4765, "status": "done", "branch": "claude/issue-4765-uniqueness-deprecation-jsdoc", "pr": "https://github.com/objectstack-ai/objectui/pull/5614", "premise_still_valid": true, "summary": "Rewrote the @deprecated prescription on UniquenessValidation in packages/types/src/data-protocol.ts. Both defects the card names reproduced against the installed GA @objectstack/spec@17.0.0 (the card measured rc.6): indexes[].partial is a tombstone rejected at any value, and indexes[].unique: true is the deprecated positional spelling of 'global'. The measurement REFINED the card in a way that changed the fix: 'unique' is scope vocabulary shared by two surfaces where the same bare true means different things, and the third prescribed spelling, FIELD-level unique: true, is NOT deprecated (spec: 'stays valid indefinitely ... no trap'). A uniform rewrite of both occurrences would have swapped one piece of false guidance for another, so the replacement names the per-surface difference explicitly and closes by naming what would falsify it (UniqueScopeSchema / IndexSchema). Interface's own deprecation untouched, as instructed. Comment-only, verified: of 38 changed lines in the .ts, non-comment lines = 0. Changeset is patch, decided by measuring the real build, not by the docs-do-not-publish heuristic.", "measurements": { "spec_version_installed": "17.0.0 (GA; card measured 17.0.0-rc.6)", "partial_retired": { "probe": "IndexSchema.safeParse({ fields: ['a'], partial: \"s = 'open'\" }) -> success=false, issue.code=invalid_type, issue.path=[\"partial\"]", "quoted_message": "`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 (predicate) from a runtime migration (this is what metadata-protocol's ensureOverlayIndex already does for sys_metadata). Drift detection is unaffected - it reads partiality back from the database's own DDL, never from this key. Run `os migrate meta --from 16` to rewrite existing sources automatically.", "rejected_at_any_value": "partial: true rejected with the identical message, so it is a tombstone and not a type error on one value", "control_probes": [ "CONTROL A (green): { fields: ['a'] } parses success=true -> the probe is capable of passing", "CONTROL B (green): { fields: ['a'], zzz_not_a_key: 1 } parses success=true -> the shape is .strip(), NOT strict, so the partial rejection is a targeted tombstone rather than generic unknown-key behaviour. Corroborated in the emitted type: partial is typed z.never()." ] }, "unique_true_deprecated_on_index_surface": { "quoted_describe": "Whether the index enforces uniqueness, and at which scope (ADR-0120). 'global' = materialized over exactly `fields`, no organization column injected - one holder across the whole installation; 'organization' = the driver prepends the NULL-safe organization key part (COALESCE(organization_id, '__global__')) at registration - one holder per organization; 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. 'tenant'/'org' are rejected - the word is 'organization'", "control_probes": [ "CONTROL (green): unique: 'global' and unique: 'organization' both parse success=true", "CONTROL (red): unique: 'nonsense_scope' rejected -> the union genuinely discriminates, so the green results above are not vacuous" ] }, "refinement_that_changed_the_fix": { "finding": "The card treats `unique: true` as one uniform defect. It is two different facts on two surfaces.", "helper_readout": "isUniqueDeclared/isGlobalUnique/isOrganizationUnique for true = true/false/false; for 'global' = true/true/false; for 'organization' = true/false/true. true sets neither scope flag, so 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 via isUniqueDeclared && !isGlobalUnique, i.e. per-organization.", "field_level_is_fine": "FieldSchema.safeParse({ type: 'text', unique: true }) parses success=true; spec prose: 'bare true stays valid indefinitely (it has exactly one documented meaning here and no trap)'. So field-level unique: true was CORRECT in the original comment and was deliberately not rewritten.", "independent_corroboration": "objectstack#8463 / PR objectstack#8512 corrected the parallel claim on isOrganizationScopedUnique's JSDoc for exactly this field-vs-index divergence, and records the maintainer ruling (2026-08-13, #8323 option 1 rejected) that makes the divergence load-bearing rather than an inconsistency to tidy away." }, "retained_claim_verified": { "claim": "ValidationRuleSchema rejects type: 'unique' (kept from the original comment rather than trusted)", "result": "Rejected at the discriminator; accepted discriminants are 'script' | 'state_machine' | 'format' | 'cross_field' | 'json_schema' | 'conditional'", "control_probes": [ "My first positive control was BADLY CHOSEN and I redid it: { type: 'required' } also fails, because 'required' is not a listed discriminant.", "GREEN CONTROL (fully green parse): { type: 'format', field: 'email', format: 'email', message: 'bad', name: 'r1' } parses success=true -> the schema does accept valid rules, so the 'unique' rejection is a real measurement and not an always-red probe." ] }, "comment_only_proof": "Not asserted - verified. Every added/removed line in the .ts diff was classified as comment vs code: 38 comment lines, 0 non-comment lines. Nothing that emits JavaScript was touched. git status shows exactly 2 files changed: the .ts and the new changeset.", "changeset_form": { "chosen": "patch, for @object-ui/types", "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 so the baseline had a restore point", "d_ts": "dist/data-protocol.d.ts 40218 -> 41781 bytes; new prose present in the emitted .d.ts, stale prose absent. Consumer-visible API documentation changes, so it is declared.", "js": "dist/data-protocol.js is BYTE-IDENTICAL (207 bytes both, sha256 a3de34c5...). Reported as measured, against the dispatch's expectation that removeComments: false makes a comment edit move emitted JS: data-protocol.ts is a types-only module whose entire JS output is the license banner plus `export {}`, so a comment on an erased `interface` reaches the .d.ts and nothing else. The .d.ts is what decided the bump, exactly as instructed." } }, "tests": "No test and no ablation is possible on this change, and saying so plainly is the honest answer rather than a gap: the diff is a comment, nothing executes it, so no assertion could distinguish old prose from new and mutating it could not turn any suite red. What stands in for a test is the parse measurement above, each probe paired with a control. Gate union re-run AFTER the final commit, at HEAD 531561746; exit codes captured before any pipe (redirect-then-capture), and each verdict below is the gate's OWN printed line, never a bare $?. check-control-bytes EXIT=0 'OK (scanned 4666 tracked text file(s); skipped 85 binary)'. check-changeset-presence EXIT=0 '1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'. check-changeset-no-major EXIT=0 'No changeset declares a major bump'. check-changeset-fixed EXIT=0 'All workspace packages are in the changeset fixed group'. check-type-check-coverage EXIT=0 '45/46 via type-check, 0 errors outstanding'. check-lint-coverage EXIT=0 '46/46 packages linted, 0 with outstanding errors'. check-spec-symbol-derivation EXIT=0 '1290 files scanned against 4912 spec export names'. pnpm --filter @object-ui/types type-check EXIT=0 (log echoes the hyphenated script name, so this is not the zero-match silent-pass trap). pnpm --filter @object-ui/types lint EXIT=0, '255 problems (0 errors, 255 warnings)', all pre-existing no-explicit-any. pnpm exec vitest run packages/types/ from repo ROOT (per objectui#3378) EXIT=0, 40 files / 460 tests passed. Re-derived from .github/workflows/ against my actual diff and additionally ran check-phantom-dependencies EXIT=0, check-package-self-import EXIT=0, check-skills-paths EXIT=0. NOTE worth PM attention: control-bytes was deliberately re-run after the changeset was COMMITTED - it scans TRACKED files, so the first run (4665 files) had not seen the still-untracked changeset; the second run (4666) did.", "declared_narrowing": "Repo-wide `pnpm lint` (turbo run lint, 46 packages) was not run locally; @object-ui/types was linted in full instead. Three pieces of evidence that this narrowing cannot hide a failure: (1) population read from eslint's OWN --format json output, not my guess - 99 files; (2) counts from that same output - 0 errors, 255 warnings, with the changed file present in the population and contributing 44 pre-existing no-explicit-any warnings on `any` types, none of which a comment can create; (3) invariance for untouched files - 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: / parserOptions, with controls on that same file ('rules' 10 hits, 'files' 10 hits) proving the search works. With no type information consulted, no untouched file's verdict can move. CI runs the full farm regardless.", "open_questions": [], "out_of_scope_findings": [ "filed as objectstack-ai/objectstack#10928 (unassigned, unlabelled for PM triage): UniqueScopeSchema's rejection message calls 'organization' \"the explicit spelling of true\" on BOTH surfaces, but on a declared index bare true means 'global' - measured, the message fires verbatim from IndexSchema.safeParse. An author refused on that key is told to write 'organization', which silently changes materialization (prepends the org key part) on an index already on deployed databases - the unannounced reinterpretation the #8323 ruling and #5082 sequencing exist to prevent. Searched for duplicates first (two searches, objectstack repo); the nearest prior art #8463/#8512 fixed a DIFFERENT artifact (isOrganizationScopedUnique's JSDoc in driver-sql), not the shared error map." ], "notes_for_pm": [ "Premise held, but with a refinement that materially changed the fix: only TWO of the three prescribed spellings were wrong, and which two matters. Field-level `unique: true` is correct today and was deliberately left standing; harmonizing it would have shipped a fresh piece of false guidance on a card whose whole purpose is to stop that.", "PR body carries a duplicate signature footer - I wrote the required session-URL footer and the platform appended a second one on creation. Not another actor editing the PR; flagged rather than edited, since re-writing the body risks the sanitizer.", "Fence honoured: packages/types/src/ui-action.ts (#4611) was never opened or edited. My diff is exactly 2 files.", "CI convergence not awaited, per the standing dispatch contract; gate status at report time is local-green with CI in progress." ] }
Generated by Claude Code
os-dev-report
Marker fallback: the previous comment (issuecomment-5373507983) carried the standard comment-shaped scan marker as its first line and GitHub's sanitizer ate it on write — verified by reading the stored body back, which now begins with a blank line. This comment is the same report re-posted with the marker as literal text so the PM scan can find it. Prefer this one; the earlier comment is a duplicate, not a second run.
{ "issue": 4765, "status": "done", "branch": "claude/issue-4765-uniqueness-deprecation-jsdoc", "pr": "https://github.com/objectstack-ai/objectui/pull/5614", "premise_still_valid": true, "summary": "Rewrote the @deprecated prescription on UniquenessValidation in packages/types/src/data-protocol.ts. Both defects the card names reproduced against the installed GA @objectstack/spec@17.0.0 (the card measured rc.6): indexes[].partial is a tombstone rejected at any value, and indexes[].unique: true is the deprecated positional spelling of 'global'. The measurement REFINED the card in a way that changed the fix: 'unique' is scope vocabulary shared by two surfaces where the same bare true means different things, and the third prescribed spelling, FIELD-level unique: true, is NOT deprecated (spec: 'stays valid indefinitely ... no trap'). A uniform rewrite of both occurrences would have swapped one piece of false guidance for another, so the replacement names the per-surface difference explicitly and closes by naming what would falsify it (UniqueScopeSchema / IndexSchema). Interface's own deprecation untouched, as instructed. Comment-only, verified: of 38 changed lines in the .ts, non-comment lines = 0. Changeset is patch, decided by measuring the real build, not by the docs-do-not-publish heuristic.", "measurements": { "spec_version_installed": "17.0.0 (GA; card measured 17.0.0-rc.6)", "partial_retired": { "probe": "IndexSchema.safeParse({ fields: ['a'], partial: \"s = 'open'\" }) -> success=false, issue.code=invalid_type, issue.path=[\"partial\"]", "quoted_message": "`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 (predicate) from a runtime migration (this is what metadata-protocol's ensureOverlayIndex already does for sys_metadata). Drift detection is unaffected - it reads partiality back from the database's own DDL, never from this key. Run `os migrate meta --from 16` to rewrite existing sources automatically.", "rejected_at_any_value": "partial: true rejected with the identical message, so it is a tombstone and not a type error on one value", "control_probes": [ "CONTROL A (green): { fields: ['a'] } parses success=true -> the probe is capable of passing", "CONTROL B (green): { fields: ['a'], zzz_not_a_key: 1 } parses success=true -> the shape is .strip(), NOT strict, so the partial rejection is a targeted tombstone rather than generic unknown-key behaviour. Corroborated in the emitted type: partial is typed z.never()." ] }, "unique_true_deprecated_on_index_surface": { "quoted_describe": "Whether the index enforces uniqueness, and at which scope (ADR-0120). 'global' = materialized over exactly `fields`, no organization column injected - one holder across the whole installation; 'organization' = the driver prepends the NULL-safe organization key part (COALESCE(organization_id, '__global__')) at registration - one holder per organization; 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. 'tenant'/'org' are rejected - the word is 'organization'", "control_probes": [ "CONTROL (green): unique: 'global' and unique: 'organization' both parse success=true", "CONTROL (red): unique: 'nonsense_scope' rejected -> the union genuinely discriminates, so the green results above are not vacuous" ] }, "refinement_that_changed_the_fix": { "finding": "The card treats `unique: true` as one uniform defect. It is two different facts on two surfaces.", "helper_readout": "isUniqueDeclared/isGlobalUnique/isOrganizationUnique for true = true/false/false; for 'global' = true/true/false; for 'organization' = true/false/true. true sets neither scope flag, so 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 via isUniqueDeclared && !isGlobalUnique, i.e. per-organization.", "field_level_is_fine": "FieldSchema.safeParse({ type: 'text', unique: true }) parses success=true; spec prose: 'bare true stays valid indefinitely (it has exactly one documented meaning here and no trap)'. So field-level unique: true was CORRECT in the original comment and was deliberately not rewritten.", "independent_corroboration": "objectstack#8463 / PR objectstack#8512 corrected the parallel claim on isOrganizationScopedUnique's JSDoc for exactly this field-vs-index divergence, and records the maintainer ruling (2026-08-13, #8323 option 1 rejected) that makes the divergence load-bearing rather than an inconsistency to tidy away." }, "retained_claim_verified": { "claim": "ValidationRuleSchema rejects type: 'unique' (kept from the original comment rather than trusted)", "result": "Rejected at the discriminator; accepted discriminants are 'script' | 'state_machine' | 'format' | 'cross_field' | 'json_schema' | 'conditional'", "control_probes": [ "My first positive control was BADLY CHOSEN and I redid it: { type: 'required' } also fails, because 'required' is not a listed discriminant.", "GREEN CONTROL (fully green parse): { type: 'format', field: 'email', format: 'email', message: 'bad', name: 'r1' } parses success=true -> the schema does accept valid rules, so the 'unique' rejection is a real measurement and not an always-red probe." ] }, "comment_only_proof": "Not asserted - verified. Every added/removed line in the .ts diff was classified as comment vs code: 38 comment lines, 0 non-comment lines. Nothing that emits JavaScript was touched. git status shows exactly 2 files changed: the .ts and the new changeset.", "changeset_form": { "chosen": "patch, for @object-ui/types", "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 so the baseline had a restore point", "d_ts": "dist/data-protocol.d.ts 40218 -> 41781 bytes; new prose present in the emitted .d.ts, stale prose absent. Consumer-visible API documentation changes, so it is declared.", "js": "dist/data-protocol.js is BYTE-IDENTICAL (207 bytes both, sha256 a3de34c5...). Reported as measured, against the dispatch's expectation that removeComments: false makes a comment edit move emitted JS: data-protocol.ts is a types-only module whose entire JS output is the license banner plus `export {}`, so a comment on an erased `interface` reaches the .d.ts and nothing else. The .d.ts is what decided the bump, exactly as instructed." } }, "tests": "No test and no ablation is possible on this change, and saying so plainly is the honest answer rather than a gap: the diff is a comment, nothing executes it, so no assertion could distinguish old prose from new and mutating it could not turn any suite red. What stands in for a test is the parse measurement above, each probe paired with a control. Gate union re-run AFTER the final commit, at HEAD 531561746; exit codes captured before any pipe (redirect-then-capture), and each verdict below is the gate's OWN printed line, never a bare $?. check-control-bytes EXIT=0 'OK (scanned 4666 tracked text file(s); skipped 85 binary)'. check-changeset-presence EXIT=0 '1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'. check-changeset-no-major EXIT=0 'No changeset declares a major bump'. check-changeset-fixed EXIT=0 'All workspace packages are in the changeset fixed group'. check-type-check-coverage EXIT=0 '45/46 via type-check, 0 errors outstanding'. check-lint-coverage EXIT=0 '46/46 packages linted, 0 with outstanding errors'. check-spec-symbol-derivation EXIT=0 '1290 files scanned against 4912 spec export names'. pnpm --filter @object-ui/types type-check EXIT=0 (log echoes the hyphenated script name, so this is not the zero-match silent-pass trap). pnpm --filter @object-ui/types lint EXIT=0, '255 problems (0 errors, 255 warnings)', all pre-existing no-explicit-any. pnpm exec vitest run packages/types/ from repo ROOT (per objectui#3378) EXIT=0, 40 files / 460 tests passed. Re-derived from .github/workflows/ against my actual diff and additionally ran check-phantom-dependencies EXIT=0, check-package-self-import EXIT=0, check-skills-paths EXIT=0. NOTE worth PM attention: control-bytes was deliberately re-run after the changeset was COMMITTED - it scans TRACKED files, so the first run (4665 files) had not seen the still-untracked changeset; the second run (4666) did.", "declared_narrowing": "Repo-wide `pnpm lint` (turbo run lint, 46 packages) was not run locally; @object-ui/types was linted in full instead. Three pieces of evidence that this narrowing cannot hide a failure: (1) population read from eslint's OWN --format json output, not my guess - 99 files; (2) counts from that same output - 0 errors, 255 warnings, with the changed file present in the population and contributing 44 pre-existing no-explicit-any warnings on `any` types, none of which a comment can create; (3) invariance for untouched files - 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: / parserOptions, with controls on that same file ('rules' 10 hits, 'files' 10 hits) proving the search works. With no type information consulted, no untouched file's verdict can move. CI runs the full farm regardless.", "open_questions": [], "out_of_scope_findings": [ "filed as objectstack-ai/objectstack#10928 (unassigned, unlabelled for PM triage): UniqueScopeSchema's rejection message calls 'organization' \"the explicit spelling of true\" on BOTH surfaces, but on a declared index bare true means 'global' - measured, the message fires verbatim from IndexSchema.safeParse. An author refused on that key is told to write 'organization', which silently changes materialization (prepends the org key part) on an index already on deployed databases - the unannounced reinterpretation the #8323 ruling and #5082 sequencing exist to prevent. Searched for duplicates first (two searches, objectstack repo); the nearest prior art #8463/#8512 fixed a DIFFERENT artifact (isOrganizationScopedUnique's JSDoc in driver-sql), not the shared error map." ], "notes_for_pm": [ "Premise held, but with a refinement that materially changed the fix: only TWO of the three prescribed spellings were wrong, and which two matters. Field-level `unique: true` is correct today and was deliberately left standing; harmonizing it would have shipped a fresh piece of false guidance on a card whose whole purpose is to stop that.", "PR body carries a duplicate signature footer - I wrote the required session-URL footer and the platform appended a second one on creation. Not another actor editing the PR; flagged rather than edited, since re-writing the body risks the sanitizer.", "Fence honoured: packages/types/src/ui-action.ts (#4611) was never opened or edited. My diff is exactly 2 files.", "This comment is the marker-fallback re-post; issuecomment-5373507983 is the same report whose comment-shaped marker the sanitizer stripped.", "CI convergence not awaited, per the standing dispatch contract; gate status at report time is local-green with CI in progress." ] }
Generated by Claude Code
Found while converging the console's index fallback schema for objectstack-ai/objectstack#5247 (PR touches
EmbeddedItemEditor.tsxonly; this sits outside that card's declared file surface, so it is filed rather than fixed).What
packages/types/src/data-protocol.ts(~line 1013), on the@deprecatedtag ofUniquenessValidation:Two of the three spellings it prescribes are no longer correct:
partialwas RETIRED in@objectstack/spec17.0.0 (ADR-0049 enforce-or-remove:IndexSchema.type与IndexSchema.partial没有任何 DDL 消费者 objectstack#5248, ADR-0049 enforce-or-remove, maintainer ruling 2026-08-06). It is aretiredKeytombstone inIndexSchematoday, so the parse rejects it at any value. Measured against the installed@objectstack/spec@17.0.0-rc.6:A scoped/partial unique constraint is now built at the database layer by a runtime migration issuing
CREATE UNIQUE INDEX … WHERE, not declared.unique: trueis the DEPRECATED positional spelling ofunique: 'global'under ADR-0120. Lintunique/unscoped-declared-indexwarns on it in 17.x and protocol 18 rejects it (ADR-0120 协议 18 项:D2 conversion(声明索引 unique: true → 'global')+ 裸 true 硬拒 + synonym pin 退役 —— 挂 18 列车,现在勿动工 objectstack#5082). The prescription should state the scope:unique: 'global'orunique: 'organization'.Why it is worth fixing
This is guidance that claims more than the platform delivers — the class objectstack-ai/objectstack#5247 calls out explicitly, citing the strictness ledger's finding 18 (four pieces of false guidance already shipped in that campaign). It sits on a
@deprecatedtag, i.e. precisely the text an author reads at the moment they are migrating off the old shape, so the wrong spelling is maximally likely to be copied.The interface itself is correctly deprecated and unreachable from the server (
ValidationRuleSchemarejectstype: 'unique') — only the replacement advice is stale. A comment-only change; no runtime behaviour.Suggested shape
Rewrite the
@deprecatedprescription to{ fields, unique: 'global' | 'organization' }, drop thepartialclause, and point partial constraints at the database-layer migration route.Backlink: objectstack-ai/objectstack#5247 · PR #4772
Generated by Claude Code