Skip to content

feat(spec): a lookup field declares which fields a pick fills (lookupAutofill) - #28244

Merged
objectstack-fleet[bot] merged 10 commits into
mainfrom
claude/project-thread-jim54h
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 10 commits into
mainfrom
claude/project-thread-jim54h

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

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 卡。
  • 为什么改:HotCRM 报价行的销售价只能在保存后由服务端钩子补上,表单里选完产品看不到价格;平台没有任何元数据能声明「选查找就带出字段」。
  • 风险与回滚:纯新增可选键,现有元数据零影响;明细网格 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-value lookup / master_detail field may declare [{ target, source, overwrite? }]. A pick in the lookup copies the picked record's source field (a field of the reference object) into this record's target field. 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 prescriptions from → 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: true says otherwise out loud and replaces the target on every pick. This is the HotCRM server hook's own rule moved to the form: its negotiated unit_price is 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 formViews entry, 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.autofill keeps its contract: it governs only the grid's implicit same-named copy, on by default (GridField.tsx#lookupAutofillPatch overwrites 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: false still 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:

Door Refuses Code + path
FieldSchema superRefine the key off lookup / master_detail; on multiple: true custom at lookupAutofill
ObjectSchema superRefine (refuseDishonestLookupAutofill) a target that is the lookup itself, not a declared field, readonly, computed (formula, summary, autonumber, or an expression), or named twice custom at fields.FIELD.lookupAutofill.N.target
defineStack cross-reference pass (collectLookupAutofillErrors) a source the referenced object does not declare, an internal source, a source whose value the target cannot hold STACK_CROSS_REFERENCE_INVALID (422), one finding per pair, prefixed Object '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 follows collectHydratedInlineColumnErrors: 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; number and currency into each other; a relationship only into a relationship over the same object. A formula source is judged by its returnType. A summary, or a formula with no returnType, 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.json gains a planned row (with target / source / overwrite children), naming the producer (HotCRM's quote line) and the consumer (the ui card: LookupField and GridField). No authorWarn, on the referenceDeletedWhen precedent. 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.ts gains an omit row for field.lookupAutofill (planned, not yet read), beside conditionalFormatting.

Changeset: .changeset/27945-lookup-pick-autofill.md, @objectstack/spec minor, 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:ui card. This PR does not touch ui/.

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):

  • Field half: accepts on lookup and master_detail (control); refuses the key on a text field and on a multiple: true lookup (custom at lookupAutofill); refuses an empty list (too_small), a non-snake_case name (invalid_format at lookupAutofill.0.target) and an unknown key, whose message names source (unrecognized_keys at lookupAutofill.0).
  • Target half: the HotCRM shape and three target kinds parse clean (control); the lookup itself, an undeclared target, a readonly target, a formula, an autonumber, a stored field with an expression, and a target named twice (refused at the later pair, index 1) are each one custom issue at fields.crm_product.lookupAutofill.N.target. The registered object type schema (the save door) refuses the same way.
  • Source half: the HotCRM shape and seven compatible pairings build clean (control), as do an unresolved referenced object and an injected audit column; an undeclared source, an internal source, and five type mismatches (text → currency, number → boolean, a lookup over another object, a list → a single value, a currency formula → boolean) each raise one STACK_CROSS_REFERENCE_INVALID (422) finding at lookupAutofill[N].source.
  • Control, grid autofill unchanged: a lookup column still parses with autofill: true and false, an identity-only column still parses, the key's describe is pinned byte-for-byte, and a grid column refuses lookupAutofill (the pairs live on the field only).

Also re-run on the merged head: packages/spec vitest over src/data, src/stack*, src/compose-stacks*, src/define-stack*, src/inline-grid-column-carriers.test.ts and src/system/metadata-form-zod-reconciliation.test.ts. The result was Test Files 200/200 and Tests 6051 passed, 1 todo. pnpm --filter @objectstack/spec typecheck passed (exit 0), and so did pnpm --filter @objectstack/spec build (no new dropped-refinement site: the checks sit in the existing FieldSchema / ObjectSchema superRefines).

