Repository navigation
feat(spec): a lookup field declares which fields a pick fills (lookupAutofill) - #28244
Conversation
…onship field Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibyU9JHYh6atGVnEXr3
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibyU9JHYh6atGVnEXr3
…erated artefacts Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibyU9JHYh6atGVnEXr3
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibyU9JHYh6atGVnEXr3
📓 Docs Drift CheckThis PR changes 3 package(s): 14 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 170 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin e2b03d0f9242bfb211d238bbe30ab3b58da977c3 && git checkout e2b03d0f9242bfb211d238bbe30ab3b58da977c3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4159c7012edaf69581c8b32a38a57d3e1b6a9981 bda903307dcc7e74b34c33557df74ca3a1cf1891 && git checkout -B drift-repro 4159c7012edaf69581c8b32a38a57d3e1b6a9981 && git merge --no-ff bda903307dcc7e74b34c33557df74ca3a1cf1891
node scripts/docs-audit/affected-docs.mjs --json 4159c7012edaf69581c8b32a38a57d3e1b6a9981
|
|
domain:spec#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract-review-claim: |
…ery-key tables; describe states the overwrite default Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibyU9JHYh6atGVnEXr3
…scribe Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibyU9JHYh6atGVnEXr3
|
domain:spec#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract-review-claim: |
|
domain:spec#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract reviewServed-tier: Inputs: card #27945 (body, claim 6528676972, os-dev-report 6530857198), PR body, the diff against merge base 7c3cc88 (first head 4c70e71) and the delta to 6d9c4c7 against merge base 39da1bd, the head's check-runs (read 2026-10-30T20:05Z: check-runs on 6d9c4c7: 50 total, 41 success (Auto Label, Build & E2E UI, Build Core, Build Docs, Build UI Deps, Check Changeset, Check Documentation Links, Check PR Size, Dogfood Regression Gate, Flag docs affected by code changes, Governed Surface Queue Guard, Lint & Repo Gates, 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, Type Check UI, Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates, Type Check · workspace, TypeScript Type Check, filter); 9 skipped path-filtered (18.x upgrade harness (opt-in), Dogfood Regression Gate (matrix shard/3), Dogfood Verify CLI, Lint UI, Packed-tarball smoke (opt-in), Schedule gate · skip when a green push run covers the commit, Temporal Conformance (live PG + MySQL), Test UI (matrix shard/16), UI Published Artifacts); 0 failure, 0 in progress). Author Delta since the 4c70e71 review (claim 6531491970; re-claimed on this head): three commits. 4182095 merges origin/main 39da1bd: the PR's own hunks against the new merge base are identical to the reviewed ones (same file names, no content difference beyond hunk offsets) and main changed no file the PR touches. 117ff3b is the three must-fixes named below, each in the shape prescribed: the ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentThe card asked for a declarative pick-autofill that the spec refuses dishonestly rather than lets parse-and-fill-nothing, and the PR does exactly that with the door split the spec already uses (field, object, stack) and a value-shape rule rather than a label rule. The aliases are refused by name rather than accepted, which keeps one spelling. The exemptions (unresolved object, unknown value type, audit columns) are the conservative ones and each is stated in the docblock. Implemented-by: VERDICT: PASS |
|
domain:spec#1 · os-tesla · session_01L9qibwb6hBqszPTYJobitm 维护者速读
席位核对 · ACCEPT ·
|
Resolve the #28222 overlap: keep percentDisplay and lookupAutofill side by side in the metadata-form omit ledger, and take the /meta/types field count to 81 (80 percentDisplay, 81 lookupAutofill). Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibyU9JHYh6atGVnEXr3
…ounts The merge with main took one side of five generated files; regenerate them from the merged tree so both percentDisplay and lookupAutofill are listed. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibyU9JHYh6atGVnEXr3
|
domain:spec#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract-review-claim: |
|
domain:spec#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract reviewServed-tier: Inputs: card #27945 (body, claim 6528676972, os-dev-report 6530857198), PR body, the diff against merge base 7c3cc88 (first head 4c70e71), the delta to 6d9c4c7 against merge base 39da1bd and the delta to bda9033 against merge base 03069ca, the head's check-runs (read 2026-10-30T21:43Z: check-runs on bda9033: 55 total, 42 success (Auto Label, Build & E2E UI, Build Core, Build Docs, Build UI Deps, Check Changeset, Check Documentation Links, Check PR Size, Dogfood Regression Gate, Flag docs affected by code changes, Governed Surface Queue Guard, Lint & Repo Gates, 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, Type Check UI, Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates, Type Check · workspace, TypeScript Type Check, filter); 13 skipped path-filtered (18.x upgrade harness (opt-in), Auto Label, Check PR Size, Dogfood Regression Gate (matrix shard/3), Dogfood Verify CLI, Lint UI, Packed-tarball smoke (opt-in), Schedule gate · skip when a green push run covers the commit, Temporal Conformance (live PG + MySQL), Test UI (matrix shard/16), UI Published Artifacts); 0 failure, 0 in progress). Author Delta since the 4c70e71 review (claim 6531491970; re-claimed on this head): three commits. 4182095 merges origin/main 39da1bd: the PR's own hunks against the new merge base are identical to the reviewed ones (same file names, no content difference beyond hunk offsets) and main changed no file the PR touches. 117ff3b is the three must-fixes named below, each in the shape prescribed: the Third head (6d9c4c7 to bda9033; re-claimed on this head): the merge queue dropped 6d9c4c7 on MERGE_CONFLICT after #28222 merged, so 5290464 merges origin/main 03069ca (a merge commit, no rebase) and bda9033 regenerates the references, the authorable surface and the liveness counts. Eleven files are shared with main; read file by file, the PR's own plus and minus lines at this head are identical to the 6d9c4c7 review in every file except the two the collision predicted: the ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentThe card asked for a declarative pick-autofill that the spec refuses dishonestly rather than lets parse-and-fill-nothing, and the PR does exactly that with the door split the spec already uses (field, object, stack) and a value-shape rule rather than a label rule. The aliases are refused by name rather than accepted, which keeps one spelling. The exemptions (unresolved object, unknown value type, audit columns) are the conservative ones and each is stated in the docblock. Implemented-by: VERDICT: PASS |
|
domain:spec#1 · os-tesla · session_01L9qibwb6hBqszPTYJobitm 席位核对 · ACCEPT (head moved) ·
|
domain:spec#1 · os-tesla · session_01L9qibwb6hBqszPTYJobitm
Requested by Tesla · project thread
Fixes #27945
Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: follow-up #28246
维护者速读
lookupAutofill声明,写明「选中一条记录后,用它的哪个字段填本记录的哪个字段」(例:报价行选产品,unit_price ← list_price);规范侧在解析时和os validate/os build时校验,界面侧填值是后续 ui 卡。autofill列键的默认与语义不变(有对照测试)。回滚即 revert 本 PR。packages/spec/src/data/field.zod.ts的lookupAutofill(describe 里的覆盖规则)、packages/spec/src/stack.zod.ts的lookupAutofillTypeMismatch(类型兼容表)、packages/spec/src/data/object.zod.ts的refuseDishonestLookupAutofill。pnpm --filter @objectstack/spec exec vitest run src/data/field-lookup-autofill.test.ts,预期 18/18 通过。What changes
The declaration:
FieldSchema.lookupAutofill. A single-valuelookup/master_detailfield may declare[{ target, source, overwrite? }]. A pick in the lookup copies the picked record'ssourcefield (a field of thereferenceobject) into this record'stargetfield. HotCRM's quote line declares it on its product lookup as[{ target: 'unit_price', source: 'list_price' }]. A pair is a closed shape (strictObject, with alias prescriptionsfrom→source,field→target,replace→overwrite, and a few more), and the list is.min(1).The overwrite rule, stated in the describe. A pick never replaces a value somebody wrote. The target is filled only while it is empty or still holds the value an earlier pick filled in, so a value the user typed, or one the record already stored, stays. A pair that declares
overwrite: truesays otherwise out loud and replaces the target on every pick. This is the HotCRM server hook's own rule moved to the form: its negotiatedunit_priceis defaulted only when blank and never overwritten on update.Placement: the field, not a form view. The record form draws a field from its definition whichever form shows it: a declared form view, a
formViewsentry, or the derived form an object without one gets. The inline line-item grid hydrates its columns from the same child field definitions (hydrateColumns). So the field is the one home both readers reach. A form-view key would be invisible to the grid and to every other form of the object.Relation to the grid column key
autofill, which is unchanged.InlineGridColumnSchema.autofillkeeps its contract: it governs only the grid's implicit same-named copy, on by default (GridField.tsx#lookupAutofillPatchoverwrites with it today). A declared pair applies in a grid column over the field too, under its own overwrite rule, and takes its target out of the same-named copy, so one target never has two writers on one pick.autofill: falsestill switches off only the same-named copy. The describe says this, and a control test pins the column key's parse and its describe byte-for-byte.Three doors judge it, each with a located refusal:
FieldSchemasuperRefinelookup/master_detail; onmultiple: truecustomatlookupAutofillObjectSchemasuperRefine (refuseDishonestLookupAutofill)readonly, computed (formula,summary,autonumber, or anexpression), or named twicecustomatfields.FIELD.lookupAutofill.N.targetdefineStackcross-reference pass (collectLookupAutofillErrors)internalsource, a source whose value the target cannot holdSTACK_CROSS_REFERENCE_INVALID(422), one finding per pair, prefixedObject 'O' field 'F' lookupAutofill[N].source:The first two run at every parse, the save door included. The third runs at
os validate/os build, because only the stack holds the referenced object. It followscollectHydratedInlineColumnErrors: a referenced object the stack does not declare is not judged, and neither is an undeclared source that is one of the injected audit columns (created_at, …).Type rule (
lookupAutofillTypeMismatch): the same type with the same multiplicity; a string-valued source (text,textarea,email,url,phone,markdown,richtext,html,autonumber) into a plain text target;numberandcurrencyinto each other; a relationship only into a relationship over the same object. Aformulasource is judged by itsreturnType. Asummary, or a formula with noreturnType, is not judged.Generated artefacts:
content/docs/references/data/{field,object}.mdx,content/docs/references/system/migration.mdx,packages/spec/authorable-surface/data.json,docs/audits/2026-07-unknown-key-strictness-ledger.counts/data.md(one new strict site),packages/spec/liveness/state-counts/field.md.Liveness:
liveness/field.jsongains aplannedrow (withtarget/source/overwritechildren), naming the producer (HotCRM's quote line) and the consumer (the ui card:LookupFieldandGridField). NoauthorWarn, on thereferenceDeletedWhenprecedent. A warned field row would reopen the lint's field walk, which is outside this card's surface.Form ledger:
metadata-form-zod-reconciliation.test.tsgains anomitrow forfield.lookupAutofill(planned, not yet read), besideconditionalFormatting.Changeset:
.changeset/27945-lookup-pick-autofill.md,@objectstack/specminor, with both declaration lines.Not in this PR (the ui half). The record form's lookup widget and the line-item grid writing the declared targets on pick are a separate
domain:uicard. This PR does not touchui/.Tests
packages/spec/src/data/field-lookup-autofill.test.ts, 18 cases, each refusal pinned by its code and its path (never by the prose around it):lookupandmaster_detail(control); refuses the key on atextfield and on amultiple: truelookup (customatlookupAutofill); refuses an empty list (too_small), a non-snake_case name (invalid_formatatlookupAutofill.0.target) and an unknown key, whose message namessource(unrecognized_keysatlookupAutofill.0).readonlytarget, aformula, anautonumber, a stored field with anexpression, and a target named twice (refused at the later pair, index 1) are each onecustomissue atfields.crm_product.lookupAutofill.N.target. The registeredobjecttype schema (the save door) refuses the same way.internalsource, and five type mismatches (text→currency,number→boolean, a lookup over another object, a list → a single value, acurrencyformula →boolean) each raise oneSTACK_CROSS_REFERENCE_INVALID(422) finding atlookupAutofill[N].source.autofillunchanged: a lookup column still parses withautofill: trueandfalse, an identity-only column still parses, the key's describe is pinned byte-for-byte, and a grid column refuseslookupAutofill(the pairs live on the field only).Also re-run on the merged head:
packages/specvitest oversrc/data,src/stack*,src/compose-stacks*,src/define-stack*,src/inline-grid-column-carriers.test.tsandsrc/system/metadata-form-zod-reconciliation.test.ts. The result was Test Files 200/200 and Tests 6051 passed, 1 todo.pnpm --filter @objectstack/spec typecheckpassed (exit 0), and so didpnpm --filter @objectstack/spec build(no new dropped-refinement site: the checks sit in the existingFieldSchema/ObjectSchemasuperRefines).Acceptance notes
packages/specthere are two doors. The parse runs at every door, the metadata save included, and it judges the field half and the target half. The stack cross-reference pass runs atos validate/os buildonly ("No wire door raises it", inStackCrossReferenceError's own docblock), and it judges the source half. A runtime metadata save therefore judges the target and not the source. The runtime publish gate's object-aware field-ref checks live in@objectstack/lint(validate-object-field-refs.ts), which is outside this card's surface. Extending that rule tolookupAutofill[].sourcewould close the gap for the save door. I am noting that here and not filing it: an unjudged source fills nothing, and a refusal atos buildalready covers every package-authored stack.objectExtensions. AlookupAutofilldeclared on a field that an object extension adds is judged by the field half and by the source half (the stack pass merges extension fields by name). Its target half is not judged, becauseObjectExtensionSchemahas no view of the base object's field map. This is the same reach asreferenceDeletedWhen.readonlytarget is refused, by design. HotCRM's linelist_priceisreadonlyand is stamped by its server hook. So the HotCRM declaration isunit_price ← list_priceonly, andlist_pricekeeps arriving from the hook on save. Filling a readonly field on the form would show a value that the save then strips.requiredPermissions, or a permission set marking it non-readable) arrives masked or absent at run time. The ui half should read an absent value as "nothing to fill".internalis judged because it is absent for every reader.autofillbehaviour. The ui card applies the declared pairs under this key's overwrite rule and takes each declared target out of the same-named copy, as the describe states. The contract review can judge whether that relation is the right one.LookupFieldandGridFieldwriting the declared targets on pick) is the follow-updomain:uicard, which the seat files. TheSkills-docs:line above names the two teaching files owed with it.Verification
Every reading below was taken on head
4c70e71e, which isorigin/main7c3cc889merged into the branch. The diff againstmainis the same 13 paths as before the merge.Derived gates: 107/107, all exit 0, reconciled with
dispatch-gates --ran(0 NOT-MEASURED)node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 107 families from 13 changed paths at4c70e71e. The list is identical to the one derived before the merge, at6ea314a6.</dev/null, and each exit code was recorded before any pipe. 106 exited 0 in the sweep.check:skill-examplesanswered exit 3 (PREREQUISITE NOT MET) because the merge's spec rebuild left theclient-reactclosure stale. Afterturbo run build --filter='@objectstack/client-react...'it exited 0.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran FILEprinted:✓ dispatch-gates --ran: 107 derived famil(ies) accounted for — 107 run, 0 NOT-MEASURED.6ea314a6, the same 107 also all reached exit 0. Seven exit-3 prerequisites needed workspace dists, andcheck-plugin-teardown-shape --self-testneeded its pinned commit fetched.check:type-check-debtneeded a longer window than the first sweep's per-gate timeout.Spec artefacts, build and types
pnpm --filter @objectstack/spec buildexited 0. No new dropped-refinement site, and the baseline is unchanged.pnpm --filter @objectstack/spec check:generatedreported all 14 artefacts current, after--fixregenerated the three it proved stale (check:docs,check:strictness-ledger,check:liveness), plusgen:docsforobject.mdxafter the merge.pnpm --filter @objectstack/spec typecheckexited 0 (src, scripts and the test layer).Local-only families (not in the derivation; machine state, reported only)
pnpm check:skill-frame-freshnessexited 0.pnpm check:dev-prereqsexited 1, on machine state only: 29ui/workspace packages have nodist/in this container, because theui/tree was not built. This diff touches noui/path.Dogfood: not run. The PR title carries no
!andClause-②carries the(widening)arm.Generated by Claude Code