Skip to content

feat(spec): a lookup can declare its target's name visible to every reader of the record (referenceNameVisible) - #29379

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-yert63
Nov 2, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-yert63

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread
Fixes #29171

Clause-②: yes (widening)
Metadata-protocol: added

维护者速读

  • 改了什么:lookup(及 user)字段新增可选布尔键 referenceNameVisible。设为 true 表示:能读这条记录的人,即使读不到被引用的那条记录,也可以看到它的名称(目标对象 nameField 的值)和 id,别的都看不到;展开同样只给名称 + id,界面只显示文字、不带链接。
  • 为什么改:执行裁决 6574398310 字母 B。门户用户(如 hotsrm 供应商)读得到自己的记录,但上面指向品类、物料、内部员工的关联只显示「受限记录」;现在由应用按字段声明哪些名称可以给门户看。
  • 风险与回滚:只加宽(可选新键,默认关,不写就和今天完全一样);其他字段类型(含 master_detail)写这个键会在解析时被拒。本 PR 只是契约,服务端还不返回名称(describe 带 [PLANNED — not yet served])。回滚即 revert。
  • 先看:packages/spec/src/data/field.zod.ts(referenceNameVisible 声明块与 superRefine 拒收分支)、packages/spec/src/data/field-reference-name-visible.test.ts、.changeset/29171-lookup-reference-name-visible.md。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/data/field-reference-name-visible.test.ts,预期 1 个文件全绿(19 条)。
  • 需要你做的:无,席位落地(合并前席位跑一次契约复审)。

What changes

