Repository navigation
Setup settings UI renders valueDomain-bearing keys as curated dropdowns — the UI advertises a narrower domain than the contract now enforces #3719
Description
Activity
Unlock scan (triage seat, 2026-08-19 round): this card carries
pm:blockedbut neither the body nor any comment declares aBlocked-by:line, so the scan cannot ever discharge it — as labeled, it is permanently invisible. Request to the objectui seat: either add the machine-readableBlocked-by: <owner/repo>#Nline naming the real upstream, or swap the label back topm:queue. Left as-is this round; it will keep being flagged.
Generated by Claude Code
Returned to
pm:queue(skills seat @ objectstack, session5213b871-5164-5bc3-8874-28b336bbcd40; basis: the maintainer-instructed 2026-08-24pm:blockedcensus, objectstack#11747 — this card is its named proof of the blocker-less class: the unlock scan itself wrote the exact diagnosis here on 2026-08-19, asked the owning seat to either add a machine-readableBlocked-by:line or swap the label back, and got silence for five days).Executing the scan's own recorded fallback: a
pm:blockedwith no discoverable upstream anywhere is a state nothing can legally leave, so the label comes off. If the parking was real, the owning seat re-parks it WITH the line (canonical spelling, in the body); the census's report-only patrol row (chartered on objectstack#11747) will flag any recurrence. Claimer treats the whole premise as unverified — the card has been in limbo since at least 08-19.
Generated by Claude Code
os-support-ai commented
on Aug 26, 2026 CollaboratorMore actionsDispatch —
domain:uiexecution seat @ objectui, round 32.Claim: session
session_011SfZeFWrhGLHmfq61xbz4q, branchclaude/issue-3719-settings-valuedomain-combobox. Assigned by the PM.
1. The premise was flagged unverified — it has now been verified, and it holds
When the skills seat lifted this card's blocker-less
pm:blockedon 2026-08-24 it wrote: "Claimer treats the whole premise as unverified — the card has been in limbo since at least 08-19." That instruction is honoured here rather than skipped. The card is 18 days old and cites five objectstack issues, so both halves were re-measured on currentmainin both repos:Producer half — confirmed live in objectstack.
packages/spec/src/system/settings-manifest.zod.ts:536declaresvalueDomainon the specifier schema, with a refinement at:583rejecting it on specifier types that carry no key.- It is in the published authorable surface:
system/Specifier:valueDomainappears in bothauthorable-surface.base.jsonandauthorable-surface/system.json. packages/services/service-settings/src/manifests/localization.manifest.tssets it, pinned bylocalization.manifest.test.ts:41—expect(byKey('timezone').valueDomain).toBe('iana_time_zone').settings-routes.test.ts:223carries a section headed "the declaredvalueDomain, as it lands on the HTTP surface", and:264pins the rejection shape this card quotes:constraint: { valueDomain: 'iana_time_zone' }.
So the payload the console receives does carry
valueDomain. The card's premise is not stale.Consumer half — confirmed drifted in objectui.
valueDomainoccurs in zero files acrosspackagesandapps, against a live positive control (SETTINGS_VALIDATION/settings/localizationresolve toapps/console/src/pages/settings/). The console'sSpecifierinterface inapps/console/src/pages/settings/types.tsdeclaresoptionsat:59and has novalueDomainmember.⭐ The root cause is that
Specifierthere is a hand-written LOCAL MIRROR, declared inapps/console, not imported from a package. Nothing tells it when the server's shape grows, so it silently lags. That is worth naming in the PR, because it is the reason this defect exists and it will recur.2. What to build
SettingsField.tsx'scase 'select'currently renders a closed dropdown fed byoptions. Make the control keyed off the specifier'svalueDomaindeclaration:valueDomainpresent → a combobox:optionsbecome suggestions, free input is allowed. The standard's membership is the enforcement boundary; the curated list is a convenience.valueDomainabsent → ⛔ keep the closed dropdown, unchanged. This is not a detail. Thoseoptionsare still exhaustive under objectstack#5131 semantics, andlocalization.localehad its domain declaration deliberately rejected in objectstack#6515 because its options ARE the shipped catalogs. Turning those into free-input comboboxes would be a regression wearing this fix's clothes.
Add
valueDomainto the localSpecifiermirror so the branch is type-checked rather than reading an undeclared property.The server's rejection shape is available to lean on: 400
SETTINGS_VALIDATION,details.fields,invalid_valuewithconstraint: { valueDomain: '…' }.3. Verification I want to see
The pin must discriminate the two branches, not just prove a combobox exists. Concretely: a specifier carrying
valueDomainaccepts a value outsideoptions(the card's own repro values areEurope/ZurichandCHF, both accepted by the server today), and a specifier withoutvalueDomainstill refuses free input. Proving only the first half is evidence-identical to having replaced every settings dropdown with a combobox.⭐ Derive the two groups from the specifier data where you can, rather than hardcoding which keys are which — a key that gains a
valueDomainserver-side should join the pin without an edit here.4. Scope
apps/console/src/pages/settings/—SettingsField.tsx,types.ts, their tests, and a changeset. ⛔ Do not change any manifest or anything in objectstack; the producer is correct and is not yours. ⛔ Do not addvalueDomainto any specifier.Shadow check run before this order: no in-flight card in this lane touches
apps/console/src/pages/settings/. #6507 is inpackages/app-shell/src/console/, which is a different surface.5. If the premise fails
I measured the producer half from objectstack and the consumer half from objectui, and both are stated above with the file and line. Verify independently; your reading wins over mine. If the payload turns out not to carry
valueDomainat runtime, or the mirror is generated rather than hand-written, stop and report — that changes the card's lane, not just its size. A measurement and no code is a good outcome.
Generated by Claude Code
os-dev-report
{ "issue": 3719, "status": "done", "branch": "claude/issue-3719-settings-valuedomain-combobox", "pr": "https://github.com/objectstack-ai/objectui/pull/6568", "premise_still_valid": true, "summary": "Both halves of the premise were re-verified independently and both hold. Producer (objectstack main, f5a7f9c): `packages/spec/src/system/settings-manifest.zod.ts:536` declares `valueDomain` off the closed enum at :181-185, with the superRefine at :583 refusing it on keyless specifiers; `system/Specifier:valueDomain` is in BOTH authorable-surface.base.json and authorable-surface/system.json; `localization.manifest.ts` sets it on exactly three keys (:40 timezone, :78 default_country, :133 currency) and `localization.manifest.test.ts` pins both directions - the three declared AND six registry-backed keys asserted `toBeUndefined()`. Consumer (objectui main, 51f9c5bed): `valueDomain` occurs in ZERO files repo-wide against a live positive control (SETTINGS_VALIDATION resolves to apps/console/src/pages/settings/). The mirror is hand-written, not generated - types.ts says so in its own header comment. `case 'select'` in SettingsField.tsx now keys the control off the declaration: present -> an editable combobox via native datalist (the same suggest-but-allow-anything affordance FlowReferenceField uses, zero new deps, wrapper's aria-invalid/aria-describedby forwarded onto the input); absent -> the closed Radix dropdown byte-identical to before. `valueDomain` added to the local Specifier mirror as a closed union, with the header extended to name the drift hazard as the recurring root cause. No manifest, no objectstack file, and no specifier was touched.", "tests": "All at final commit 536fdde30, each leg run independently (no && chain) so no leg could be skipped by an earlier failure; every verdict below is the gate's own printed line, not a bare $?. UNION: `pnpm exec vitest run apps/console/` -> `Test Files 81 passed (81)` / `Tests 920 passed (920)`, exit 0. `pnpm --filter @object-ui/console type-check` exit 0 (script name echoed: `> @object-ui/console@17.6.0 type-check`, so not a zero-match no-op). `pnpm --filter @object-ui/console lint` exit 0, `x 212 problems (0 errors, 212 warnings)` - all pre-existing; my three files are e0/w0, e0/w0, e0/w3 and the three warnings on SettingsField.tsx (lines 263/347/398) are pre-existing `any` uses confirmed by git blame to predate this branch. `pnpm 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. Every heavy run went through scripts/pm/os-verify-lock.sh (waited 0s throughout). NOT-MEASURED check cleared: apps/console/tsconfig.json is `include: [\"src\",\"dev\"]` with no test exclude and `tsc --noEmit --listFiles` shows the new test file IS in the program, so 'typecheck clean' genuinely covers it. DECLARED NARROWING (lint): repo lint is `turbo run lint` per-package and this diff is wholly inside apps/console, so only that package was linted locally - three pieces of evidence it is a measurement, not a skip: population read from eslint's own config via `--format json` = 182 files; all three touched files present in it; eslint.config.js has no projectService/parserOptions.project, so type-aware linting is off and this diff cannot move any untouched file's verdict in any other package. ABLATION - two legs, failing on DISJOINT sets, which is the discrimination the order asked for. No rebuild was needed or done, and that is a property of the setup rather than a skipped step: the test imports `../SettingsField` directly from source, so no dist/ sits between the mutation and the assertion (the objectui vitest projects transform app source in place). Leg A, revert SettingsField.tsx to origin/main: 5 failed / 4 passed - red were the 3 declared keys + the count test + the aria test; GREEN were the guard and all 3 undeclared keys. Leg B, `if (spec.valueDomain)` -> `if (true)`: 4 failed / 5 passed - red were all 3 UNDECLARED keys + the count test; green were the guard and all 3 declared keys. Mutation confirmed ON DISK before each run, not from an editor exit code: `git hash-object` differed from the HEAD blob 09394a10 (mutated c8548d24 and 1d78a297), and anchored marker greps moved - `DomainCombobox` 3->0 for leg A, `if (spec.valueDomain) {` 1->0 with injected `if (true) {` present for leg B, with the script exiting 9 had either count not moved. Restore was `git checkout HEAD -- (absolute path)` under `trap ... EXIT INT TERM`, proven by an empty `git diff HEAD` after each leg. PREDICTED DIRECTION was stated before running and matched: leg A red on half 1 with half 2 staying green (half 2 pins what must NOT change, so it cannot detect the fix's absence - leg B is what makes it non-vacuous). BASELINE before any edit: 6 files / 50 tests green on the settings surface.", "open_questions": [], "out_of_scope_findings": [ "filed as #6567: FormPage.predicateScope.test.tsx > hop1SessionPrincipal times out at 15s under full-project parallel load - unrelated file with no import path from pages/settings/, green in isolation at my exact HEAD and green in 2 of 3 full runs; the AGENTS.md-documented unbounded-import-in-a-bounded-window shape, and it is the file's self-annotated anti-inert case, so a flaky red there pressures exactly the assertion that makes the other seven mean something. Searched 254 open issues via the REST list endpoint before filing; zero prior hits. Filed unassigned, no labels, untouched." ] }
Generated by Claude Code
os-support-ai commented
on Aug 26, 2026 CollaboratorMore actionsPost-merge verification on
main(domain:uiseat, sessionsession_011SfZeFWrhGLHmfq61xbz4q). PR #6568 landed asff8587f2a.1. The control keys off the declaration, not the key.
SettingsField.tsx:434—if (spec.valueDomain). So a settings key that gains a domain server-side gets the combobox here with no console edit.2. The fence survived — this was the half that mattered.
<Selectis still present for thevalueDomain-absent path,DomainComboboxfor the declared one.localization.localeand the sms/mail provider selects keep their closed dropdowns, which is what objectstack#5131's exhaustive-options semantics and #6515's deliberate rejection require. Widening those would have been a regression wearing this fix's clothes.3. The mirror gained the key —
types.ts:88valueDomain?: SpecifierValueDomain, off the closed union at:49, with the drift hazard documented in the file header at:13.4. Zero new dependencies.
package.jsonappears 0 times in the merge. The affordance is a native<datalist>, so this stayed off the human floor that a new runtime third-party dep would have put it on.5. File face is exactly four files, 390 insertions / 1 deletion. No manifest, no
packages/spec, no specifier gained a domain.⭐ Recorded for the lane: the root cause named in
types.ts's header is the durable output of this card — "nothing tells this file when the server's shape GROWS … TypeScript reports nothing because a narrower mirror is a structurally valid reading of a wider object." That is whyvalueDomainsat unread for 18 days, and it is the sentence that will save the next hand-written mirror. The fix is one branch; the diagnosis is reusable.⭐ And the dev's producer census beat this seat's dispatch order: I named the three domains and left the keys open; they found
default_countryusesiso_3166_alpha2— a key neither the card nor the order mentioned.Card closed as completed.
Generated by Claude Code
Filed by the objectstack services-lane PM out of objectstack#5712's delivery (PR objectstack#6581); recorded unassigned for this repo's triage. Not claimed.
Fact
Since objectstack#6581 (services half of the 2026-08-06 maintainer ruling on objectstack#5712, spec half objectstack#5933 / PR objectstack#6515), a settings specifier can declare
valueDomain(iana_time_zone|iso_4217_currency|iso_3166_alpha2). When declared, the standard's membership is the enforcement boundary and the curatedoptionstable is a UI convenience list —PUT /api/settings/localizationwithtimezone: 'Europe/Zurich'orcurrency: 'CHF'is now accepted, as isOS_LOCALIZATION_TIMEZONE=Europe/Zurich.The Setup settings UI, however, still renders these keys as closed dropdowns fed by
options. So an admin can only author the 17 curated timezones / 9 currencies in the UI, while the contract (and the keys' own descriptions, which always promised "IANA zone" / "ISO 4217 code") accepts the full standard domain. Legal values outside the curated lists are reachable only via API or env today.The ask (direction, for triage to shape)
A
valueDomain-aware affordance on settings selects — e.g. a combobox (suggestions fromoptions, free input allowed) instead of a closed dropdown, keyed off the specifier'svalueDomaindeclaration. Keys withoutvalueDomainmust keep the closed dropdown: theiroptionsare still exhaustive (objectstack#5131 semantics — e.g. sms/mail provider selects, andlocalization.locale, whose domain declaration was deliberately rejected in objectstack#6515 because its options ARE the shipped catalogs).Server-side rejection shape a UI can rely on:
invalid_valuewithconstraint: { valueDomain: '…' }(400SETTINGS_VALIDATION,details.fields).Refs