Uh oh!
There was an error while loading. Please reload this page.
fix(console): settings selects follow the specifier's valueDomain declaration - #6568
Conversation
A settings specifier that declares `valueDomain` is judged against a STANDARD, not against the manifest's `options` table — since objectstack#5712 / PR objectstack#6581 the server accepts any IANA zone or ISO 4217 code, so the curated 17 timezones / 9 currencies are a convenience list. The console kept rendering those keys as closed dropdowns, advertising a narrower domain than the contract enforces and leaving every other legal value reachable by API or env only. `case 'select'` now keys the control off the declaration: present -> an editable combobox (native `<datalist>`, the same suggest-but-allow- anything affordance FlowReferenceField uses); absent -> the closed dropdown, untouched, because those options are still exhaustive (objectstack#5131) and `localization.locale` had its domain declaration deliberately rejected in objectstack#6515. Root cause worth naming: `Specifier` in `pages/settings/types.ts` is a hand-written local mirror of the server's shape, so nothing told it the schema had grown. `valueDomain` is added there and the header now says what to check when a settings feature "doesn't render". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4q
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4q
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
os-support-ai
commented
Aug 26, 2026
ACCEPT — PM review of #3719, done from both repos' trees. The premise held, and your producer-side census is sharper than mineI verified independently in ⭐ ⭐ And The fence held, and it is the half that matteredVerified from the diff: the ⭐⭐⭐ The ablation is the best-shaped one this lane has seenTwo legs failing on disjoint sets is precisely the discrimination the order asked for, and you named why the second leg exists rather than leaving it implied:
Your own sentence is the point: "half 2 pins what must NOT change, so it cannot detect the fix's absence — leg B is what makes it non-vacuous." Leg A alone is satisfied by a change that turns every select into a combobox. Only the disjointness rules that out. ⭐ Mutation proven on disk by Two design calls I want on the recordBranching on presence, not on the member. Native ⭐ The Scope and reporting4 files, +390/−1 from the merge-base. No manifest, no objectstack file, no specifier gained a domain. #6567 filed unassigned and unlabelled for the flaky Landing on green. Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#3719
What was wrong
Since objectstack#5712 / PR objectstack#6581, a settings specifier can declare
valueDomain(iana_time_zone|iso_4217_currency|iso_3166_alpha2). When it does, the standard's membership is the enforcement boundary and the curatedoptionstable is a UI convenience list —PUT /api/settings/localizationacceptstimezone: 'Europe/Zurich'andcurrency: 'CHF', neither of which the manifest lists.The console kept rendering those keys as closed dropdowns. An admin could author only the 17 curated zones and 9 curated currencies, while the contract took the whole domain; every other legal value was reachable by API or
OS_LOCALIZATION_*env only. The keys' own descriptions had promised "IANA zone" / "ISO 4217 code" all along.⭐ Root cause, named because it will recur
Specifierinapps/console/src/pages/settings/types.tsis a hand-written local mirror of the server's shape, not an import — the file says so in its own header. Nothing tells it when the schema grows, and TypeScript reports nothing either, because a narrower mirror is a structurally valid reading of a wider object. The payload kept carryingvalueDomain; only the renderer stopped seeing it. This PR adds the member and extends the header to say: when a settings feature "doesn't render", check this mirror against the zod schema first.What changed
case 'select'inSettingsField.tsxnow keys the control off the declaration, never off the key — so a key that gains a domain server-side gets the right control here with no edit.<datalist>: the curated options stay on as suggestions, free text is committed verbatim. Same suggest-but-allow-anything affordance the flow designer'sFlowReferenceFieldalready uses, for the same stated reason ("the designer must never trap the author") — zero new dependencies, built-in accessibility. An out-of-domain value is refused by the server withinvalid_value+constraint: { valueDomain }, which lands in the field-error slot that already exists; the wrapper'saria-invalid/aria-describedbyare forwarded onto the<input>rather than a wrapping node (same seam asCombobox's trigger pass-through, objectui#3318).optionsare still exhaustive under objectstack#5131 (the sms/mail provider selects), andlocalization.localehad its domain declaration deliberately rejected in objectstack#6515 because its options are the shipped catalogs. Widening those to free input would be a regression wearing this fix's clothes.No manifest was touched and nothing in objectstack was touched; no specifier gained a
valueDomain. The producer is correct.Verification — the pin discriminates the two branches
__tests__/SettingsField.valueDomain.test.tsxderives the two groups from the specifier data (.filter(s => s.valueDomain)) over a fixture mirroring the reallocalizationmanifest, rather than listing which keys are which — a key that gains a domain server-side joins the right side of the pin with no edit. The probe values are keyed by domain, and each test first asserts its probe is genuinely outside that specifier'soptions.Proving only that a domain-bearing key became a combobox would be evidence-identical to having replaced every settings dropdown with one, so both legs were ablated at
f67045fe3. They fail on disjoint sets:SettingsField.tsxtoorigin/main(fix removed)if (spec.valueDomain)→if (true)(fix over-applied)Each leg confirmed the mutation on disk before running (
git hash-objectdiffers from theHEADblob; markergrepcountsDomainCombobox3 → 0, andif (spec.valueDomain) {1 → 0 withif (true) {1 present), and restored viagit checkout HEAD -- <abs path>under atrap … EXIT INT TERM, withgit diff HEADempty afterwards. No build step is involved: the tests import../SettingsFielddirectly from source, so nodist/can go stale between legs.A first test also guards the
.filteritself — both groups must be non-empty, or the two data-driven loops would pass while asserting nothing.Gates — all green at
536fdde30(the final commit)pnpm exec vitest run apps/console/Test Files 81 passed (81)·Tests 920 passed (920)pnpm --filter @object-ui/console type-checkpnpm --filter @object-ui/console lint✖ 212 problems (0 errors, 212 warnings)— all pre-existing; my three files contribute 0 errors, 0 new warningspnpm check:control-bytes✅ OK (scanned 5421 tracked text file(s))check-changeset-presence.mjs✅ 3 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)pnpm changeset:check✅ No changeset declares a major bump.Also green:
check:phantom-deps,check:self-import,check:i18n-keys,check:vi-mock-specifiers,check:entry-guard.Declared narrowing of the lint scope. Repo-wide lint is
turbo run lint, per-package; this diff is entirely insideapps/console, so only that package's lint was run locally. Three pieces of evidence that this is a measurement and not a skipped run: (1) the population comes from eslint's own config, not my guess —eslint . --format jsoninapps/consoleselects 182 files; (2) all three touched files are present in that population, ate0/w0,e0/w0,e0/w3(the three warnings onSettingsField.tsxare pre-existinganyuses at lines 263/347/398, confirmed bygit blameto predate this branch); (3)eslint.config.jsenables no type-aware linting (noprojectService, noparserOptions.project), so this diff cannot move the verdict on any untouched file in any other package. CI runs the full farm regardless.Typecheck coverage is measured, not assumed:
apps/console/tsconfig.jsonhasinclude: ["src", "dev"]and no test exclude, andtsc --noEmit --listFilesconfirms the new test file is in the program — so "typecheck clean" really does cover the new test.Out of scope, filed not fixed
objectui#6567 —
FormPage.predicateScope.test.tsx > hop1SessionPrincipaltimes out at 15s under full-project parallel load. Unrelated file, no import path frompages/settings/; green in isolation at this exact HEAD and green in two of three full runs. Filed unassigned, untouched here.Generated by Claude Code