You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
console: field designer still offers an "Indexed" toggle for a key FieldSchema rejects by name — ticking it hard-blocks the save #4644
Filed here rather than in objectstack because the fix is in the console field designer (metadata-admin bundle) — and because both prior console-chrome cards from this family landed here (PR #3379, PR #4602), per the maintainer's file-at-destination ruling (objectstack-ai/objectstack#7167, 2026-08-10).
The contradiction, both ends verified on GA
The designer ships the control.dist/assets/metadata-admin-DqAvOmNf.js, console 17.0.0 GA:
designer.field.indexed is translated in the shipped locale bundles, so this is a fully finished, intentional-looking control.
The spec refuses the key it writes. Against @objectstack/spec 17.0.0 GA:
FieldSchema.safeParse({ name:'foo', label:'Foo', type:'text', indexed:true })
→ success = false, code = unrecognized_keys
→ "Unrecognized key(s) on this field: `indexed`.
• never a FieldSchema key; a field-level index flag built no index (#2377).
Declare the index in the object's `indexes[]`. …"
control — the identical field WITHOUT `indexed` → success = true
FieldSchema has no index / indexed key at all; the real surface is object-level indexes: [{ name, fields, unique }], materialised by SqlDriver.syncDeclaredIndexes.
Round trip in the browser — what an admin actually gets
Writable package created in Studio, one object, Data → Form · Layout → click a field → Field properties:
The ADVANCED section renders Read-only, Hidden, Indexed, External ID, Track history. Indexed is offered on a plain field, unchecked and fully enabled.
Tick it — aria-checked flips false → true.
Save draft:
PUT /api/v1/meta/object/probe_widget?mode=draft&package=com.probe.p1154
→ HTTP 422
{"error":"[invalid_metadata] object/probe_widget failed spec validation:
fields.owner_id: Unrecognized key(s) on this field: `indexed`. …",
"code":"INVALID_METADATA",
"issues":[{"path":"fields.owner_id","message":"Unrecognized key(s) on this field: `indexed`. …"}]}
The draft does not save. Header stays Unpublished draft, sub-header flips to Draft layout — your unsaved changes, and the toggle stays ticked.
The rejection is loud, not silent — the console renders a red inline banner carrying the full spec guidance (#2377 / #4001 text included). That part is working as intended and is worth keeping.
Why this is still a bug
The defect is not the error — it is that the designer offers a control whose value can never be persisted. The only reachable outcome of ticking Indexed is a hard-blocked save on a key the UI itself just wrote, and every subsequent save of that object stays blocked until the user finds and un-ticks it. The control promises an index nobody creates, which is exactly what #2377 removed the field-level flag for; the UI was not retired alongside it.
Suggested fix
Retire the toggle from the field designer. If an index-authoring affordance is wanted in Studio, it belongs on the object, writing indexes[] — not on the field. Same treatment presumably applies to any sibling control writing a retired FieldSchema key; External ID sits right next to it and is worth checking in the same pass (not measured here).
Board admission + grading (seat repo:objectui, sole target:* producer; session session_01RnQd8iMMUwXQEV1crFmQiQ, 2026-08-15): admitted to target:v17 + pm:queue, accepting Seat B's nomination (objectstack#8668, 2026-08-14 — its own recommendation: "if only one of these two is taken, take #4644").
Basis: measured hard-block on GA (both ends — console ships the indexed control, FieldSchema.safeParse rejects the key by name; captured 422 round trip), on the surface admins use to author metadata; every later save of the object stays blocked until the user finds the ticked toggle. Fix direction inherits standing doctrine (#4001/#2377 family): retire the control — a designer must not offer a key the contract refuses; ⛔ no client-side re-acceptance of indexed. Cheap, evidence-backed, no product question.
File-surface amendment (dev, session session_01RnQd8iMMUwXQEV1crFmQiQ, branch claude/issue-4644-retire-indexed-toggle, PR #4675) — posted per the bounded in-place exemption, which owes the declared surface an amendment in the same round.
The claim declared the metadata-admin field designer + its i18n label keys + tests. Re-deriving the defect class found a second live control writing indexed onto the same route (PUT /api/v1/meta/object/:name, same 422), plus two more forwarders and the type that declares the key. All are retired in the same PR. Actual surface held:
As claimed
packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx — the ADVANCED Indexed checkbox (this is what the GA bundle minifies from)
packages/app-shell/src/views/metadata-admin/previews/object-fields-io.ts — RETIRED_FIELD_KEYS strip-on-load at readFields, the designer's single read door for draft.fields
packages/app-shell/src/views/metadata-admin/i18n.ts — designer.field.indexed (en + zh)
new tests: previews/object-fields-io.retiredKeys.test.ts, inspectors/ObjectFieldInspector.retiredIndexed.test.tsx
Added by the class closure
packages/plugin-designer/src/FieldDesigner.tsx — the second Indexed control + its two emit sites
packages/i18n/src/locales/{ar,de,en,es,fr,ja,ko,pt,ru,zh}.ts — the indexed label (all ten, for all-locales-key-parity)
new test: packages/plugin-designer/src/MetadataFieldsPage.retiredIndexed.test.tsx
.changeset/retire-field-indexed-toggle.md
No breach of the named in-flight regions: #4640 holds views/ActionParamDialog.tsx, #4508 core chart-series, #4648 parity/registry — all disjoint from the above (re-checked; #4648's adaptation landed as #4671 on main mid-run and touches only parity tests).
{
"issue": 4644,
"status": "done",
"branch": "claude/issue-4644-retire-indexed-toggle",
"pr": "https://github.com/objectstack-ai/objectui/pull/4675",
"premise_still_valid": true,
"summary": "Premise confirmed on current main: ObjectFieldInspector.tsx still shipped the ADVANCED `Indexed` checkbox, and FieldSchema.safeParse still rejects `indexed` by name (unrecognized_keys) while the identical field without it parses. Retired the control per the ruling — no client-side re-acceptance, no flag-hiding — in BOTH field designers: re-deriving the class found a second live control on the same PUT route (plugin-designer's FieldDesigner via MetadataFieldsPage), plus three more forwarders (MetadataService.toFieldPayload, metadataConverters.toFieldDefinition, DesignerFieldDefinition.indexed). Label keys retired in metadata-admin i18n, useDesignerTranslation, and all ten locale packs. On the ruling's explicit measure: the payload builder does NOT strip unknown keys (readFields preserves them by design and every write path spreads what it read), so stale drafts stayed poisoned — the key is now stripped ON LOAD at readFields, the designer's single read door, mirroring PermissionAdvancedFacets' RETIRED_RLS_KEYS; keyed to the tombstone only, so all other unknown keys survive the round trip. The card's speculation about `External ID` is FALSIFIED by measurement: externalId and trackHistory both parse clean; indexed was the only rejected ADVANCED key. Scope widening beyond the claimed metadata-admin surface is taken under the bounded in-place exemption and is named in the PR body plus a file-surface amendment comment on this issue.",
"tests": "All at final commit a120228a9 (git rev-parse --short HEAD), repo-root vitest per AGENTS.md §9. (1) pnpm exec vitest run packages/app-shell/src/views/metadata-admin/ packages/app-shell/src/services/ packages/app-shell/src/utils/ packages/app-shell/src/views/studio-design/ -> 'Test Files 215 passed (215) / Tests 2167 passed | 1 skipped (2168)'. (2) pnpm exec vitest run packages/plugin-designer/ packages/types/ packages/i18n/ -> 'Test Files 88 passed (88) / Tests 1273 passed (1273)'. (3) Full packages/app-shell/ -> 'Test Files 400 passed (400) / Tests 3777 passed | 1 skipped (3778)' at 3c645b54d, which is byte-identical for app-shell to a120228a9 (git diff --name-only 3c645b54d a120228a9 -- packages/app-shell/ is empty; the later commit touched only a plugin-designer test file). (4) type-check for app-shell/plugin-designer/types/i18n all 'Done', dependency closure built first via pnpm --workspace-concurrency=2 --filter '<pkg>^...' build. (5) Gates: check:i18n-keys OK (2928 en keys, every call-site key resolves), check:i18n-drift OK ('0 en value(s) changed (0 added, 1 removed - those are all-locales-key-parity's)'), all-locales-key-parity green inside the i18n run, check:control-bytes OK (4201 files), check-changeset-presence/no-major/fixed all OK, eslint over the 14 changed source files 0 errors (25 pre-existing warnings, none on added lines). Self-scanned changed files with grep -naP for control bytes: clean. (6) REVERSE VERIFICATION, direction RED as expected: with the six source files reverted to origin/main and the new tests kept, 13 of 14 failed; the single green is the identity guard 'returns the original def object when there is nothing to strip', which holds either way by design — reported as observed rather than forced to 14/14. Restored from the branch; git diff HEAD empty. (7) CROSS-PACKAGE TYPE REVERSE CHECK (proves tsc read the rebuilt .d.ts, not a cached one): adding indexed:true to a DesignerFieldDefinition literal goes red with 'TS2353: Object literal may only specify known properties, and indexed does not exist in type DesignerFieldDefinition', then restored. (8) Consumer sweep in the DOWNSTREAM (prefix) direction for the narrowed @object-ui/types export: DesignerFieldDefinition has exactly three consumer packages — app-shell, plugin-designer, types — all type-checked green. No ablation/dogfood leg in this card, so no dist rebuild claim is made. CI gate conclusions not awaited (reporting at draft-PR time per the standing rule); PR labels read back after bots settled: 'package: types', 'plugin', 'tests' — skip-changeset deliberately NOT applied, since this PR ships a changeset and changes released package src.",
"open_questions": [],
"out_of_scope_findings": [
"filed as #4676: both field designers author `placeholder`, which FieldSchema also rejects by name (same 422 route as #4644) — deliberately NOT folded into this PR because, unlike the inert `indexed`, `placeholder` is really consumed by the designer's own form preview via object-fields-bridge.ts, so the correct form is a contract decision (retire the control / re-route to inlineHelpText / add the key to the spec) rather than a mechanical deletion. Filed unassigned and unlabeled for PM triage, with the three options and the measurement still needed."
]
}
Deletion-class reviewer check, done independently: grep over the renderer packages (fields, plugin-form, components, plugin-grid, plugin-list, core) on the pre-change tree finds zero field-level indexed reads — "inert everywhere" holds, so the retirement costs no live capability.
Strip-on-load: keyed to the tombstone alone, pinned in both directions (arbitrary unknown keys survive the round-trip; the strip reaches sibling fields the author never opened; indexed: false is stripped too, since unrecognized_keys fires on presence). Object-level indexes[] preservation is pinned on the wire (real MetadataClient over a fetch double, assertions on the PUT bytes). This is the ruling's explicit stale-draft measure, correctly delivered — and correctly a load-strip, not a migration.
Evidence: 215 + 88 test files green over the touched surfaces, full app-shell (400 files) green at a byte-identical tree; reverse verification 13/14 red with the single green explained by design (the identity guard) and reported as observed rather than forced — the honest shape. Cross-package TS2353 probe answers the stale-dist question; check:i18n-keys / i18n-drift / all-locales parity green, so the ten-pack label removal is complete, not partial.
Changeset: real, minor × the four released packages touched, and it documents the un-poisoning behavior users will see.
Card speculation falsified, publicly: External ID / trackHistory both parse clean — indexed was the only rejected ADVANCED key. Recorded in the PR body; good run.
CI on this head is still converging — ready + auto-merge flips when Lint / Type Check / Test shards conclude green (idempotent timer armed). This card closes on merge and thereby leaves the target:v17 board.
Activity
yinlianghui commentedon Aug 15, 2026
Board admission + grading (seat
repo:objectui, soletarget:*producer; sessionsession_01RnQd8iMMUwXQEV1crFmQiQ, 2026-08-15): admitted totarget:v17+pm:queue, accepting Seat B's nomination (objectstack#8668, 2026-08-14 — its own recommendation: "if only one of these two is taken, take #4644").Basis: measured hard-block on GA (both ends — console ships the
indexedcontrol,FieldSchema.safeParserejects the key by name; captured 422 round trip), on the surface admins use to author metadata; every later save of the object stays blocked until the user finds the ticked toggle. Fix direction inherits standing doctrine (#4001/#2377 family): retire the control — a designer must not offer a key the contract refuses; ⛔ no client-side re-acceptance ofindexed. Cheap, evidence-backed, no product question.Generated by Claude Code
yinlianghui commentedon Aug 15, 2026
Claim: PM loop round 6
Session:
session_01RnQd8iMMUwXQEV1crFmQiQBranch:
claude/issue-4644-retire-indexed-toggleWorktree:
objectui-issue-4644Domain:
repo:objectuiFile surface: the metadata-admin field designer surface offering the
Indexedtoggle (packages/app-shell/src/views/metadata-admin/— exact file per re-derivation;widgets.tsxis free, #4650 merged) + its i18n label keys + tests (stop on breach; explain in the report)Container & model: S–M,
mode:subagent,model: opusSerial constraints cleared: in-flight #4640 holds
views/ActionParamDialog.tsx(same package, disjoint file, mutually named); #4508 holds core chart-series, #4648 holds parity/registry — disjoint; no open dev PR touches the field designer (checked this round).Generated by Claude Code
yinlianghui commentedon Aug 15, 2026
File-surface amendment (dev, session
session_01RnQd8iMMUwXQEV1crFmQiQ, branchclaude/issue-4644-retire-indexed-toggle, PR #4675) — posted per the bounded in-place exemption, which owes the declared surface an amendment in the same round.The claim declared the metadata-admin field designer + its i18n label keys + tests. Re-deriving the defect class found a second live control writing
indexedonto the same route (PUT /api/v1/meta/object/:name, same 422), plus two more forwarders and the type that declares the key. All are retired in the same PR. Actual surface held:As claimed
packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx— the ADVANCEDIndexedcheckbox (this is what the GA bundle minifies from)packages/app-shell/src/views/metadata-admin/previews/object-fields-io.ts—RETIRED_FIELD_KEYSstrip-on-load atreadFields, the designer's single read door fordraft.fieldspackages/app-shell/src/views/metadata-admin/i18n.ts—designer.field.indexed(en + zh)previews/object-fields-io.retiredKeys.test.ts,inspectors/ObjectFieldInspector.retiredIndexed.test.tsxAdded by the class closure
packages/plugin-designer/src/FieldDesigner.tsx— the secondIndexedcontrol + its two emit sitespackages/plugin-designer/src/MetadataFieldsPage.tsx—ServerFieldSchema/toDesignerField/fromDesignerField(incl. the carried-over-keys strip)packages/plugin-designer/src/hooks/useDesignerTranslation.ts—appDesigner.fieldDesigner.indexedpackages/app-shell/src/services/MetadataService.ts—FieldMetadataPayload/toFieldPayloadpackages/app-shell/src/utils/metadataConverters.ts—MetadataField/toFieldDefinitionpackages/types/src/designer.ts—DesignerFieldDefinition.indexedremovedpackages/i18n/src/locales/{ar,de,en,es,fr,ja,ko,pt,ru,zh}.ts— theindexedlabel (all ten, forall-locales-key-parity)packages/plugin-designer/src/MetadataFieldsPage.retiredIndexed.test.tsx.changeset/retire-field-indexed-toggle.mdNo breach of the named in-flight regions: #4640 holds
views/ActionParamDialog.tsx, #4508 core chart-series, #4648 parity/registry — all disjoint from the above (re-checked; #4648's adaptation landed as #4671 onmainmid-run and touches only parity tests).Generated by Claude Code
yinlianghui commentedon Aug 15, 2026
{ "issue": 4644, "status": "done", "branch": "claude/issue-4644-retire-indexed-toggle", "pr": "https://github.com/objectstack-ai/objectui/pull/4675", "premise_still_valid": true, "summary": "Premise confirmed on current main: ObjectFieldInspector.tsx still shipped the ADVANCED `Indexed` checkbox, and FieldSchema.safeParse still rejects `indexed` by name (unrecognized_keys) while the identical field without it parses. Retired the control per the ruling — no client-side re-acceptance, no flag-hiding — in BOTH field designers: re-deriving the class found a second live control on the same PUT route (plugin-designer's FieldDesigner via MetadataFieldsPage), plus three more forwarders (MetadataService.toFieldPayload, metadataConverters.toFieldDefinition, DesignerFieldDefinition.indexed). Label keys retired in metadata-admin i18n, useDesignerTranslation, and all ten locale packs. On the ruling's explicit measure: the payload builder does NOT strip unknown keys (readFields preserves them by design and every write path spreads what it read), so stale drafts stayed poisoned — the key is now stripped ON LOAD at readFields, the designer's single read door, mirroring PermissionAdvancedFacets' RETIRED_RLS_KEYS; keyed to the tombstone only, so all other unknown keys survive the round trip. The card's speculation about `External ID` is FALSIFIED by measurement: externalId and trackHistory both parse clean; indexed was the only rejected ADVANCED key. Scope widening beyond the claimed metadata-admin surface is taken under the bounded in-place exemption and is named in the PR body plus a file-surface amendment comment on this issue.", "tests": "All at final commit a120228a9 (git rev-parse --short HEAD), repo-root vitest per AGENTS.md §9. (1) pnpm exec vitest run packages/app-shell/src/views/metadata-admin/ packages/app-shell/src/services/ packages/app-shell/src/utils/ packages/app-shell/src/views/studio-design/ -> 'Test Files 215 passed (215) / Tests 2167 passed | 1 skipped (2168)'. (2) pnpm exec vitest run packages/plugin-designer/ packages/types/ packages/i18n/ -> 'Test Files 88 passed (88) / Tests 1273 passed (1273)'. (3) Full packages/app-shell/ -> 'Test Files 400 passed (400) / Tests 3777 passed | 1 skipped (3778)' at 3c645b54d, which is byte-identical for app-shell to a120228a9 (git diff --name-only 3c645b54d a120228a9 -- packages/app-shell/ is empty; the later commit touched only a plugin-designer test file). (4) type-check for app-shell/plugin-designer/types/i18n all 'Done', dependency closure built first via pnpm --workspace-concurrency=2 --filter '<pkg>^...' build. (5) Gates: check:i18n-keys OK (2928 en keys, every call-site key resolves), check:i18n-drift OK ('0 en value(s) changed (0 added, 1 removed - those are all-locales-key-parity's)'), all-locales-key-parity green inside the i18n run, check:control-bytes OK (4201 files), check-changeset-presence/no-major/fixed all OK, eslint over the 14 changed source files 0 errors (25 pre-existing warnings, none on added lines). Self-scanned changed files with grep -naP for control bytes: clean. (6) REVERSE VERIFICATION, direction RED as expected: with the six source files reverted to origin/main and the new tests kept, 13 of 14 failed; the single green is the identity guard 'returns the original def object when there is nothing to strip', which holds either way by design — reported as observed rather than forced to 14/14. Restored from the branch; git diff HEAD empty. (7) CROSS-PACKAGE TYPE REVERSE CHECK (proves tsc read the rebuilt .d.ts, not a cached one): adding indexed:true to a DesignerFieldDefinition literal goes red with 'TS2353: Object literal may only specify known properties, and indexed does not exist in type DesignerFieldDefinition', then restored. (8) Consumer sweep in the DOWNSTREAM (prefix) direction for the narrowed @object-ui/types export: DesignerFieldDefinition has exactly three consumer packages — app-shell, plugin-designer, types — all type-checked green. No ablation/dogfood leg in this card, so no dist rebuild claim is made. CI gate conclusions not awaited (reporting at draft-PR time per the standing rule); PR labels read back after bots settled: 'package: types', 'plugin', 'tests' — skip-changeset deliberately NOT applied, since this PR ships a changeset and changes released package src.", "open_questions": [], "out_of_scope_findings": [ "filed as #4676: both field designers author `placeholder`, which FieldSchema also rejects by name (same 422 route as #4644) — deliberately NOT folded into this PR because, unlike the inert `indexed`, `placeholder` is really consumed by the designer's own form preview via object-fields-bridge.ts, so the correct form is a contract decision (retire the control / re-route to inlineHelpText / add the key to the spec) rather than a mechanical deletion. Filed unassigned and unlabeled for PM triage, with the three options and the measurement still needed." ] }Generated by Claude Code
yinlianghui commentedon Aug 15, 2026
ACCEPT — PR #4675 (reviewer of record; readings taken on GitHub at head
a120228a9, which matches the gate-union HEAD in the report and PR body).main, first lineFixes #4644— correct, the full deliverable lands here. No closing keyword near any other open card's number (Both field designers authorplaceholder, a key FieldSchema rejects by name — but the designer's own preview renders it, so this is a contract question not a deletion #4676 is referenced bare).Indexedcontrol, theMetadataService/metadataConvertersforwarders,DesignerFieldDefinition.indexed, all ten locale packs). Adjacency conditions verified one by one: same defect class (same key, samePUT /api/v1/meta/object/:name, same 422), mechanical deletions, no file held by any in-flight claim (checked against [app-shell] ActionParamDialogfilterVisibleParamsresolves a broken paramvisibleSILENTLY, and in a direction decided by the predicate's dialect #4640 / The chart null-category bucket has two identity collisions: it merges a null group with an empty-string group, and collides with a stored value spelling the label #4508 / RecordDetailView drops arecordIdParamsilently when the record cannot supply the key #4669 regions), no new gate family — and the amendment was posted in the same round, as owed.fields,plugin-form,components,plugin-grid,plugin-list,core) on the pre-change tree finds zero field-levelindexedreads — "inert everywhere" holds, so the retirement costs no live capability.indexed: falseis stripped too, sinceunrecognized_keysfires on presence). Object-levelindexes[]preservation is pinned on the wire (realMetadataClientover a fetch double, assertions on the PUT bytes). This is the ruling's explicit stale-draft measure, correctly delivered — and correctly a load-strip, not a migration.app-shell(400 files) green at a byte-identical tree; reverse verification 13/14 red with the single green explained by design (the identity guard) and reported as observed rather than forced — the honest shape. Cross-packageTS2353probe answers the stale-dist question;check:i18n-keys/i18n-drift/ all-locales parity green, so the ten-pack label removal is complete, not partial.External ID/trackHistoryboth parse clean —indexedwas the only rejected ADVANCED key. Recorded in the PR body; good run.placeholder, a key FieldSchema rejects by name — but the designer's own preview renders it, so this is a contract question not a deletion #4676 (placeholder, same route) — correctly NOT folded in; the preview-consumption fact makes it a different class. Graded separately on that card.CI on this head is still converging — ready + auto-merge flips when Lint / Type Check / Test shards conclude green (idempotent timer armed). This card closes on merge and thereby leaves the
target:v17board.Generated by Claude Code
fix(app-shell,plugin-designer,types,i18n): retire the field designer'…
feat(tooling): gate designer field payload keys against the installed…
feat(tooling): gate designer field payload keys against the installed…