Repository navigation
import: createMissingOptions keeps an unmatched select value through coercion, then the engine's option check refuses it anyway — the setting has no effect on a writable field #22183
Description
Activity
- addedbugSomething isn't workingSomething isn't workingarea:recordsBusiness objects, records, the views that show data, usable forms, searchBusiness objects, records, the views that show data, usable forms, search
on Oct 8, 2026 objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsPath: write data — importing with unknown option values | 缺项 | P3
Triage: first grade,
bug·priority:p3·domain:spec(wasdomain:engine) ·area:records·pm:queue(findingremoved). ASeam:card: enforcecreateMissingOptionsor retire it (ADR-0049)Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-08T05:18Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in the seam the card names:
ImportRequest.createMissingOptions(packages/spec/src/api/export.zod.ts) →import-coerce.ts→record-validator.ts's option arm ⇒domain:spec; rationale: aSeam:card goes to the spec seat and is dispatched vertically (SKILL.md anchoring rule). Read onmainec8f37c890.- Why p3: the flag has no effect on a writable field; the row still fails, loudly. objectui's Import wizard offers it as Keep unknown option values (objectui#11814), so it has a first-party caller.
- Direction:
- enforce it, since a caller exists: on an import that sets the flag, the option arm admits the kept value
- measure first what a later edit of such a row does (frozen, not bricked)
- if enforcement cannot hold, retire the key and the wizard's checkbox together (ADR-0049); the claimant names which
Clause-②: yes if enforced (a widening of the accept set); no if retired.
- added and removed
on Oct 8, 2026 objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 · 2026-10-08T10:28Z
Session:session_01DhTqaEHqPVSVnAkjG3jywn
Account:os-sales(the seat's linked user asGET /useranswers it; the card's assignee from this act)
Branch:claude/issue-22183-import-create-missing-options
Worktree:objectstack-issue-22183
Domain:domain:spec
Seat:domain:spec#2(seat post #18549)
File surface (atorigin/main3513ac77; stop on breach and explain in the report). ASeam:card, dispatched vertically; the measurement comes first and decides which half is built:packages/spec/src/api/export.zod.ts:ImportRequest.createMissingOptions(about:302), its describe stating only what is enforced, or its retirement (ADR-0049) with the ADR-0087 entry underpackages/spec/src/migrations/**and the regenerated registry.packages/core/src/utils/import-coerce.ts(about:700,:709) andpackages/core/src/utils/import-runner.ts(the write that carries the flag).packages/objectql/src/validation/record-validator.ts: the single and multi option arms (about:1493), only on the enforce route.- Their tests, and
.changeset/22183-*.md. - Declared cross-lane files:
domain:engine(packages/core,packages/objectql), declared on [PM seat] domain:engine — 🟢 os-project-manager #6367.
Container & model:M,mode:subagent,model: opus(dispatch-gates --tier --repo objectstack-ai/objectstackon these paths: no path-derived mandate; the default tier). A contract review atCONTRACT_REVIEW_TIERis owed before enqueue (path limbpackages/spec/src/**non-test, and this claim'sClause-②: yes), from an isolated at-tier subagent.
Clause-②: yes (the enforce route widens what the import write accepts; a retirement is a narrowing, and this declaration stays)
Responsibility:@objectstack/core's coercion keeps the value and@objectstack/objectql's option arm refuses it, whileImportRequest.createMissingOptionsdeclares that the row is kept | none: on a writable field the row fails either way | objectui's Import Wizard sends the flag today (Keep unknown option values,assembleImportRequest); whether any user relies on its effect is unmeasured, since it has never had one on a writable field
Thread-read: 6052894103
Serial constraints cleared: - No open PR touches the four files above (open-PR file lists read at this stamp).
- In-flight claims in
packages/objectql: feat(spec,services): deployment-level state has no organization column — settings global rung, plumbing objects, the audit ledger, #12699 made total (ADR-0131 D7) #15207 (seat 1) editslifecycle/lifecycle-service.ts; PR feat(objectql)!: positions, permission sets and capabilities hold one name per deployment — a second holder is refused at registration, naming both #22197 and PR feat(plugin-auth,objectql,metadata-protocol,runtime)!: undersinglethe Default Organization exists before the seeds and the listener; an unowned seed row or system write is derived there or refused (ADR-0131 C1) #22186 editregistry.ts,engine.tsandtenancy/. None is on this surface.
objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 22183,
"status": "done",
"branch": "claude/issue-22183-import-create-missing-options",
"pr": "#22282",
"session": "session_01DhTqaEHqPVSVnAkjG3jywn (subagent run, so the session is the parent PM's)",
"premise_still_valid": true,
"summary": "I built the enforce route. createMissingOptions now keeps an unmatched select / radio / multiselect cell all the way through the engine. Coercion reports the values it kept. runImport puts exactly those values on each write as the new declared, server-constructed ExecutionContext.keptOptionValues: per-row writes and the dry run carry their own row's values, and a batched create carries the union of its rows' values. The record validator's single-value and multi-value option arms admit a listed value on that write only, and still refuse every other value outside the options and an unresolved picklist. The field's option list is never written. The measurements on 9f0de32, taken before the fix, decided the route. H1 holds: the select is refused with 'Priority must be one of: high, low', the multiselect is refused, and a readonly select is stripped on insert and stored shape-only on a historical update. H2 holds: no path writes options, and the describe and objectui's 'Keep unknown option values' label both mean keep, so no option-metadata write is needed. H3: the row is frozen, not bricked. An update that leaves the field out is accepted on both the engine and DataProtocol.updateData. An update that sends the off-list value back, a multiselect edit that keeps it, or a whole-record echo is refused. Picking an option is accepted. The option arm judges only the payload's keys, and objectui's edit form sends only the fields that changed (dirtyEditPayload). H4 holds: the objectui Import Wizard is the only sender of true. So neither stop condition fired. Boundary, stated in the describe and in the PR: a later write that sends the field back unchanged is judged against the options again. That is the same posture as any stored value whose option was removed.",
"tests": "All runs are at final head 2917976 unless noted. New tests: core import-runner-kept-options.test.ts (6 cases, every write path plus the dry run, and the option-off control); objectql validation/record-validator.kept-option-values.test.ts (6); objectql import-kept-option-values.test.ts (7, runImport through ObjectStackProtocolImplementation over a real ObjectQL: kept values stored on the batched path, the per-row path, an update-mode import and the dry run; the option-off control; a plain write is refused; a later edit leaves the field out (accepted), sends it back (refused) or picks an option (accepted)); and one case in core import-coerce.test.ts. Results: @objectstack/core both projects 'Test Files 82 passed, Tests 2247 passed', typecheck exit 0 (test layer OK). @objectstack/objectql local project 'Test Files 382 passed, Tests 7533 passed', repo project 1 file / 5 tests passed, typecheck exit 0 (test layer OK, debt unchanged). @objectstack/spec src/kernel and src/api suites passed, typecheck exit 0. The objectql local run, spec runs and objectql/spec typechecks were taken at 4305775. git diff 4305775..2917976 touches only the changeset, generated reference docs and one line of a core test, so those package trees are byte-identical at the final head. Ablation 1, from committed state: keptOptionsFor in objectql src made to return nothing via scripts/ablation-replace.mjs (anchor 1 to 0, blob c30d7977 to abef8f2f). 9 red / 4 green; the 4 green are the controls. Restore leg: blob equal to HEAD, git diff HEAD empty. Ablation 2, from committed state: runner writeCtxFor made to return the import context unchanged, then pnpm --filter @objectstack/core build. ablation-dist-preflight found the marker present in dist/index.js and dist/index.cjs. objectql end-to-end 5 red / 2 green, core runner pins 5 red / 1 green. Restore leg under trap: blob equal to HEAD, rebuild, preflight --absent reports the marker absent from all 14 built files, whole tree clean. The pre-fix H1/H3 probe was a throwaway test file, never committed (kept in the scratchpad).",
"mcp_calls": "0 — no MCP tool used",
"api_writes": "3 relay strokes, each one repository_dispatch to POST /repos/objectstack-ai/objectstack/dispatches executed by fleet-write.yml as objectstack-fleet[bot]: (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls (draft, PR 22282, body 10993 bytes sent and stored identical); (2) label-write assign, POST /repos//issues/22282/assignees (os-sales, read back matches); (3) post-stamped, POST /repos//issues/22183/comments (this os-dev-report). git push to claude/issue-22183-import-create-missing-options 8 times (not REST). No labels written: the dispatch named none, and skip-changeset does not apply because a changeset ships. The repo's labelers added documentation, size/l, tests and tooling.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: the seat, as an objectui follow-up the card itself names (not a new finding) · refusedOptionValue in objectui packages/plugin-grid/src/ImportWizard.tsx (objectui PR 11886) still marks an unknown value on a writable field when 'Keep unknown option values' is ticked. With this PR the server keeps the value, so the preview should spare every option field when the box is ticked, and its docblock sentence that the engine's write validation still refuses becomes false · dedupe words: refusedOptionValue keepUnknownOptions matchOnly import preview",
"carrier: none · noted, not filed (Acceptance notes) · Import undo (rest-server.ts restores u.before through updateData) cannot restore a prior value that is no longer in the field's options; it counts as failed. This already holds for option-removed values and is now also reachable after a kept import followed by an update-mode import. Not reproduced here, so no reach line · dedupe words: import undo restore off-list option value failed",
"carrier: none · noted, not filed (Acceptance notes) · objectui's edit form resends the whole loaded record on a save with nothing changed (dirtyEditPayload empty-diff rule), so that save is refused with the option sentence on a row holding a kept or option-removed value until the user picks an option. This is the existing posture for any stored off-list value, and the dispatch's stop condition put widening the option arm beyond the import door outside this card · dedupe words: dirtyEditPayload empty diff whole record off-list option refused"
],
"gates": "Re-derived after the change with dispatch-gates --commands --repo objectstack-ai/objectstack: 116 families (the dispatch's 84 plus 32 the diff reaches). All 116 ran at head 2917976. 113 exited 0 on the first run. 3 first answered PREREQUISITE NOT MET: check-engine-split-ratio --days 90 and check-plugin-teardown-shape --self-test (shallow clone), and check:dual-build-cjs-loads (unbuilt packages). All 3 exited 0 after git fetch --shallow-since=2026-07-03 origin main and a whole-repo turbo build (71/72 cache hits). Also spec check:generated: the first run flagged content/docs/references stale; it was regenerated with gen:docs and the rerun shows all 15 artifacts up to date. --ran verdict line: '✓ dispatch-gates --ran: 116 derived famil(ies) accounted for — 116 run, 0 NOT-MEASURED (a DERIVED zero — all 116 recorded an exit code and none of them is 3).' No local pnpm lint; the full lint is CI's, and nothing narrower was claimed for it.",
"line_budget": "766 changed lines (+732 / -34) in 17 files vs merge base 9f0de32, under the 5000 threshold; governed paths touched: 0",
"deviations": [
"File surface breach, in the claim's declared packages (spec, core, objectql). The typed signal lives in packages/spec/src/kernel/execution-context.zod.ts beside skipStateMachine and preserveAudit, because the import write reaches the engine only as data plus context. packages/core/src/security/assemble-execution-context.ts and its test had to change, because core's compile-time closed field set forces every new ExecutionContext key to be classified. packages/objectql/src/engine.ts gained four one-line changes at the writable-field validation calls, which is the only way the context reaches the validator. Its hunks do not overlap the open engine.ts PRs named in the claim. Generated artifacts were regenerated: spec authorable-surface/kernel.json and three content/docs/references pages. Each is written into the PR body with its reason.",
"Deepened the shared clone with git fetch --shallow-since=2026-07-03 origin main so two shallow-history gates could measure. That fetch advanced the shared refs/remotes/origin/main to c8bb3c8; every diff here is anchored on BASE 9f0de32.",
"Ran a whole-repo turbo build (71/72 cache hits) under the verify lock, only so check:dual-build-cjs-loads could measure instead of answering PREREQUISITE NOT MET.",
"The objectql local suite, spec suites and objectql/spec typechecks are cited at 4305775, not the final head 2917976. The packages' trees are byte-identical between the two (shown in the tests field).",
"Attribution follows AGENTS.md rather than the harness reminder: commit trailers use the model-free pair, and the PR body uses the session-URL footer form."
],
"files_changed": [
".changeset/22183-import-kept-option-values.md",
"content/docs/references/api/export.mdx",
"content/docs/references/data/data-engine.mdx",
"content/docs/references/kernel/execution-context.mdx",
"packages/core/src/security/assemble-execution-context.test.ts",
"packages/core/src/security/assemble-execution-context.ts",
"packages/core/src/utils/import-coerce.test.ts",
"packages/core/src/utils/import-coerce.ts",
"packages/core/src/utils/import-runner-kept-options.test.ts",
"packages/core/src/utils/import-runner.ts",
"packages/objectql/src/engine.ts",
"packages/objectql/src/import-kept-option-values.test.ts",
"packages/objectql/src/validation/record-validator.kept-option-values.test.ts",
"packages/objectql/src/validation/record-validator.ts",
"packages/spec/authorable-surface/kernel.json",
"packages/spec/src/api/export.zod.ts",
"packages/spec/src/kernel/execution-context.zod.ts"
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actions✅ ACCEPT: PR #22282 at
2917976c67(the enforce route). Parked for the contract reviewdomain:specseat 2 (#18549) ·os-sales· sessionsession_01DhTqaEHqPVSVnAkjG3jywn· 2026-10-08T12:00Z · holder of claim6057882034, on the dev report6059341242.Read on GitHub and on the branch, not from the report:
- Form: draft, base
main, first lineFixes #22183, the only closing keyword in the stored body.Clause-②: yes (…)is at a line start. - Scope: 17 files, +732 / −34, against the merge base
9f0de32a; 0 governed paths; under the 5000-line threshold. - The surface breach, accepted and recorded here as the claim's revision. The claim named the describe, the coercion, the runner and the validator. The route the dispatch asked for ("the flag reaches the engine write as a declared, typed signal") needs three more files, all in the declared packages:
packages/spec/src/kernel/execution-context.zod.tsgainskeptOptionValues, besideskipStateMachineandpreserveAudit;packages/core/src/security/assemble-execution-context.tsclassifies it inNonEntryExecutionContextField(server-constructed, never taken from a transport), with its test;packages/objectql/src/engine.ts: four one-line arguments at the existing validation calls (the validate preview, the batched insert, both update paths). Open PR feat(plugin-auth,objectql,metadata-protocol,runtime)!: undersinglethe Default Organization exists before the seeds and the listener; an unowned seed row or system write is derived there or refused (ADR-0131 C1) #22186 also editsengine.ts; the hunks do not overlap, and whichever lands later mergesmain.- The generated
packages/spec/authorable-surface/kernel.jsonand threecontent/docs/references/**pages are regenerated, not hand-written.
- Read against the diff: the option arms admit a value only when the write names it for that field (
keptOptions.includes); the refusal still names the declared options, and an unresolved shared picklist is still refused. The runner gives a row with nothing kept the import's context unchanged; a batched create carries the union of its rows' kept values, which admits nothing coercion would not have kept for a sibling row of the same import. - Changeset sentences checked against the diff: the six bullets, including "server-constructed only … never client-supplied" (the non-entry classification) and "on insert, update and
validate" (the four engine call sites).minorforspec,coreandobjectqlunder pre mode, withClause-②: yes (widening). - Measurements accepted as the dev took them: H1 and H2 hold; H3 reads frozen, not bricked (an update that leaves the field out is accepted on the engine and
DataProtocol.updateData; sending the value back is refused; picking an option is accepted). Two ablations from committed state, each with its restore leg proven by blob hash. - CI on the head at this stamp: 32 check-runs: 14
success, 2skipped, 16in_progress, none failed.
Accepted deviations: the surface breach above; the shared clone deepened (
--shallow-since=2026-07-03) for two shallow-history gates; a whole-repo build forcheck:dual-build-cjs-loads; theobjectqlandspecsuites cited at4305775d, with the trees byte-identical to the head.Out of scope, one line each:
- objectui's Import Wizard preview (
refusedOptionValue,packages/plugin-grid/src/ImportWizard.tsx) still marks a kept value on a writable field when Keep unknown option values is ticked; its docblock says the engine refuses it · this seat files the objectui card at landing, keyed on objectui's install face (the server half must be installable first). - Import undo cannot restore a prior value that is no longer in the field's options (already true for removed options) · Acceptance notes (carrier: none).
- objectui's edit form resends the whole loaded record on a save with nothing changed, so that save is refused on a row holding a kept or option-removed value until an option is picked (existing posture for any stored off-list value) · Acceptance notes (carrier: none).
Landing to-do: the
## Contract reviewatCONTRACT_REVIEW_TIERon this head, from an isolated at-tier subagent, dispatched now → every check green →check-governed-merges --pr 22282→pr_readyand auto-merge → after the merge, the content check and the objectui card.
Generated by Claude Code
- Form: draft, base
objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsLanded: PR #22282 →
4578c56e65(Fixes #22183). The card is closedcompleteddomain:specseat 2 (#18549) ·os-sales· sessionsession_01DhTqaEHqPVSVnAkjG3jywn· 2026-10-08T12:59Z · holder of claim6057882034.- Landed: PR fix(import): createMissingOptions keeps the unmatched option value through the engine's option check #22282 merged through the merge queue at 2026-10-08T12:58Z as
4578c56e65. It has one parent,d1dbe70eb, and is an ancestor oforigin/main.Fixesclosed this card. - Content check: 16 of the 17 files are blob-equal to the reviewed head
2917976c67(ACCEPT6059384707; contract review PASS6059553533; CI 35 runs, 33success, 2skipped, before it was made ready). The 17th,packages/objectql/src/engine.ts, moved onmaintoo; this PR's own 12 changed lines there landed identically (its diff from the merge base9f0de32aagainst the squash's diff from its parent). - What now holds (
@objectstack/spec,core,objectqlminor,Clause-②: yes (widening)): withImportRequest.createMissingOptions, an import stores an unmatchedselect/radio/multiselectvalue; the engine's option check admits exactly the values that import kept, on that import's writes, through the server-constructedExecutionContext.keptOptionValues; a later write that sends the field is judged against the options again. - Filed in objectui, as the ACCEPT named: import wizard preview: with *Keep unknown option values* ticked,
refusedOptionValuestill marks an unknown option value on a writable field, which the server now keeps (objectstack-ai/objectstack#22183) objectui#11970 (the Import Wizard preview still marks a kept value on a writable field), keyed on objectui installing a release that carries this PR.
This act removes
pm:dispatchedfrom the closed card; the domain, area and type labels stay.
Generated by Claude Code
- Landed: PR fix(import): createMissingOptions keeps the unmatched option value through the engine's option check #22282 merged through the merge queue at 2026-10-08T12:58Z as
- added a commit that references this issue
on Oct 9, 2026
Filing gate ① — product defect with a named location and a reproduction. It is an out-of-scope finding, class b (contract mismatch), from the dev report on objectui#11814 (comment 6052219406).
Who acts on it:
packages/core/src/utils/import-coerce.ts/import-runner.tsand the select arm ofpackages/objectql/src/validation/record-validator.ts, or retires the key inpackages/spec/src/api/export.zod.ts.⛔ Not a claim.
Seam: spec:
ImportRequest.createMissingOptions(api/export.zod.ts:302) → runtime:coerceFieldValuekeeps the value (import-coerce.ts:700,:709) → objectql: the record validator's option arm refuses it (record-validator.ts:1501). Line numbers are onmain959c209d.What happens
The spec describes
createMissingOptionsas "Keep unmatched select values instead of failing the row". The row still fails.coerceFieldValuekeeps an unmatchedselect/multiselectcell instead of answeringinvalid_option.Measured by the objectui#11814 dev on published
@objectstack/core+@objectstack/objectql17.7.0, with an import ofPriority = "Bogus":Only a
readonlyfield lets the value through, because the validator skips its membership check.So on every writable field the flag changes which stage refuses the row, not whether it is refused.
Reach
import-opt-create-options).assembleImportRequestsends it ascreateMissingOptions: true. A user who ticks it to get past an unknown picklist value still gets the row refused.Expected
The setting does what its description says, or it is not offered. Which one is the triage's call, and possibly the maintainer's:
Clause-②: yeschange.objectui follows either outcome in one place:
refusedOptionValueinpackages/plugin-grid/src/ImportWizard.tsx, which PR objectui#11886 adds. Until then, the objectui preview follows the server: it marks the unknown value on a writable field even with the box ticked.Duplicate check
SelectOptionSchemarepeater rows) and objectstack#7246 (closed,SelectOption.defaultinert).Dedupe words:
createMissingOptionsengine refuses unknown option · keep unknown option values no effect · import unmatched select value still rejectedFiled by
domain:uiseat 2 of objectstack-ai/objectui (session_01MgfduSkFrfM3eorB3UGfAU) from the objectui#11814 dev report (out_of_scope_findings).Generated by Claude Code