FieldSchema (packages/spec/src/data/field.zod.ts) gains one optional boolean, referenceNameVisible, beside referenceVia / referenceDeletedWhen. It carries the ruling's semantics (director ruling 6574398310, letter B):

  • true declares that a principal who can read the record carrying the field may see the referenced record's display name (the target object's nameField, ADR-0079) and its id, even when it cannot read the referenced record. Nothing else of the target: $expand on the field returns the same name and id only, the field's own displayField does not widen what is shown, and the name renders as text with no link.
  • Absent or false keeps the restricted-reference posture (objectui#13335). No default materializes at parse.
  • Placement is the referencing field only. There is no target-object switch (the ruling refuses one).
  • Accepted on lookup and on user. A user field is the sys_user reference, which the ruling says the same key covers. Refused at parse on every other type, with one custom issue at referenceNameVisible naming the legal types and the offending one, in the mailAssociation refusal shape. That includes master_detail (the ruling places the key on the referencing lookup) and an authored false off-type.
  • Declared ahead of its producer (ADR-0049): the describe opens with [PLANNED — not yet served], which the liveness walker reads as planned (liveness/state-counts/field.md: planned 5 → 6, classified 110 → 111). The docblock states the read and expand bounds as MUST statements for the server card, and says that no runtime serves the name yet.

Generated files that ride along (check:generated --fix, which named 2 of 14 artifacts stale): content/docs/references/data/field.mdx, data/object.mdx, system/migration.mdx, packages/spec/authorable-surface/data.json (+1 key), packages/spec/liveness/state-counts/field.md.

Changeset: .changeset/29171-lookup-reference-name-visible.md. It bumps spec minor, plus driver-sql and metadata-core patch for the census entries below.

Forced edits (closed key censuses over FieldSchema)

A new FieldSchema key reds each of these by design. Each was measured red, then given one entry:

  • packages/drivers/driver-sql/src/builtin-column-collision.ts FIELD_KEY_STORAGE_CLASS: presentation. The read and expand path reads the key, never createColumn. Red was builtin-column-collision.test.ts "classifies EVERY FieldSchema key".
  • packages/metadata-core/src/object-schema-fls-references.ts FIELD_REFERENCE_POSITIONS: keep. It is a boolean facet that names no field. Red was object-schema-fls-references.test.ts "classifies every FieldSchema key".
  • packages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts: the field property count goes 81 → 82, with a dated comment line.
  • packages/spec/src/system/metadata-form-zod-reconciliation.test.ts: an omit ledger entry, on the lookupAutofill / conditionalFormatting precedent. The field form does not offer a key nothing serves. The ui card adds the form row together with the renderer.
  • packages/spec/src/data/field-customization.test.ts STRUCTURAL_FIELD_KEYS gains the key (a permission-class key). This pins that it can never join the ADR-0146 D10 display whitelist, so a customer extension cannot patch a vendor's lookup into exposing names. It was not red; the edit is added coverage.

Follow-ups (filed by the spec seat once this lands, citing ruling 6574398310)

  • server (domain:server): the objectql/REST read and $expand branch. For a field declaring referenceNameVisible: true whose target the caller cannot read, it does an elevated name-only read: the target's nameField value and id, nothing else, displayField ignored. It drops the [PLANNED — not yet served] marker and flips the pin in field-reference-name-visible.test.ts.
  • ui (domain:ui): the text-only rendering with no link, and the field form row (which removes the reconciliation omit entry).
  • hotsrm (cloud/hotsrm lane): remove the snapshot text fields and sync flows (v0.6 wrap-up: read-only snapshot fields for lookups the portal cannot read hotsrm#95) once both land.

Tests

packages/spec/src/data/field-reference-name-visible.test.ts (new, 19 tests):

  • accepts on a lookup, a multi-value lookup, a lookup to sys_user, a user field, and through ObjectSchema;
  • an explicit false is kept; absent stays absent; a non-boolean value gives invalid_type;
  • refused on master_detail, tree, text, select, formula and number. Each case checks one custom issue at the key and the named subjects, plus a positive control without the key. An off-type false is refused too, and the refusal message carries no tracker number;
  • the JSON-schema projection is an optional boolean; the description opens with the planned marker, names ADR-0049 and carries the ruling's bounds.

Consumer suites (downstream of spec), at 734d67a6, each through os-verify-lock.sh (VERDICT command-exit 0):

  • @objectstack/spec test: Test Files 792 passed (792), Tests 23411 passed, 1 todo. typecheck: exit 0.
  • @objectstack/driver-sql test: 236 passed, 12 skipped (248); 3847 passed, 214 skipped. typecheck: exit 0.
  • @objectstack/metadata-core test: 22 files, 885 tests passed. typecheck: exit 0.
  • @objectstack/metadata-protocol test: 301 passed, 4 skipped (305); 22127 passed, 26 skipped.
  • @objectstack/lint test: 189 files, 7830 tests passed.

Before the census edits, at 4c09cb03, four pins were red as designed. Each named referenceNameVisible as unclassified: driver-sql builtin-column-collision.test.ts, metadata-core object-schema-fls-references.test.ts, metadata-protocol protocol.meta-types-degenerate-derivation.test.ts (82 against 81), and spec metadata-form-zod-reconciliation.test.ts.

No ablation: the parse refusal is new code with its own refusal and control pins, and no existing tested behaviour changed.

Acceptance notes

  • user is accepted beside lookup. This reads the ruling's "the same key covers sys_user names; no separate carve-out" as covering the dedicated sys_user field type, which is stored the same way as a lookup. If the seat reads it as lookup only, the change is a one-line narrowing before landing (open question in the report).
  • master_detail is refused. The ruling names the referencing lookup. Narrowing first keeps a later widening additive.
  • The delta's "beside referenceFilters": that key is a retired tombstone on main (16.x line), so the new key sits beside referenceVia / referenceDeletedWhen.

Verification

Derived gates at 734d67a

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 106 commands (13 changed paths against merge base 2f7977cd2), each run with stdin closed. 104 exited 0 on the first pass. check:skill-examples and check:dual-build-cjs-loads exited 3 (PREREQUISITE NOT MET: no package dists). After turbo run build --filter='./packages/**' (78/78, under the lock), both exited 0. --ran reconciliation: "106 derived, 106 run, 0 NOT-MEASURED, 0 UNRUN" (a DERIVED zero).

Also run: pnpm --filter @objectstack/spec check:generated named 2 of 14 stale (check:docs, check:liveness), fixed with --fix. Offline check-changeset-no-major --event on this body: LEVEL AXIS accepted (yes (widening) + added, spec minor; driver-sql and metadata-core patch). eslint --no-inline-config on the 7 changed .ts files: exit 0.

Dogfood: n/a. The title has no !, and Clause-② carries no (narrowing) arm.

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

Declared name exposure on the referencing field (director ruling
6574398310, letter B): TRUE declares that a reader of the record may see
the referenced record's nameField value and id even when it cannot read
the target, nothing else. Refused at parse off lookup/user. Declared
ahead of its producer (ADR-0049 [PLANNED — not yet served] marker).

Claude-Session: https://claude.ai/code/session_01ADHkmQAss6gtFU3jRjApfz
Co-authored-by: Claude <noreply@anthropic.com>
The key-census tables that close over FieldSchema each gain the new key:
driver-sql storage classification (presentation), metadata-core FLS
positions (keep), the metadata-protocol field property count (81 to 82),
and the field form reconciliation ledger (omitted while planned).

Claude-Session: https://claude.ai/code/session_01ADHkmQAss6gtFU3jRjApfz
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:data protocol:system tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

11 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))
  • 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/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))

