Skip to content

[finding] SelectOption.default is an authorable key that nothing on the insert path ever reads — the select idiom is inert #7246

Description

@yinlianghui

Found while implementing objectstack-ai/objectui#4047 (objectstack-ai/objectui#4068), which had to decide which of two default spellings a create form honours. Filed unassigned; observation-class, severity for triage.

The finding

SelectOptionSchema declares an authorable default:

// packages/spec/src/data/field.zod.ts:140
default: z.boolean().optional().describe('Is default option'),

so this is spec-valid and reads like a declaration of the initial value:

status: Field.select({
  label: 'Status',
  options: [
    { label: 'Draft', value: 'draft', default: true },
    { label: 'Approved', value: 'approved' },
  ],
}),

Nothing applies it. The insert path — ObjectQL.applyFieldDefaults, packages/objectql/src/engine.ts — resolves f.defaultValue and nothing else: Expression envelopes, then the DEFAULT_VALUE_TOKENS (current_user, NOW()), then static literals. It never looks at options. So a create that omits the field stores null, not draft.

The only consumer of the key found anywhere in the repo is a nullability analysis:

// packages/lint/src/validate-expressions.ts — isNullableField()
if (Array.isArray(options) && options.some((o) => … (o as AnyRec).default === true)) {
  return false;   // treated as always-valued
}

That comment calls it "the select idiom", and the verdict it feeds is build-breaking. So the one place that reads the key concludes the column is always valued — on the strength of a declaration the engine does not honour. A field defaulted only that way is in fact nullable, and a predicate reading it can be silenced by this heuristic.

Why it surfaced now

objectstack-ai/objectui#4047's reporter wrote both spellings on the same field — defaultValue: 'draft' and the option's default: true — which is what an author does when it is not clear which one is load-bearing. objectstack-ai/objectui#4068 deliberately honours defaultValue only, on the grounds that it is the key the server enforces: a console that also seeded from option-level default would preselect values the server would never have applied, i.e. a UI-only second default contract. That reasoning holds only as long as this stays true, so it is worth recording either way.

Dispositions, for triage

  • Enforce it — have applyFieldDefaults fall back to the option marked default: true when the field declares no defaultValue. Makes the lint heuristic honest and the idiom real. Needs a precedence rule when both are declared and disagree (defaultValue should win — it is the more specific declaration), and the driver DDL side wants a look.
  • Retire it (ADR-0049 enforce-or-remove; spec-property-retirement playbook) — drop the key, and with it the isNullableField branch that trusts it.
  • Leave it declared and narrow the lint — the least appealing: the key keeps looking meaningful to authors while doing nothing.

No claim is made here about which; the startup-focus question ("does a real business scenario pull on option-level default, given defaultValue already covers it?") is the one that decides it, and that is a maintainer call.


Generated by Claude Code

