Skip to content

feat(fields): a lookup pick fills the fields its lookupAutofill declares, in the record form and the line-item grid - #28322

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-t2cn2e
Oct 31, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-t2cn2e

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Tesla · project thread

Fixes #28245

Clause-②: yes (widening: @object-ui/fields gains the widget prop onAutofill, the GridField prop lookupAutofill, an optional fourth argument on lookupAutofillPatch, and the exports declaredLookupAutofillPatch, lookupAutofillByField, LookupAutofillPair, LookupAutofillByField; @object-ui/types LookupFieldMetadata gains lookupAutofill; measured on the built declarations; no existing member, key or shape changes)
Metadata-protocol: none

维护者速读

  • 改了什么:查找字段(lookup)选中一条记录后,按字段上声明的 lookupAutofill 立刻把对应字段填进表单;明细行网格里选产品时同样生效(例如报价明细选产品,单价立即带出产品的标价),不再要等保存后服务端钩子才填。
  • 为什么改:feat(spec): a lookup field declares which fields a pick fills (lookupAutofill) #28244 已在 spec 里声明了 lookupAutofill,但界面上没有任何读取方,作者写了也不生效;本 PR 是它的 ui 一半,并把 liveness 行从 planned 改为 live。
  • 风险与回滚:默认不覆盖用户自己填的值,只填空字段或上一次选择填进去的值;只有声明了 overwrite: true 才覆盖。没有声明该键的查找字段行为不变。网格里从「浏览全部」弹窗选记录现在也会走同名字段复制(之前只写 id)。回滚即 revert 本 PR。
  • 先看:ui/packages/fields/src/widgets/lookupAutofill.ts(填充规则,唯一一处),ui/packages/fields/src/widgets/LookupField.tsx 的 fillDeclaredTargets 与 handleDialogSelect,ui/packages/fields/src/widgets/GridField.tsx 的 applyLookupSelection。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/plugin-form/src/ObjectForm.lookupAutofill-28245.test.tsx packages/fields/src/widgets/GridField.lookupAutofill-28245.test.tsx,预期 2 个文件 17 passed。
  • 需要你做的:无,席位落地。

What changes