⛔ 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
  • 2 changed file(s) yielded no anchor (packages/spec/authorable-surface/data.json, packages/spec/liveness/state-counts/field.md) — pages documenting those are invisible to this run
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 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; 101 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 dfbe34d8874326be643042c36211ba6ec5e69c7f → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 30253f2d143c4c0dcdf12d2e00f650ab25b8bc40 — the merge of head 32bc8d47a30a1a79950284a23555efdcc65ab590 into base dfbe34d8874326be643042c36211ba6ec5e69c7f, 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 30253f2d143c4c0dcdf12d2e00f650ab25b8bc40 && git checkout 30253f2d143c4c0dcdf12d2e00f650ab25b8bc40
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin dfbe34d8874326be643042c36211ba6ec5e69c7f 32bc8d47a30a1a79950284a23555efdcc65ab590 && git checkout -B drift-repro dfbe34d8874326be643042c36211ba6ec5e69c7f && git merge --no-ff 32bc8d47a30a1a79950284a23555efdcc65ab590

node scripts/docs-audit/affected-docs.mjs --json dfbe34d8874326be643042c36211ba6ec5e69c7f

⚠️ 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 dfbe34d8874326be643042c36211ba6ec5e69c7f → 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-steve · session_01ADHkmPi25XXKx92Xmg4viS

Head-sha: 32bc8d47 · read 2026-11-02T14:49Z

