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
spec(ui): UserActionsConfigSchema adopts group / hideFields / rowColor — the three toolbar affordances ListView honours but no spec-valid document can declare (ruled A on objectui#5435) #11195
Filed by the domain:spec @ objectui seat (objectui#5734) as the cross-seat transfer of a ruled card.packages/spec is this lane's sole owner, so the spec leg lands here; the downstream objectui leg is objectui#5435, now pm:blocked on this card.
Filed unassigned. Not claimed.
The ruling (binding — not re-opened here)
Maintainer, 2026-08-22, decision-inbox digest, recorded on objectui#5435. The full inbox (46 cards) was presented with per-card four-axis recommendations and accepted as a batch, verbatim:
「接受所有」
Ruled: Option A — the spec adopts the three keys.UserActionsConfigSchema gains group, hideFields and rowColor, so the three toolbar affordances ListView already honours become authorable in a spec-valid document and the runtime fold's output passes the save gate (declared = enforced closes).
Option B (retiring three live product capabilities) and Option C (permanent double-spelling) were not taken. The triage execution note routes this as a clause ② contract card at contract-review tier, with objectui's fold/renderer tests aligning on the pin afterwards.
Why the spec leg exists
packages/core/src/utils/normalize-list-view.ts in objectui folds legacy bare show* flags into the canonical userActions block via SHOW_FLAG_TO_USER_ACTION. Four of the seven targets land on keys the spec declares; three do not:
packages/plugin-list/src/ListView.tsx (toolbarFlags) reads all three regardless. So a stored legacy view carrying showGroup: false comes out of the fold as userActions: { group: false } — which ListViewSchemarejects by name. Anything that normalizes then validates hits a refusal on a key the runtime itself just wrote.
Re-measured at this repo's head (not the card's 17.0.0 reading)
origin/main52a41b7. packages/spec/src/ui/view.zod.ts:853 — UserActionsConfigSchema is a strictObject (which is why the refusal is unrecognized_keys), declaring exactly eight keys:
group, hideFields, rowColor are absent. The premise holds at head, not merely at the 17.0.0 the card measured.
Reproduce (from objectui#5435, control included — the control is what makes the three refusals mean "this key" rather than "this schema refuses everything"):
node --input-type=module -e "
import { ListViewSchema, UserActionsConfigSchema } from '@objectstack/spec/ui';
const uk = r => (r.error?.issues ?? []).filter(i => i.code === 'unrecognized_keys').flatMap(i => i.keys ?? []);
for (const k of ['group','hideFields','rowColor']) {
const r = ListViewSchema.safeParse({ name:'my_view', label:'My View', columns:[{field:'name'}], userActions:{ [k]: true } });
console.log(k, r.success, uk(r), (r.error?.issues??[]).length);
}
console.log('control sort', ListViewSchema.safeParse({ name:'my_view', label:'My View', columns:[{field:'name'}], userActions:{ sort:true } }).success);
"
⚠️ Named premise the implementer must confront — two of the three key names are already taken, with different shapes
This is not a re-adjudication of A; it is a measurement A's implementation will hit on its first stroke. Both spellings already exist in packages/spec/src/ui carrying non-boolean shapes:
key
existing declaration
shape
objectui's fold produces
hideFields
component.zod.ts:853 — "Field names to omit from the body"
z.array(z.string())
boolean (ua?.hideFields === true)
rowColor
component.zod.ts:2132 — "Row color rules" (plus RowColorConfigSchema, view.test.ts:1937)
z.unknown() / config object
boolean (ua?.rowColor === true)
So adopting them verbatim as booleans under userActions creates same-name / different-shape pairs across the spec — precisely the hazard object-strictness-batch20.test.ts:410 exists to catch, in its own words: "the same key NAME, disjoint vocabulary … which is exactly why an author who learned one writes it on the other."
Instruction, per the ruling-with-premise discipline: implement A as ruled. If the collision forces a shape or naming change (e.g. the three land under a different spelling, or hideFields takes the array shape rather than the boolean the fold emits), stop and report the fork on this card — ⛔ do not silently pick a spelling, and ⛔ do not fall back to option B or C, both of which were explicitly not taken.
Pins that move when the three keys land
packages/spec/src/data/object-strictness-batch20.test.ts:410-417 — its comment enumerates the exact eight-key vocabulary and goes false on adoption. The assertion itself ("nothing overlaps" with the object block's create/import/edit/delete/exportCsv) survives, but must be re-checked, not assumed.
packages/spec/src/ui/view.test.ts:2491+ — UserActionsConfigSchema default-value test enumerates all eight keys; the three new ones need declared defaults and pins. Note the existing defaults split true (sort/search/filter/refresh/rowHeight) from false (addRecordForm/editInline); objectui's reads imply group defaults true (ua?.group !== false) while hideFields/rowColor default false (=== true). That asymmetry is load-bearing and should be pinned, not normalised away.
packages/spec/src/ui/page.zod.ts:368 consumes UserActionsConfigSchema — check the widening does not change page-level strictness readings.
Acceptance criterion (one line, executable)
ListViewSchema.safeParse({name:'my_view',label:'My View',columns:[{field:'name'}],userActions:{group:false,hideFields:true,rowColor:true}}).success === true, with the object-block control (rejectOnObject({userActions:{sort:false}}) still naming VIEW) still red.
Downstream
objectui#5435 — the fold/renderer test alignment. pm:blocked, Blocked-by: this card. ⚠️ It is blocked twice: on this change landing, and on objectui's spec pin bump, which objectui#5716 currently holds at 17.1.0.
Type not pre-filled deliberately. The mechanical boundary test cuts both ways here: the remedy widens the accept set (⇒ Feature), while the defect being closed is a runtime emitting documents its own gate refuses (⇒ Bug; objectui#5435 is typed Bug). Triage owns the field — flagging the tension rather than guessing.
Filed by the
domain:spec@ objectui seat (objectui#5734) as the cross-seat transfer of a ruled card.packages/specis this lane's sole owner, so the spec leg lands here; the downstream objectui leg is objectui#5435, nowpm:blockedon this card.Filed unassigned. Not claimed.
The ruling (binding — not re-opened here)
Maintainer, 2026-08-22, decision-inbox digest, recorded on objectui#5435. The full inbox (46 cards) was presented with per-card four-axis recommendations and accepted as a batch, verbatim:
Ruled: Option A — the spec adopts the three keys.
UserActionsConfigSchemagainsgroup,hideFieldsandrowColor, so the three toolbar affordancesListViewalready honours become authorable in a spec-valid document and the runtime fold's output passes the save gate (declared = enforced closes).Option B (retiring three live product capabilities) and Option C (permanent double-spelling) were not taken. The triage execution note routes this as a clause ② contract card at contract-review tier, with objectui's fold/renderer tests aligning on the pin afterwards.
Why the spec leg exists
packages/core/src/utils/normalize-list-view.tsin objectui folds legacy bareshow*flags into the canonicaluserActionsblock viaSHOW_FLAG_TO_USER_ACTION. Four of the seven targets land on keys the spec declares; three do not:packages/plugin-list/src/ListView.tsx(toolbarFlags) reads all three regardless. So a stored legacy view carryingshowGroup: falsecomes out of the fold asuserActions: { group: false }— whichListViewSchemarejects by name. Anything that normalizes then validates hits a refusal on a key the runtime itself just wrote.Re-measured at this repo's head (not the card's 17.0.0 reading)
origin/main52a41b7.packages/spec/src/ui/view.zod.ts:853—UserActionsConfigSchemais astrictObject(which is why the refusal isunrecognized_keys), declaring exactly eight keys:group,hideFields,rowColorare absent. The premise holds at head, not merely at the 17.0.0 the card measured.Reproduce (from objectui#5435, control included — the control is what makes the three refusals mean "this key" rather than "this schema refuses everything"):
This is not a re-adjudication of A; it is a measurement A's implementation will hit on its first stroke. Both spellings already exist in
packages/spec/src/uicarrying non-boolean shapes:hideFieldscomponent.zod.ts:853— "Field names to omit from the body"z.array(z.string())ua?.hideFields === true)rowColorcomponent.zod.ts:2132— "Row color rules" (plusRowColorConfigSchema,view.test.ts:1937)z.unknown()/ config objectua?.rowColor === true)So adopting them verbatim as booleans under
userActionscreates same-name / different-shape pairs across the spec — precisely the hazardobject-strictness-batch20.test.ts:410exists to catch, in its own words: "the same key NAME, disjoint vocabulary … which is exactly why an author who learned one writes it on the other."Instruction, per the ruling-with-premise discipline: implement A as ruled. If the collision forces a shape or naming change (e.g. the three land under a different spelling, or
hideFieldstakes the array shape rather than the boolean the fold emits), stop and report the fork on this card — ⛔ do not silently pick a spelling, and ⛔ do not fall back to option B or C, both of which were explicitly not taken.Pins that move when the three keys land
packages/spec/src/data/object-strictness-batch20.test.ts:410-417— its comment enumerates the exact eight-key vocabulary and goes false on adoption. The assertion itself ("nothing overlaps" with the object block'screate/import/edit/delete/exportCsv) survives, but must be re-checked, not assumed.packages/spec/src/ui/view.test.ts:2491+—UserActionsConfigSchemadefault-value test enumerates all eight keys; the three new ones need declared defaults and pins. Note the existing defaults splittrue(sort/search/filter/refresh/rowHeight) fromfalse(addRecordForm/editInline); objectui's reads implygroupdefaults true (ua?.group !== false) whilehideFields/rowColordefault false (=== true). That asymmetry is load-bearing and should be pinned, not normalised away.packages/spec/src/ui/page.zod.ts:368consumesUserActionsConfigSchema— check the widening does not change page-level strictness readings.Acceptance criterion (one line, executable)
ListViewSchema.safeParse({name:'my_view',label:'My View',columns:[{field:'name'}],userActions:{group:false,hideFields:true,rowColor:true}}).success === true, with theobject-block control (rejectOnObject({userActions:{sort:false}})still naming VIEW) still red.Downstream
pm:blocked,Blocked-by:this card.userActionsview/object name collision), objectui#5240 /LocalManifestSource.read()把「条目不存在」和「条目损坏」都答成null,两个 HTTP 调用方因此只能回一句无因由的 500 #5426 (theplugin-gridhalf), objectui#4337 (the legacylist-viewvocabulary the fold accepts has no declared type).Notes for triage
Feature), while the defect being closed is a runtime emitting documents its own gate refuses (⇒Bug; objectui#5435 is typedBug). Triage owns the field — flagging the tension rather than guessing.domain:speclabel inventory, 45 cards;since-windowed listing, 140 issues) becausesearch_issuesreturned an uncontrolled zero in this session — see finding(platform): search_issues 可整会话静默归零(含控制词);REST /search 被容器代理按设计拒绝——正确退路是 REST 列表端点,不是 MCP list_issues 手扫 #11193, filed independently minutes earlier. No existing card covers this.