FieldSchema.lookupAutofill ([{ target, source, overwrite? }] on a single-value lookup / master_detail, declared by #28244) gets its reader in the record form and in the line-item grid.

  • One fill rule — ui/packages/fields/src/widgets/lookupAutofill.ts#declaredLookupAutofillPatch. Each target takes the picked record's source. Without overwrite: true a target is filled only while it is empty or still holds the value the earlier pick filled in (the record the lookup held from an earlier pick, read at the same source); a value the user typed, or one the record already stored, stays. overwrite: true replaces on every pick. A source the picked row does not carry (not served to this reader) writes nothing. lookupAutofillByField reads an object's pairs by lookup field name, single-value relationships only.
  • Record form — LookupField#fillDeclaredTargets runs on every single-value pick path the form owns: the dropdown, the keyboard, quick-create and the browse-all dialog (the dialog commits its id through onChange, then hands the row to onSelectRecords, where the fill runs). Current values are the host's live record (dependentValues); the earlier-pick memory is the widget's own pickedRecords, so it is local to the form instance. The writer is a new widget prop, onAutofill(patch), declared on FieldWidgetComponentProps and added to toHostProps' host-plumbing list.
  • Form renderer — ui/packages/components/src/renderers/form/form.tsx hands lookup / master_detail widgets onAutofill, which writes each target with form.setValue(..., { shouldDirty: true }) (re-validating a target already in error), and passes it through stripRegisteredFieldProps for those two types only. Nothing in the submit or save path changes.
  • Line-item grid — GridField takes the child object's pairs through a new host prop, lookupAutofill (an InlineGridColumnSchema column is spec-strict and carries no field definition). MasterDetailForm keeps them on each resolved entry beside sortField; LineItemsPanel derives them from the child schema it already loads. lookupAutofillPatch applies a declared pair whatever the column's autofill says and takes its target out of the same-named copy, so a target has one writer per pick. The earlier-pick memory is a per-grid map keyed by column and the row's own lookup value, so adding, removing or reordering rows cannot misattribute it.
  • Browse-all dialog in the grid — LookupField#handleDialogSelect holds back the dialog's bare-id onSelect when the host owns the pick (onSelectRecord), and handleDialogSelectRecords hands the host the record once, so the grid's autofill runs on that path too. Clearing and multi-value picks commit through onChange as before. The PeoplePicker path is unchanged.
  • Types — LookupFieldMetadata.lookupAutofill is typed SpecField['lookupAutofill'].
  • Liveness — packages/spec/liveness/field.json lookupAutofill (and its three children) moves planned → live, citing the read points above and the two test files.
  • Changeset — .changeset/28245-lookup-autofill-fill.md, patch on the four @object-ui/* packages and @objectstack/spec (the ledger ships in its files[]).

Tests

  • ui/packages/plugin-form/src/ObjectForm.lookupAutofill-28245.test.tsx — end to end through ObjectForm (schema with the pairs → form renderer → LookupField): a pick fills an empty target and the filled value is saved; a typed value is kept while an overwrite: true pair replaces its own; a value an earlier pick filled is replaced by the next pick and kept once the user edits it; a browse-all dialog pick fills; control: a lookup without the key fills nothing, not even a same-named field.
  • ui/packages/fields/src/widgets/GridField.lookupAutofill-28245.test.tsx — the pure patch (empty target from the declared source rather than the same-named field; a written value kept because the declared target leaves the same-named copy; overwrite: true; earlier pick replaced; applies under autofill: false; control: no pair = the same-named copy, unchanged) and the grid itself (a cell pick fills the row and the next pick replaces it; a browse-all pick reaches the grid once and fills; a stored line's own price is kept).
  • Ablation (one-shot, at 0d887457, node scripts/ablation-replace.mjs): the write in declaredLookupAutofillPatch (patch[target] = picked[source]) replaced by a no-op → 10 failed | 7 passed of the 17 (every fill case red; the keep and control cases stay green, as they should); restored, blob equal to HEAD, git diff HEAD empty.
  • Reverse type check: a lookupAutofill={{ product: [{ target: 'unit_price' }] }} grid prop in @object-ui/plugin-form, compiled against the rebuilt @object-ui/fields declarations → TS2741: Property 'source' is missing; file removed.

Acceptance notes

  • Surface extension, agreed with the seat before editing. The order's surface named ui/packages/plugin-form/** as the form renderer, but the record form's state lives in ui/packages/components/src/renderers/form/form.tsx (react-hook-form), and no widget could write a sibling field. That file carries only the writer prop and its stripRegisteredFieldProps pass-through. ui/packages/types/src/field-types.ts carries only the by-reference lookupAutofill declaration.
  • Bound: a fully authored master-detail collection. MasterDetailForm skips the child-schema fetch for an entry whose relationship and every column are authored, so such a grid sees no pairs and fills only the same-named copy. Loading the child schema for every such entry is a fetch-policy change outside this card. Derived, hydrated and LineItemsPanel grids are covered.
  • Earlier-pick memory is per widget / grid instance. A remount forgets it, which errs toward keeping the target's value, never toward overwriting it.
  • A target the caller may not edit is not saved. Read in sanitizeFormData (stripUnwritable with canEdit): the form's save drops a field the caller may not edit, so a fill into such a target never reaches the server. The fill does not check field-level security itself.
  • Behaviour change on an existing path: in a line-item grid, a pick from the browse-all dialog used to write only the id; it now runs the column's same-named copy as the dropdown always has.
  • skills/docs: teach lookupAutofill (lookup-pick fills declared fields) once the ui half fills the form #28246 (skills/docs) must merge after this PR, not with it. It edits skills/** (governed, Tier H), so riding along would make this PR Tier H; its card already carries Blocked-by: #28245. Its text should also state the fully authored master-detail bound above.

Verification

All readings at head 12601c27d unless a step names another commit.

  • New tests — pnpm --dir ui exec vitest run packages/fields/src/widgets/GridField.lookupAutofill-28245.test.tsx packages/plugin-form/src/ObjectForm.lookupAutofill-28245.test.tsx → Test Files 2 passed (2), Tests 17 passed (17).
  • Touched packages' suites — pnpm --dir ui exec vitest run packages/fields/ packages/plugin-form/ packages/components/src/renderers/form/ packages/types/ (under os-verify-lock, at 0d887457; later commits touch only the liveness ledger and changeset) → Test Files 973 passed | 1 skipped (974), Tests 17791 passed | 25 skipped (17816), VERDICT command-exit 0.
  • Type-check — pnpm --filter @object-ui/types --filter @object-ui/fields --filter @object-ui/components --filter @object-ui/plugin-form run type-check against the rebuilt @object-ui/plugin-form^... closure (full declarations) → VERDICT command-exit 0.
  • Clause-② measure — on the built declarations: ui/packages/fields/dist/widgets/types.d.ts carries onAutofill, dist/widgets/GridField.d.ts the lookupAutofill prop and the fourth lookupAutofillPatch argument, dist/widgets/lookupAutofill.d.ts the new exports; ui/packages/types/dist/field-types.d.ts carries lookupAutofill. Additions only.
  • Lint — eslint --no-inline-config over the 12 changed ui/ source and test files → 0 errors.
  • Derived gates — node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, every line run with stdin closed, exit codes landed on disk first, reconciled with --ran: 70 derived famil(ies) accounted for — 69 run, 1 NOT-MEASURED, exit 0. Every run family exits 0, including check:liveness, check:generated, check:sdui-manifest (+ --self-test), check:type-check-debt, check:test-source-alias, check:cross-package-test-inputs and pnpm --dir ui exec vitest run scripts/.
    • check:liveness first went red on the generated liveness/state-counts/field.md shard (planned 9 → 6, live 100 → 103); regenerated with gen:liveness-counts in 12601c27d, then green.
    • check-plugin-teardown-shape --self-test first answered exit 3 on the shallow clone; after fetching its pinned fixture commit, 48 cases pass, exit 0.
    • NOT MEASURED: check:dual-build-cjs-loads, exit 3 (PREREQUISITE NOT MET — it needs every package's dist, a full pnpm build); the diff touches no @objectstack/* package entry. CI runs it.
  • pnpm --dir ui run check:new-line-citations → 0 new citations, exit 0.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tooling labels Oct 31, 2026
@github-actions

github-actions Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️ 2 changed file(s) yielded no anchor (packages/spec/liveness/field.json, packages/spec/liveness/state-counts/field.md), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/liveness/field.json, packages/spec/liveness/state-counts/field.md) — pages documenting those are invisible to this run
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 168 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json a8d2fd7f3014e488694ed214c9aaeffe9b2c042c → packageMentionDocs.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract-review-claim: session_01L9qibw58ehQ2gRHP2h2Puz · head 12601c27dca06c6c1034dd0a77623bc7d7452722 · 2026-10-31T04:45Z

… censuses

The relational copy-set table gains a spec row read by the lookup editor
only (the grid hands its pairs through GridField's own prop, so it stays
off the copy set), and the form-field channel ledger gains a runtime-only
row: the pairs are object-field metadata read off the resolved field stash.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibzEPqxBYQbxn2d72r5
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract-review-claim: session_01L9qibw58ehQ2gRHP2h2Puz · head 48f2c159c31f23d2ec75592acc6896e07222ef08 · 2026-10-31T05:11Z (re-claim after the head moved from 12601c27dca0; the earlier claim 6539172040 lapses)

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract-review-claim: session_01L9qibw58ehQ2gRHP2h2Puz · head 48f2c159c31f23d2ec75592acc6896e07222ef08 · 2026-10-31T05:14Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 48f2c159c31f23d2ec75592acc6896e07222ef08
Local-runs: none
Scope: ui/packages/fields/src/widgets/lookupAutofill.ts (whole: declaredLookupAutofillPatch, lookupAutofillByField, the two types), ui/packages/fields/src/widgets/LookupField.tsx (autofillPairs, fillDeclaredTargets, handleSelect, handlePickerSelectRecords, handleDialogSelect, handleDialogSelectRecords, the picker wiring), ui/packages/fields/src/widgets/GridField.tsx (lookupAutofillPatch's fourth argument, the lookupAutofill prop, applyLookupSelection, pickedLookupRecords), ui/packages/fields/src/widgets/types.ts (onAutofill), ui/packages/fields/src/widgets/toHostProps.ts, ui/packages/fields/src/index.tsx, ui/packages/components/src/renderers/form/form.tsx (LOOKUP_AUTOFILL_WIDGET_TYPES, the onAutofill writer, stripRegisteredFieldProps), ui/packages/plugin-form/src/MasterDetailForm.tsx (DetailEntry.lookupAutofill, both entry builders, the grid prop), ui/packages/plugin-form/src/LineItemsPanel.tsx (lineLookupAutofill), ui/packages/types/src/field-types.ts (LookupFieldMetadata.lookupAutofill), packages/spec/liveness/field.json (the lookupAutofill row and its three children), packages/spec/liveness/state-counts/field.md, the two test files, ui/examples/schema-catalog/test/form-field-channel-census-12130.test.tsx (the lookupAutofill ledger row), ui/packages/plugin-grid/src/relationalMetaKeys.ts (the lookupAutofill read-set row), .changeset/28245-lookup-autofill-fill.md + packages/spec/src/data/field.zod.ts on main (the lookupAutofill describe, the rule this PR implements)

Inputs: card #28245 (body and all six comments: triage 6531259211, the unblock 6535248023, the seat's claim 6538361669, its two amendments 6538388719 / 6539108226 which add the two surface files and flip Clause-② to widening, the dev report 6539093606), PR #28322 (body, the docs-drift comment, the file list of 17 files, the diff of 994 lines against main read whole on 12601c27 plus the six-line delta of 48f2c159, the five commits from merge-base 73b7270), the 64 check-runs on this head (read 2026-10-31T05:39Z: all 64 runs completed, none failed, none cancelled, none in progress; success: Auto Label, Build & E2E UI, Build Core, Build UI Deps, Check Changeset, Check PR Size, Dogfood Regression Gate, Flag docs affected by code changes, Governed Surface Queue Guard, Lint & Repo Gates, Lint UI, Lint · CI, release & engine gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Lint · scoped · tooling self-tests, Live E2E UI, PR-body guards (claim · duplicate · Part-of · single-writer path), Plan Test Core shards, Spec property liveness, Test Core, Test Core (all 8), Test UI, Test UI (all 16), Type Check UI, Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates, Type Check · workspace, TypeScript Type Check, filter; skipped: 18.x upgrade harness (opt-in), Build Docs, Dogfood Regression Gate (matrix shard/3), Dogfood Verify CLI, Packed-tarball smoke (opt-in), Schedule gate · skip when a green push run covers the commit, Temporal Conformance (live PG + MySQL), UI Published Artifacts), and the tree at this head for handleSelect (the setPickedRecords updater runs before fillDeclaredTargets, which reads the pre-pick list from its closure, so "the record the field held from an earlier pick" is the right one) and for the spec describe on main. The head is mergeable and clean against today's main (merge-tree clean); none of the files main moved since the merge-base is in this diff.

① Derived judgments

  • The fill rule is the spec's, in one place - right. declaredLookupAutofillPatch(pairs, picked, current, previous): each target takes picked[source]; a source the picked row does not carry (absent key or undefined) writes nothing; without overwrite: true the target is written only while it is empty (undefined, null, '', []) or still holds the value the earlier pick's record carried at the same source (previous, compared by value); overwrite: true writes on every pick. That is the describe on main word for word (fill while empty or still holding the earlier pick's value; TRUE replaces whatever the target holds). Pinned nine ways in the pure tests, the control included.
  • The record form reads it on every single-value pick path - right. autofillPairs is the field's lookupAutofill only when the widget is single-value, the host owns no onSelectRecord and supplies onAutofill; fillDeclaredTargets runs after onChange in the popover and keyboard path (handleSelect), from handlePickerSelectRecords for the browse-all dialog (whose onSelect committed the id just before), and quick-create lands through the same handleSelect. Current values are the host's live record (dependentValues); the earlier-pick memory is the widget's own pickedRecords, so a value the stored record held was never "picked here" and its targets stay. The form renderer's onAutofill writes each target with form.setValue(..., { shouldDirty: true }), re-validating only a target already in error, and the prop reaches lookup / master_detail widgets alone (stripRegisteredFieldProps), so no other widget spreads an unknown handler onto its DOM. Pinned end to end through ObjectForm: fill and save, typed value kept, overwrite: true replaces, an earlier pick's value replaced then kept once edited, the dialog path, and the control (no key: nothing fills, not even a same-named field).
  • The line-item grid, and the one-writer rule - right. lookupAutofillPatch takes the declared pairs as a fourth argument, applies them whatever the column's autofill says, and removes a declared target from the same-named copy, so one target has one writer per pick; the spec's sentence "the column key autofill still governs only the same-named copy; a declared pair takes its target out of that copy" is implemented as written. The pairs reach the grid through a new host prop because an InlineGridColumnSchema column carries no field definition: MasterDetailForm keeps them per resolved entry and LineItemsPanel derives them from the child schema it already loads, both through lookupAutofillByField, which reads single-value lookup / master_detail fields only. The earlier-pick memory is a per-grid map keyed by column and the row's own lookup value, not its index. Pinned on the grid: a cell pick fills and the next pick replaces, a stored line's own price is kept, the browse-all pick reaches the grid once.
  • The browse-all dialog in a grid now reaches the host once, with its record - right, and a named behaviour change on an existing path: handleDialogSelect holds back the bare-id onSelect when the host owns the pick, and handleDialogSelectRecords hands the host the record through onSelectRecord exactly as a popover pick does. Before, that path wrote the id alone; now the same-named copy runs there too, as it always did from the dropdown. Clearing and multi-value picks commit through onChange as before.
  • The two field-meta censuses name the new read - right, and this is what 48f2c159 adds after the first head's Test UI shards 12 and 13 went red on them: form-field-channel-census-12130 gets a lookupAutofill row (RUNTIME-ONLY, read off the resolved field stash by the lookup widget alone), and plugin-grid's RELATIONAL_META_READ_SET gets a lookupAutofill row (spec, LOOKUP_EDITOR_ONLY, off the copy set because the grid takes the pairs through its own prop). Both are ledger rows the censuses demand of any new field-meta read; no product line moved between the two heads.
  • Liveness - right. The lookupAutofill row and its three children go planned to live with the read points cited by path and symbol and the two test files as evidence; state-counts/field.md is the generated shard (live 100 to 103, planned 9 to 6, classified 110). No spec source changes.
  • Accept-set: nothing an author writes moves - right. Metadata-protocol: none is right.
  • Check-runs as gate verdicts - read as above; every run on this head completed and none is red, cancelled or in progress at the read.

② Semver level

③ Boundary flags

  • skills/docs: teach lookupAutofill (lookup-pick fills declared fields) once the ui half fills the form #28246 (the skills and docs text) lands after this PR, never with it - it edits skills/**, a governed Tier H surface; riding here would make this PR Tier H. Its card carries Blocked-by: #28245. Its text should also state the bound below.
  • A fully authored master-detail collection fills only the same-named copy - MasterDetailForm skips the child-schema fetch for an entry whose relationship and every column are authored, so no pairs reach that grid. Loading the child schema for such an entry is a fetch-policy change outside this card; named in the body, worth a card if a product hits it.
  • The fill does not judge field-level security itself - a target the caller may not edit is dropped by sanitizeFormData at save (stripUnwritable), so a fill into it never reaches the server, but the person sees it filled until then. Acceptable for this card; a later card could hide the write at the widget.
  • A typed value equal to the earlier pick's source value reads as pick-filled - the describe's rule is by value, so this is the spec's reading, not a defect; named so a report of "my typed price was replaced" can be answered.
  • No name approval owed - the key was ruled with A form cannot fill a field from a picked lookup record (declared lookup-pick autofill) #27945 / feat(spec): a lookup field declares which fields a pick fills (lookupAutofill) #28244; onAutofill and lookupAutofill on the ui surfaces follow the key's name.

④ Design judgment

  • Nothing blocking, and no should-fix on the diff. Tried: a pair whose source the reader may not read (absent from the served row, nothing written, pinned); a null served source on an empty target (written as null, a no-op; under overwrite: true it replaces, which is TRUE's contract); a multi-value lookup (no pairs read, by the spec's refusal and by lookupAutofillByField); a remount of the form or grid (the earlier-pick memory is forgotten, which errs toward keeping a value, never toward overwriting); rows added, removed or reordered in the grid (memory keyed by the row's own lookup value). One rule in one module, two readers that each own their memory, and the writer owned by the surface that owns the values: the right shape, and the same shape onSelectRecord already has.

Verdict reasoning. The reader implements the describe as written, in one function both surfaces call, with the overwrite rule, the one-writer rule for the grid and the "fills nothing without the key" control each pinned and an ablation shown ten red / seven green. The two surface files outside the order are the smallest edits that let a widget write a sibling field, agreed with the seat before editing. The widening is additive and patch is right. What remains is #28246's text and the fully authored master-detail bound, both named.

Implemented-by: claude/project-thread-t2cn2e
Reviewed-by: session_01L9qibw58ehQ2gRHP2h2Puz

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-tesla · session_01L9qibxgfM8p8t3fo6y9eFK

维护者速读

席位核对

ACCEPT · head 48f2c159c31f23d2ec75592acc6896e07222ef08 · 2026-10-31T06:53Z

  1. 形态: 17 files, +685/−26; Fixes #28245; 0 governed paths, under 3000 lines (check-governed-merges --pr 28322).
  2. 前提实测: no renderer read FieldSchema.lookupAutofill on main (liveness planned); surface extension to components/form.tsx + types/field-types.ts declared in the claim amendments on Lookup pick fills the declared fields in the record form and the line-item grid (lookupAutofill, ui half of #27945) #28245 and the body.
  3. 门禁 check-runs: 67 on the head, 57 success + 10 skipped, 0 failing; mergeable_state clean. Seat read the fill helper and the form.tsx writer: one rule both surfaces call, writer gated to lookup / master_detail.
  4. 契约复审: Clause-②: yes (widening) on the card's claim amendment; PASS 6539660422 at CONTRACT_REVIEW_TIER on this head, nothing blocking.
  5. 范围外去向: master-detail bound (fully authored entry loads no child schema) goes into skills/docs: teach lookupAutofill (lookup-pick fills declared fields) once the ui half fills the form #28246's text (Acceptance notes). Skills-docs: follow-up #28246.
  6. 落地条款: seat runs pr_ready; auto-merge waits for the maintainer's word in the seat thread.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-tesla · session_01L9qibxgfM8p8t3fo6y9eFK

席位核对

ACCEPT (delta) · head 8f9ef9ee951909908585b2e9f895c8287df0c69d · 2026-10-31T14:59Z · supersedes the head of ACCEPT 6540305963 (48f2c159)

  1. Delta: merge of origin/main (04791a75b) to clear the conflict in the generated packages/spec/liveness/state-counts/field.md, then that file regenerated with gen:liveness-counts (field 104 live / 5 planned = main's 101/8 plus this PR's three flips). No hand-written hunk changed: the PR's own diff against main is still 17 files, +685/−26.
  2. 门禁 check-runs: 64 on the head, 56 success + 8 skipped, 0 failing; mergeable_state clean. Dev reran the touched-package suites on the merged tree (977 files / 17848 tests passing).
  3. 契约复审: PASS 6539660422 stands — the delta touches no published surface (merge + generated count only).
  4. Governed: 0 governed paths, 711 lines ≤ 3000 (check-governed-merges --pr 28322).
  5. 落地: the maintainer's go 2026-10-31T14:22Z 「使用 pm-dispatch 技能审核和落地pr」 → pr_ready + auto-merge now. skills/docs: teach lookupAutofill (lookup-pick fills declared fields) once the ui half fills the form #28246 (skills text) merges after this one.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 31, 2026 15:01
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 31, 2026 15:01
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 31, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract-review-claim: session_01L9qibw58ehQ2gRHP2h2Puz · head 8f9ef9ee951909908585b2e9f895c8287df0c69d · 2026-10-31T15:15Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 8f9ef9ee951909908585b2e9f895c8287df0c69d
Local-runs: none
Scope: the delta from the reviewed head 48f2c159c31f23d2ec75592acc6896e07222ef08 (PASS 6539660422) to this head: the merge commit 04791a75 of origin/main at 4723519 into claude/project-thread-t2cn2e and the regeneration commit 8f9ef9ee (packages/spec/liveness/state-counts/field.md, the field row 103 live / 6 planned → 104 / 5 over main's 101 / 8); the hand-written surface of PASS 6539660422 otherwise unchanged (lookupAutofill.ts, LookupField.tsx, GridField.tsx, types.ts, toHostProps.ts, fields index, form.tsx, MasterDetailForm.tsx, LineItemsPanel.tsx, field-types.ts, liveness/field.json, the two test files, the census ledger row, relationalMetaKeys.ts, the changeset)

Inputs: PR #28322 (the seat's 席位核对 ACCEPT (delta) 6545070006 on this head: merge to clear the conflict in the generated state-counts file, then gen:liveness-counts, no hand-written hunk changed, 711 lines, 0 governed paths; the maintainer's go of 2026-10-31T14:22Z 「使用 pm-dispatch 技能审核和落地pr」 quoted there), the 65 check-runs on this head (read 2026-10-31T15:18Z: all 65 runs completed, none failed, none cancelled, none in progress; success: Auto Label, Build & E2E UI, Build Core, Build UI Deps, Check Changeset, Check PR Size, Dogfood Regression Gate, Flag docs affected by code changes, Governed Surface Queue Guard, Lint & Repo Gates, Lint UI, Lint · CI, release & engine gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Lint · scoped · tooling self-tests, Live E2E UI, PR-body guards (claim · duplicate · Part-of · single-writer path), Plan Test Core shards, Spec property liveness, Test Core, Test Core (all 8), Test UI, Test UI (all 16), Type Check UI, Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates, Type Check · workspace, TypeScript Type Check, filter; skipped: 18.x upgrade harness (opt-in), Build Docs, Dogfood Regression Gate (matrix shard/3), Dogfood Verify CLI, Packed-tarball smoke (opt-in), Schedule gate · skip when a green push run covers the commit, Temporal Conformance (live PG + MySQL), UI Published Artifacts; the same check names as the reviewed head), and the delta read whole: the interdiff of the PR-only diffs (merge-base to head, before 73b72709..48f2c159 and after 47235194..8f9ef9ee; index and hunk headers stripped) is the two regenerated count rows and nothing else; git show --cc of the merge commit has no evaluated hunk; the three files both sides changed since the old base (liveness/field.json, components/src/renderers/form/form.tsx, fields/src/index.tsx) carry main's hunks from #28396 (form refusal said once), #28363 (immutable locks) and #28315 (percent plain face), none of which names lookupAutofill or onAutofill, and the merge took both sides without a conflict. The head is mergeable and clean against today's main.

① Derived judgments

② Semver level

  • Unchanged - @object-ui/fields, @object-ui/components, @object-ui/types, @object-ui/plugin-form patch, @objectstack/spec patch; Clause-②: yes (widening), Metadata-protocol: none, as PASS 6539660422 accepted.

③ Boundary flags

④ Design judgment

  • A merge whose combined diff has no evaluated hunk, with an interdiff of regenerated counts only, is the no-delta case; the one read worth doing was main's hunks in the two shared files, done above. Nothing to add.

Verdict reasoning. The only change since the reviewed head is a conflict-free merge of main plus the regenerated liveness counts, main's hunks in the files both sides touched are outside the fill, the declarations and the levels are unchanged, and every latest check on this head is green or skipped.

Implemented-by: claude/project-thread-t2cn2e
Reviewed-by: session_01L9qibw58ehQ2gRHP2h2Puz

VERDICT: PASS

Merged via the queue into main with commit ea1751b Oct 31, 2026
65 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-t2cn2e branch October 31, 2026 15:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Lookup pick fills the declared fields in the record form and the line-item grid (lookupAutofill, ui half of #27945)

1 participant