Repository navigation
console: field designer still offers an "Indexed" toggle for a key FieldSchema rejects by name — ticking it hard-blocks the save #4644
Description
Activity
- addedtarget:v17v17 发布窗口工作集(GA 前排查 2026-08-04)v17 发布窗口工作集(GA 前排查 2026-08-04)
on 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
Claim: PM loop round 6
Session:session_01RnQd8iMMUwXQEV1crFmQiQ
Branch:claude/issue-4644-retire-indexed-toggle
Worktree:objectui-issue-4644
Domain:repo:objectui
File surface: the metadata-admin field designer surface offering theIndexedtoggle (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: opus
Serial constraints cleared: in-flight #4640 holdsviews/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
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)- 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 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)- 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 onmainmid-run and touches only parity tests).
Generated by Claude Code
{ "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
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).- Shape: draft →
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). - Scope (from the 23 changed files): matches the file-surface amendment comment above exactly — the as-claimed metadata-admin surface plus the bounded in-place class closure (plugin-designer's second
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. - 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-levelindexedreads — "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: 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. - 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-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. - Changeset: real, minor × the four released packages touched, and it documents the un-poisoning behavior users will see.
- Card speculation falsified, publicly:
External ID/trackHistoryboth parse clean —indexedwas the only rejected ADVANCED key. Recorded in the PR body; good run. - Out-of-scope finding: Both field designers author
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
- Shape: draft →
- added a commit that references this issue
on Aug 17, 2026 - added a commit that references this issue
on Aug 24, 2026 - added a commit that references this issue
on Sep 1, 2026
Measured on
@objectstack/console17.0.0 GA in a real browser, downstream at objectstack-ai/hotcrm#1122 (GA close-out probe objectstack-ai/hotcrm#1154).Filed here rather than in
objectstackbecause the fix is in the console field designer (metadata-adminbundle) — 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.indexedis translated in the shipped locale bundles, so this is a fully finished, intentional-looking control.The spec refuses the key it writes. Against
@objectstack/spec17.0.0 GA:FieldSchemahas noindex/indexedkey at all; the real surface is object-levelindexes: [{ name, fields, unique }], materialised bySqlDriver.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:
Read-only,Hidden,Indexed,External ID,Track history.Indexedis offered on a plain field, unchecked and fully enabled.aria-checkedflipsfalse → true.Unpublished draft, sub-header flips toDraft 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
Indexedis 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 retiredFieldSchemakey;External IDsits right next to it and is worth checking in the same pass (not measured here).Refs: objectstack-ai/objectstack#2377 (field-level flag removed), #4001 (silent-drop closed → loud rejection), #5248 (sibling
indexes[].type/indexes[].partialremovals).Downstream: objectstack-ai/hotcrm#1122.