Acceptance notes

  • The publish-time door, measured. The order asked for the declared map to be validated "at publish". Inside packages/spec there 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 at os validate / os build only ("No wire door raises it", in StackCrossReferenceError'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 to lookupAutofill[].source would close the gap for the save door. I am noting that here and not filing it: an unjudged source fills nothing, and a refusal at os build already covers every package-authored stack.
  • objectExtensions. A lookupAutofill declared 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, because ObjectExtensionSchema has no view of the base object's field map. This is the same reach as referenceDeletedWhen.
  • A readonly target is refused, by design. HotCRM's line list_price is readonly and is stamped by its server hook. So the HotCRM declaration is unit_price ← list_price only, and list_price keeps arriving from the hook on save. Filling a readonly field on the form would show a value that the save then strips.
  • Field-level security is not judged at authoring. A source the reader cannot read (field 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". internal is judged because it is absent for every reader.
  • The grid's same-named copy overwrites; a declared pair does not. This PR does not change the grid's autofill behaviour. 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.
  • Out of scope, by the order: the ui half (the record form's LookupField and GridField writing the declared targets on pick) is the follow-up domain:ui card, which the seat files. The Skills-docs: line above names the two teaching files owed with it.

Verification

Every reading below was taken on head 4c70e71e, which is origin/main 7c3cc889 merged into the branch. The diff against main is 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/objectstack derived 107 families from 13 changed paths at 4c70e71e. The list is identical to the one derived before the merge, at 6ea314a6.
  • Each family ran with </dev/null, and each exit code was recorded before any pipe. 106 exited 0 in the sweep. check:skill-examples answered exit 3 (PREREQUISITE NOT MET) because the merge's spec rebuild left the client-react closure stale. After turbo run build --filter='@objectstack/client-react...' it exited 0.
  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran FILE printed: ✓ dispatch-gates --ran: 107 derived famil(ies) accounted for — 107 run, 0 NOT-MEASURED.
  • On the pre-merge head 6ea314a6, the same 107 also all reached exit 0. Seven exit-3 prerequisites needed workspace dists, and check-plugin-teardown-shape --self-test needed its pinned commit fetched. check:type-check-debt needed a longer window than the first sweep's per-gate timeout.
Spec artefacts, build and types
  • pnpm --filter @objectstack/spec build exited 0. No new dropped-refinement site, and the baseline is unchanged.
  • pnpm --filter @objectstack/spec check:generated reported all 14 artefacts current, after --fix regenerated the three it proved stale (check:docs, check:strictness-ledger, check:liveness), plus gen:docs for object.mdx after the merge.
  • pnpm --filter @objectstack/spec typecheck exited 0 (src, scripts and the test layer).
Local-only families (not in the derivation; machine state, reported only)
  • pnpm check:skill-frame-freshness exited 0.
  • pnpm check:dev-prereqs exited 1, on machine state only: 29 ui/ workspace packages have no dist/ in this container, because the ui/ tree was not built. This diff touches no ui/ path.

Dogfood: not run. The PR title carries no ! and Clause-② carries the (widening) arm.


Generated by Claude Code

@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/driver-sql, @objectstack/metadata-core, @objectstack/spec, touching 20 documentable anchor(s). ⚠️ 3 changed file(s) yielded no anchor (packages/spec/authorable-surface/data.json, 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.

14 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-driven.mdx (via FieldSchema (symbol, a top-level const), ObjectSchemaBase (symbol, a top-level const object))
  • content/docs/data-modeling/currency.mdx (via list_price (literal, a string literal in FieldSchema))
  • content/docs/data-modeling/external-datasources.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/data-modeling/field-types.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/data-modeling/validation-rules.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/deployment/troubleshooting.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/deployment/validating-metadata.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/getting-started/common-patterns.mdx (via unit_price (literal, a string literal in FieldSchema))
  • content/docs/getting-started/quick-reference.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/kernel/contracts/data-engine.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/protocol/backward-compatibility.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/protocol/objectql/types.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/protocol/objectui/concept.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/protocol/objectui/layout-dsl.mdx (via unit_price (literal, a string literal in FieldSchema))

⛔ 2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-0.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/releases/v17/17-1.mdx (via FieldSchema (symbol, a top-level const))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 3 changed file(s) yielded no anchor (packages/spec/authorable-surface/data.json, packages/spec/liveness/field.json, packages/spec/liveness/state-counts/field.md) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: master_detail (literal, 39 pages)
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 59 of 211 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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 — 170 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 4159c7012edaf69581c8b32a38a57d3e1b6a9981 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from e2b03d0f9242bfb211d238bbe30ab3b58da977c3 — the merge of head bda903307dcc7e74b34c33557df74ca3a1cf1891 into base 4159c7012edaf69581c8b32a38a57d3e1b6a9981, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 4159c7012edaf69581c8b32a38a57d3e1b6a9981 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract-review-claim: session_01L9qibw58ehQ2gRHP2h2Puz · head 4c70e71e5277beaeb66da3e67ee64e965bb5dfdc · 2026-10-30T19:15Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract-review-claim: session_01L9qibw58ehQ2gRHP2h2Puz · head 6d9c4c774af9ee5c323971342be7d855152e5bfd · 2026-10-30T19:44Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 6d9c4c774af9ee5c323971342be7d855152e5bfd
Local-runs: none
Scope: packages/spec/src/data/field.zod.ts (lookupAutofill member + FieldSchema superRefine), packages/spec/src/data/object.zod.ts (refuseDishonestLookupAutofill), packages/spec/src/stack.zod.ts (collectLookupAutofillErrors, lookupAutofillTypeMismatch), packages/spec/src/data/field-lookup-autofill.test.ts, packages/spec/src/system/metadata-form-zod-reconciliation.test.ts (omit row), packages/spec/liveness/field.json + state-counts, packages/spec/authorable-surface/data.json, content/docs/references/data/field.mdx + object.mdx + system/migration.mdx, .changeset/27945-lookup-pick-autofill.md

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 user.login is objectstack-fleet[bot]; the drafter is session_01L9qibyU9JHYh6atGVnEXr3 on claude/project-thread-jim54h.

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 /meta/types field count 79 to 80 with its [#27945] ledger comment; FIELD_KEY_STORAGE_CLASS.lookupAutofill: 'presentation' in driver-sql (the form and grid read it on pick, createColumn never); and in metadata-core a LOOKUP_AUTOFILL_PAIR_POSITIONS table (target: pointer, source: keep, overwrite: keep) behind a lookupAutofillPair scrub that drops a pair whole when its target names a denied field and keeps source because it names the referenced object's field, mounted as lookupAutofill: arrayOf(lookupAutofillPair) so an emptied list is deleted, with a behaviour test (denied target dropped, all-denied list removed) and an every-pair-key completeness pin. The same commit makes the describe state the overwrite default in words (overwrite absent or false: never replaces a typed or stored value), which 6d9c4c7 regenerates into the three reference pages, resolving the should-fix. The changeset gains @objectstack/driver-sql: minor and @objectstack/metadata-core: minor lines with a sentence each (the #28222 grade), and @objectstack/metadata-protocol is test-only so carries no line. ①②③④ stand.

① Derived judgments

  • The member: FieldSchema.lookupAutofill is an optional non-empty array of strict { target, source, overwrite? } pairs, with the legacy spellings (field / to / targetField, from / sourceField / lookupField, replace / force) refused by name at the pair door with the canonical key in the message. The describe states the overwrite rule (default false: fill only an empty target). Matches the card's answer shape; no silent alias acceptance.
  • Three doors, each judging only what it can see, as the card's ADR-0049 stance asks: FieldSchema refuses the key off lookup / master_detail and on multiple: true (no single picked record); ObjectSchema's superRefine refuses a target that is the lookup itself, undeclared, readonly, computed (formula / summary / autonumber or any stored field with expression), or named twice, with one located issue per pair at ['fields', field, 'lookupAutofill', i, 'target']; the stack's cross-reference pass refuses a source the referenced object does not declare (audit-provenance columns exempt via AUDIT_FIELD_DEFS), an internal source, and a value-shape mismatch. An unresolved referenced object or an unknown source value type (summary, formula without returnType) is not judged, the stance of collectHydratedInlineColumnErrors.
  • The type rule is the stored value's shape: same type and multiplicity; string-valued sources into plain text targets; number and currency into each other; a record id only into a relationship over the same reference. multiselect / checkboxes / tags count as lists without multiple. Consistent with the card's "fill nothing silently" rule.
  • Liveness: planned row with children target / source / overwrite; the authorable-surface and unknown-key ledger counts move with it; the metadata-form omit row names the same ui card as the reader. The 18-case test covers the alias refusals, the three doors and the type rule.

② Semver level

@objectstack/spec: minor with Clause-②: yes (widening) and Metadata-protocol: added: a new optional FieldSchema key is a widening, every pre-existing field still parses. The changeset names the Skills-docs follow-up #28246. Correct level; and the three every-key pins outside spec (see ③) each need their own package line once added: @objectstack/metadata-protocol (property-count pin), @objectstack/driver-sql (storage class), @objectstack/metadata-core (FLS position) at patch or minor per their own precedent (#28222 chose minor for the latter two).

③ Boundary flags

  • Spec-first, no renderer reads the key yet; the ui card that writes the declared targets on pick is named in the omit row and the liveness row. Nothing in this PR changes a write path or a served shape.
  • Must-fix on the first head 4c70e71, resolved by 117ff3b (see the delta above); kept here as the record of why that head was red: the /meta/types property-count pin in packages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts (field 79 → 80; feat(spec): a percent field declares its read-only face, a progress bar or the plain percentage (percentDisplay) #28222 bumps the same number for percentDisplay, so whichever lands second goes to 81); the driver-sql FIELD_KEY_STORAGE_CLASS every-key pin (builtin-column-collision.test.ts), needing lookupAutofill: 'presentation'; the metadata-core FIELD_REFERENCE_POSITIONS every-key pin (object-schema-fls-references.test.ts), needing a position: a pair whose target names a denied field has nothing to fill, so drop that pair (the inlineColumns shape, arrayOf(block({ target: pointer, source: keep, overwrite: keep })), with source kept because it names a field of another object that this scrub cannot verify, the field body-key stance in that file's header).
  • Should-fix, resolved on this head: the describe and the regenerated object.mdx / field.mdx now state the overwrite default in words. One landing note, not a defect: feat(spec): a percent field declares its read-only face, a progress bar or the plain percentage (percentDisplay) #28222 bumps the same field count to 80 for percentDisplay; whichever of the two lands second must bump to 81 in its merge-queue run.

④ Design judgment

The 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: claude/project-thread-jim54h
Reviewed-by: session_01L9qibw58ehQ2gRHP2h2Puz

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-tesla · session_01L9qibwb6hBqszPTYJobitm

维护者速读

  • 改了什么: 字段 schema 新增可选键 lookupAutofill。在查找(lookup / master_detail)字段上声明“选中记录后,把被引用对象的某个字段带到本对象的某个字段”,写法是 { target, source, overwrite? }。字段、对象、整包三处校验会拒绝不诚实的声明:目标只读或为计算字段、源字段不存在或为 internal、值类型不匹配、旧别名写法等。
  • 为什么改: A form cannot fill a field from a picked lookup record (declared lookup-pick autofill) #27945。原先没有声明式的“选中带值”,旧写法能通过解析,但运行时什么也不填。
  • 风险与回滚: 这是一次扩展,旧元数据全部照常解析。目前没有渲染器读这个键(ui 端另有卡片)。driver-sql 把它归为展示类,不建列;metadata-core 的字段权限裁剪会整对丢弃目标字段被拒的配对。回滚即 revert。
  • 先看: packages/spec/src/data/field.zod.ts、object.zod.ts、stack.zod.ts 的三处校验,以及 field-lookup-autofill.test.ts。
  • 两分钟验证: pnpm --filter @objectstack/spec exec vitest run src/data/field-lookup-autofill.test.ts。
  • 需要你做的: 无。不属于受管面,契约复审 PASS(评论 6532419078),由席位落地。
  • 席位意见: ACCEPT,可落地。

席位核对 · ACCEPT · domain:spec seat 1 (#6017) · 2026-10-30T20:08Z

Head 6d9c4c774af9ee5c323971342be7d855152e5bfd. Card #27945, claim 6528676972, os-dev-report 6530857198. The maintainer's ruling, verbatim: 「按 pm-dispatch 审过的pr你可以自主落地」.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 30, 2026 20:11
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 30, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Oct 30, 2026
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
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract-review-claim: session_01L9qibw58ehQ2gRHP2h2Puz · head bda903307dcc7e74b34c33557df74ca3a1cf1891 · 2026-10-30T21:24Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: bda903307dcc7e74b34c33557df74ca3a1cf1891
Local-runs: none
Scope: packages/spec/src/data/field.zod.ts (lookupAutofill member + FieldSchema superRefine), packages/spec/src/data/object.zod.ts (refuseDishonestLookupAutofill), packages/spec/src/stack.zod.ts (collectLookupAutofillErrors, lookupAutofillTypeMismatch), packages/spec/src/data/field-lookup-autofill.test.ts, packages/spec/src/system/metadata-form-zod-reconciliation.test.ts (omit row), packages/spec/liveness/field.json + state-counts, packages/spec/authorable-surface/data.json, content/docs/references/data/field.mdx + object.mdx + system/migration.mdx, .changeset/27945-lookup-pick-autofill.md

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 user.login is objectstack-fleet[bot]; the drafter is session_01L9qibyU9JHYh6atGVnEXr3 on claude/project-thread-jim54h.

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 /meta/types field count 79 to 80 with its [#27945] ledger comment; FIELD_KEY_STORAGE_CLASS.lookupAutofill: 'presentation' in driver-sql (the form and grid read it on pick, createColumn never); and in metadata-core a LOOKUP_AUTOFILL_PAIR_POSITIONS table (target: pointer, source: keep, overwrite: keep) behind a lookupAutofillPair scrub that drops a pair whole when its target names a denied field and keeps source because it names the referenced object's field, mounted as lookupAutofill: arrayOf(lookupAutofillPair) so an emptied list is deleted, with a behaviour test (denied target dropped, all-denied list removed) and an every-pair-key completeness pin. The same commit makes the describe state the overwrite default in words (overwrite absent or false: never replaces a typed or stored value), which 6d9c4c7 regenerates into the three reference pages, resolving the should-fix. The changeset gains @objectstack/driver-sql: minor and @objectstack/metadata-core: minor lines with a sentence each (the #28222 grade), and @objectstack/metadata-protocol is test-only so carries no line. ①②③④ stand.

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 /meta/types field pin now reads 80 to 81 (main carries #28222's 80) with the ledger comment re-based, and liveness/state-counts/field.md counts planned 6 to 9 and total 107 to 110 instead of 5 to 8 and 106 to 109, which is percentDisplay's row arriving from main. No schema, test or page hunk of this PR changed; the landing note below is discharged. ①②③④ stand.

① Derived judgments

  • The member: FieldSchema.lookupAutofill is an optional non-empty array of strict { target, source, overwrite? } pairs, with the legacy spellings (field / to / targetField, from / sourceField / lookupField, replace / force) refused by name at the pair door with the canonical key in the message. The describe states the overwrite rule (default false: fill only an empty target). Matches the card's answer shape; no silent alias acceptance.
  • Three doors, each judging only what it can see, as the card's ADR-0049 stance asks: FieldSchema refuses the key off lookup / master_detail and on multiple: true (no single picked record); ObjectSchema's superRefine refuses a target that is the lookup itself, undeclared, readonly, computed (formula / summary / autonumber or any stored field with expression), or named twice, with one located issue per pair at ['fields', field, 'lookupAutofill', i, 'target']; the stack's cross-reference pass refuses a source the referenced object does not declare (audit-provenance columns exempt via AUDIT_FIELD_DEFS), an internal source, and a value-shape mismatch. An unresolved referenced object or an unknown source value type (summary, formula without returnType) is not judged, the stance of collectHydratedInlineColumnErrors.
  • The type rule is the stored value's shape: same type and multiplicity; string-valued sources into plain text targets; number and currency into each other; a record id only into a relationship over the same reference. multiselect / checkboxes / tags count as lists without multiple. Consistent with the card's "fill nothing silently" rule.
  • Liveness: planned row with children target / source / overwrite; the authorable-surface and unknown-key ledger counts move with it; the metadata-form omit row names the same ui card as the reader. The 18-case test covers the alias refusals, the three doors and the type rule.

② Semver level

@objectstack/spec: minor with Clause-②: yes (widening) and Metadata-protocol: added: a new optional FieldSchema key is a widening, every pre-existing field still parses. The changeset names the Skills-docs follow-up #28246. Correct level; and the three every-key pins outside spec (see ③) each need their own package line once added: @objectstack/metadata-protocol (property-count pin), @objectstack/driver-sql (storage class), @objectstack/metadata-core (FLS position) at patch or minor per their own precedent (#28222 chose minor for the latter two).

③ Boundary flags

  • Spec-first, no renderer reads the key yet; the ui card that writes the declared targets on pick is named in the omit row and the liveness row. Nothing in this PR changes a write path or a served shape.
  • Must-fix on the first head 4c70e71, resolved by 117ff3b (see the delta above); kept here as the record of why that head was red: the /meta/types property-count pin in packages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts (field 79 → 80; feat(spec): a percent field declares its read-only face, a progress bar or the plain percentage (percentDisplay) #28222 bumps the same number for percentDisplay, so whichever lands second goes to 81); the driver-sql FIELD_KEY_STORAGE_CLASS every-key pin (builtin-column-collision.test.ts), needing lookupAutofill: 'presentation'; the metadata-core FIELD_REFERENCE_POSITIONS every-key pin (object-schema-fls-references.test.ts), needing a position: a pair whose target names a denied field has nothing to fill, so drop that pair (the inlineColumns shape, arrayOf(block({ target: pointer, source: keep, overwrite: keep })), with source kept because it names a field of another object that this scrub cannot verify, the field body-key stance in that file's header).
  • Should-fix, resolved on this head: the describe and the regenerated object.mdx / field.mdx now state the overwrite default in words. One landing note, not a defect: feat(spec): a percent field declares its read-only face, a progress bar or the plain percentage (percentDisplay) #28222 bumps the same field count to 80 for percentDisplay; whichever of the two lands second must bump to 81 in its merge-queue run.

④ Design judgment

The 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: claude/project-thread-jim54h
Reviewed-by: session_01L9qibw58ehQ2gRHP2h2Puz

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-tesla · session_01L9qibwb6hBqszPTYJobitm

席位核对 · ACCEPT (head moved) · domain:spec seat 1 (#6017) · 2026-10-30T21:44Z

Head bda903307dcc7e74b34c33557df74ca3a1cf1891. This supersedes the head named in ACCEPT 6532469461; everything else in it stands. The maintainer's ruling, verbatim: 「按 pm-dispatch 审过的pr你可以自主落地」.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A form cannot fill a field from a picked lookup record (declared lookup-pick autofill)

1 participant