维护者速读

  • 改了什么:lookup / user 字段新增可选键 referenceNameVisible。设为 true 表示:能读这条记录的人,即使读不了被引用的记录,也能看到它的名称(nameField 值)和 id,别的都看不到;$expand 同样只给名称和 id,渲染成不带链接的文字。默认关闭,其他字段类型(含 master_detail)写了会被拒。
  • 为什么改:总监裁决 B(Portal UX: let a lookup show the target's name when the viewer cannot read the target (decision) #29171):门户用户看自己的记录时,关联到不可读对象的字段只显示「受限记录」。键先声明,标 PLANNED,读取和渲染由 server、ui 后续卡实现。
  • 风险与回滚:放宽(新增可选键),不影响未声明的字段。回滚即撤销本 PR。
  • 先看:packages/spec/src/data/field.zod.ts 新键及其类型限制。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/data/field-reference-name-visible.test.ts。
  • 需要你做的:无,席位落地。

席位意见:可落地,等契约复审。

席位核对 — ACCEPT · domain:spec#1 · head 32bc8d47

  1. Shape: draft, base main (merged after feat(spec)!: object extension fieldPatches refuse a field the extension replaces, key option patches by value, refuse __proto__, carry display renames #29331, 3fb74b6d30), Fixes #29171, Clause-②: yes (widening) / Metadata-protocol: added at line start in the body and the changeset (spec minor; driver-sql and metadata-core patch). Surface: claim 6576958258 as amended on Portal UX: let a lookup show the target's name when the viewer cannot read the target (decision) #29171 (four forced census entries). Not governed.
  2. Diff, read on the head: FieldSchema.referenceNameVisible (optional boolean, beside referenceVia / referenceDeletedWhen), accepted on lookup and user, refused at parse on every other type (one custom issue naming the types, an off-type false included). The describe opens [PLANNED — not yet served] and states the ruling's semantics: name and id only, $expand the same, displayField does not widen it, rendered as text with no link. No field.form row yet: an omit ledger entry (the lookupAutofill precedent); the ui follow-up adds the row with the renderer.
  3. Open question answered A (the seat, on the thread): user is the sys_user reference the ruling covers; master_detail stays out because the ruling places the key on the referencing lookup, and a later widening stays additive.
  4. Readings (dev, on 32bc8d47): 106/106 derived gates, DERIVED zero; spec 792 files; metadata-protocol 304 files; both census tests green; offline level gate accepted. Check-runs: 65, all success or skipped. The PR body's readings are at the pre-merge 734d67a6; the report carries the head's.
  5. Ruling note: the ruling asked for a contract review before landing. Under the current lane rule (triage 6575141496) the seat runs it at CONTRACT_REVIEW_TIER (claim line follows).
  6. Landing: after a PASS, the seat readies and queues, then files the follow-ups citing the ruling: server (objectql/REST read and $expand branch), ui (text-only render and the form row), and hotsrm snapshot removal (cloud/hotsrm lane).

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS

Contract-review-claim: session_01ADHkmPi25XXKx92Xmg4viS · head 32bc8d4 · 2026-11-02T14:49Z

@objectstack-fleet objectstack-fleet Bot added the needs:contract-review Marker only — PR awaits its at-tier Contract review PASS; no check reads it, the record decides label Nov 2, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 32bc8d47a30a1a79950284a23555efdcc65ab590
Local-runs: none
Scope: packages/spec/src/data/field.zod.ts + packages/spec/src/data/field-reference-name-visible.test.ts, packages/spec/src/data/field-customization.test.ts, packages/spec/src/system/metadata-form-zod-reconciliation.test.ts, packages/drivers/driver-sql/src/builtin-column-collision.ts, packages/metadata-core/src/object-schema-fls-references.ts, packages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts, packages/spec/liveness/state-counts/field.md, packages/spec/authorable-surface/data.json, .changeset/29171-lookup-reference-name-visible.md, content/docs/references/data/field.mdx, content/docs/references/data/object.mdx, content/docs/references/system/migration.mdx; read to judge them: packages/spec/scripts/liveness/check-liveness.mts (marker and ledger resolution), packages/spec/liveness/field.json and app.json (planned-row precedent), packages/spec/src/data/object.zod.ts (nameField), docs/adr/0049-no-unenforced-security-properties.md, docs/adr/0079-record-display-name.md, scripts/check-changeset-no-major.mjs (level axis), the referenceVia / referenceDeletedWhen / mailAssociation / lookupAutofill gates and the user type note in field.zod.ts on this head, and the driver-sql / metadata-core CHANGELOGs for the mailAssociation census precedent.

Inputs read: card #29171 body and every comment, the director ruling 6574398310 letter B 2026-11-02T10:05:41Z (binding), seat claim 6576958258 naming referenceNameVisible, dev report 6580079196, claim amendment 6580125189; PR #29379 body, file list (13 files) and git diff origin/main...32bc8d47 (+293 / −2); the 65 check-runs on this head (last completed 2026-11-02T14:22:17Z): every completed run success or skipped, the ten required among them, plus Check Changeset, PR-body guards and Spec property liveness success. The dispatch order and the seat's conclusions were not inputs.

① Derived judgments

  • Accept-set change, measured on the tree: FieldSchema is a strictObject, so on main the key referenceNameVisible is refused everywhere as unrecognised. This head accepts an optional boolean on type: 'lookup' and type: 'user' and refuses it on every other type through the superRefine (field.referenceNameVisible !== undefined && type !== 'lookup' && type !== 'user'), one custom issue at ['referenceNameVisible'] naming the two legal types and the offending one. Widening only: no document that parses on main parses differently here (git grep referenceNameVisible origin/main is empty; the only new refusals are of a key main already refused). No default materialises (the test pins KEY in result.data false when absent).
  • Refusing an off-type false is right: it is the mailAssociation precedent (same !== undefined gate, pinned in field-mail-association.test.ts line 94), the same shape referenceVia, referenceDeletedWhen and lookupAutofill use, and it keeps a tool from spraying referenceNameVisible: false onto fields it describes nothing on. Refusing master_detail is a narrowing of the ruling's silence, not a contradiction: the ruling names the referencing lookup; a later widening to master_detail stays additive (the lookupAutofill gate shows the two-type form it would take).
  • The four forced census entries hold against the code they claim: FIELD_KEY_STORAGE_CLASS has exactly two classes (storage | presentation) and the key reaches no createColumn branch, so presentation is the only true answer; FIELD_REFERENCE_POSITIONS: keep is right because the key is a boolean naming no field of either object (the mailAssociation row beside it is the template); the /meta/types count 81 → 82 is the one property added; the form reconciliation omit entry follows the lookupAutofill precedent and states the true reason (no reader). STRUCTURAL_FIELD_KEYS gaining the key pins it out of the ADR-0146 D10 display whitelist, which matters: a customer extension patch must not be able to flip a vendor lookup into exposing names.
  • Liveness: check-liveness.mts resolves ledger row → describe marker → UNCLASSIFIED, and MARKER_RE.planned is /\[planned|not yet implemented|coming soon/i, so [PLANNED — not yet served] classifies as planned with no ledger row; state-counts planned 5 → 6, classified 110 → 111 is the regenerated consequence, and the CI gate is green.
  • Key name and placement collide with nothing: no nameVisible / name_visible spelling exists on main in packages or ui; the reference* prefix is the family the key belongs to, and referenceFilters (the delta's "beside" anchor) is a 16.x tombstone at field.zod.ts line 1339, so beside referenceVia / referenceDeletedWhen is the right seat.

② Semver level

@objectstack/spec: minor — correct; Clause-②: yes (widening) and Metadata-protocol: added are the true pair for a new optional authorable key. @objectstack/driver-sql and @objectstack/metadata-core: patch — correct; the level axis (check-changeset-no-major.mjs, maintainer ruling A) requires at least ONE moved package at minor for an added kind, which spec supplies, and the mailAssociation census entries shipped in both packages as Patch Changes (e5687cb). Both declaration lines are present and identical in the PR body and the changeset; the changeset body states the contract and names the no-op-today bound.

③ Boundary flags

  • Open question (types): A (lookup + user) is the right reading. field.zod.ts documents user as "a lookup specialized to the sys_user system object … stored IDENTICALLY to 'lookup' … resolved via the same $expand machinery", and the ruling says "the same key covers sys_user names; no separate carve-out". B would force Field.user() authors onto Field.lookup('sys_user') for a key the ruling says covers them; C widens past the ruling's text.
  • The six deviations: (1) forced-by-gate surface breach — accepted by claim amendment 6580125189, no objectql / REST / ui file touched; (2) driver-sql / metadata-core patch — precedent-true (② above); (3) referenceFilters tombstone — verified; (4) form omit entry instead of a row — the lookupAutofill precedent, the ui card adds the row; (5) PR-body readings at the pre-merge head — body only, the tree and this head's check-runs decide; (6) attribution footer — body only. None changes the contract.
  • Not a flag, recorded: no planned row was added to packages/spec/liveness/field.json. The marker fallback carries it and the gate is green, and the view filter or group on main is marker-only too; but the two closest spec-first precedents (app.navigation.dataSource spec / objectui navigation: an app's navigation item cannot read data, so a label or a visible gate cannot follow a live count (the control plane's 「环境」 for a single-environment organization) #24332, field.conditionalFormatting spec(ui): field-level conditionalFormatting — a field declares [{ condition, style }] cell rules (CEL over value and record), the row block's own grammar, for presentational rules that carry no semantic (maintainer-directed) #22228) carry a planned row citing the refuser as evidence with the flip condition in its note. The server card that drops the marker should write that row (or the flip) in the same edit; no authorWarn is needed on the settledWhen / conditionalFormatting reasoning (the gap is a display refinement with named consumer cards).
  • ADR-0049 is cited as the "declared ahead of its producer" authority; the ADR's own marker is [EXPERIMENTAL — not enforced] and planned is the liveness vocabulary the gate reads under the same umbrella. The citation is sound here because the key fails closed when unenforced: an authored true exposes nothing today, so there is no false security boundary of the kind the ADR forbids.

④ Design judgment

Against ruling 6574398310 letter B, clause by clause, on the describe and docblock at field.zod.ts lines 1705–1741: (a) name + id only — stated, with "Nothing else of the target"; (b) $expand returns the same — stated as a MUST in the docblock and in the describe; (c) displayField does not widen — stated (a sound derivation of "nothing else": displayField names a picker label field of the target, and the ruling fixes the exposed value to nameField); (d) text, no link — stated; (e) default off, absent or false keeps the objectui#13335 posture — stated, and the parse materialises nothing; (f) placement on the referencing field, no target-object switch — stated; (g) sys_user covered — stated through user and through lookup to sys_user, both pinned. The docblock adds one bound the ruling implies but does not spell: a principal who CAN read the target is served exactly what it is served without the key. No promise exceeds the [PLANNED — not yet served] marker: the describe says no runtime serves the name and setting the key changes no read today, which covers the renderer half as well.

Can the server and ui follow-ups implement from this contract alone? Yes, with two readings they must take from the cited ADR rather than the describe: (1) the describe says "the target object's nameField value"; ADR-0079 D4 (cited in the docblock) is the resolution every consumer shares, nameField ?? displayNameField ?? deterministic derivation, floor Record #id, so a target with no explicit nameField still has a display name to return — the server card should resolve through resolveRecordDisplayName, not read nameField literally; (2) a multi-value lookup (multiple: true) is accepted and the test says "each referenced record is named alike", but the describe does not say so — the server card returns name + id per referenced record. Neither is a must-fix: the first is reachable from the ADR the contract cites and the second is the only reading consistent with "name and id only". Tried to break it further: no object-level or stack-level refinement is needed at parse (the target's nameField is a runtime resolution, not an authoring fact); Field.lookup / Field.user configs derive from z.input of the schema, so the key is typeable with no hand-maintained interface to update; the refusal prose carries no tracker number; the generated references regenerated byte-exact (check:generated green).

Must-fix: none.

Implemented-by: claude/project-thread-yert63
Reviewed-by: session_01ADHkmPi25XXKx92Xmg4viS

VERDICT: PASS

@objectstack-fleet objectstack-fleet Bot removed the needs:contract-review Marker only — PR awaits its at-tier Contract review PASS; no check reads it, the record decides label Nov 2, 2026
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 2, 2026 14:58
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit 9467a1b Nov 2, 2026
74 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-yert63 branch November 2, 2026 15:23
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 protocol:data protocol:system size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Portal UX: let a lookup show the target's name when the viewer cannot read the target (decision)

1 participant