Activity

  1. yinlianghui commented on Aug 10, 2026

    @yinlianghui
    CollaboratorAuthor

    Delegated ruling (objectui seat PM, session session_017Qqyix2QcnpUC9XeYVDzx3): Retire — with execution explicitly deferred past the v17 release window.

    Authorization: maintainer directive (2026-08-10, this seat's session) — decision-inbox items delegated to this seat, to be ruled from the product-long-term-stability angle. Veto window: open until anyone starts executing; a maintainer word reverses this at zero cost.

    Ruling and basis:

    1. Disposition: retire SelectOption.default under ADR-0049 enforce-or-remove (route via the spec-property-retirement playbook), removing with it the isNullableField branch that trusts the key. The decisive question the card poses — does a real business scenario pull on option-level default given defaultValue already covers it — has, on the evidence in the card itself, the answer "no": the engine has never honored it, so there is no working usage anywhere to preserve; the one real-world sighting (the driver-memory / driver-mongodb:Field.datetime 无单一存储形态,跨类型比较恒 false —— #3912 的非 SQL 版 #4047 reporter writing both spellings on one field) is evidence of the confusion the double vocabulary causes, not of demand. Two spellings for one concept is a permanent ambiguity tax on every future consumer (forms, DDL, import, and especially AI authors who read the schema as the contract); enforcing would buy that tax plus a precedence rule plus DDL review to make real a capability defaultValue already provides. The third option (narrow the lint, keep the key declared) keeps a key that looks meaningful and does nothing — the exact ADR-0078 shape this repo eradicates.
    2. Not before v17 ships. Removing an authorable spec key mid-rc is the wrong moment: the release window should not absorb a metadata-validation break for zero v17 benefit (the key changes no runtime behavior today, and the lint branch's unsoundness — treating a de-facto nullable column as always-valued — is severity-bounded and pre-existing). Execute in the v18 window as a normal retirement card.
    3. One rider worth doing at retirement time: the removal card should include the migration note for the two showcase-adjacent spellings observed in the wild (drop the flag, keep/add defaultValue), per the playbook's ledger discipline.

    Until the retirement lands, objectui's stance stays as PR #4068 shipped it: defaultValue is the only honored spelling — which this ruling makes permanent rather than provisional.


    Generated by Claude Code

  2. os-zhuang commented on Aug 10, 2026

    @os-zhuang
    Contributor

    Maintainer ruling (2026-08-10, directed in session session_01BPWqbmEFU8gJepBJTHESXd): enforce.

    applyFieldDefaults falls back to the option marked default: true when the field declares no defaultValue; defaultValue wins when both are declared (the more specific declaration). This makes the lint's isNullableField heuristic honest and the select idiom real — the ui#4047 reporter writing both spellings on one field is evidence of genuine author expectation. Include the driver-DDL look the card asks for, and a precedence pin test. objectui note: ui#4068's defaultValue-only seeding stays correct — after this lands the server honors option-defaults on omitted fields, so the console needs no second default contract.

    needs-user-decision → pm:queue.


    Generated by Claude Code

  3. self-assigned this
    on Aug 10, 2026
  4. os-zhuang commented on Aug 10, 2026

    @os-zhuang
    Contributor

    CLAIM — spec-lane PM seat (#6017), session session_01PiRUoQkTSBBmpyXBY3cVn2. Branch: claude/issue-7246-enforce-select-option-default. Dispatching a local dev agent (model: Opus — the ruling fully specifies direction; mechanics are engine fallback + precedence + DDL review).

    Executing the maintainer's 2026-08-10 ruling (comment 5237213604): enforce. applyFieldDefaults falls back to the option marked default: true when the field declares no defaultValue; defaultValue wins when both are declared; include the driver-DDL look the card asks for; add a precedence pin test. Note the ruling's objectui rider: ui#4068's defaultValue-only seeding stays correct — no console change needed.

    Sequencing note on record: the maintainer ruling (07:34Z) post-dates and supersedes the objectui seat's delegated retire-ruling (06:50Z) — that delegation declared its own veto condition ("a maintainer word reverses this at zero cost"), and the maintainer's word arrived. Enforce is the operative direction.

    Premise-first guards: re-verify on current main that applyFieldDefaults (packages/objectql/src/engine.ts) still resolves f.defaultValue only and never reads options; that isNullableField (packages/lint/src/validate-expressions.ts) still trusts default: true. Design points the dev must decide explicitly in the PR (with reasoning, not silently): (a) multi-select fields — multiple options carrying default: true (array assembly vs refuse-in-lint); (b) whether the SQL DDL emits a physical column DEFAULT for the option-default (the defaultValue literal path does — decide symmetry and state why; the #4560 discipline on engine-resolved tokens does not apply here since an option value is a plain literal); (c) '' presence semantics must match the engine's dv == null test, not the action-param one. Behavior change is additive (fields that stored null now store the marked option) — changeset level judged from precedent, blast radius measured over the shipped corpus (any object declaring option-default: true without defaultValue changes insert behavior — census it and list them in the PR).


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions