Skip to content

fix(rest): the one-request record page accepts a related list keyed by a user field - #29965

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

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-nzsli6

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread

Fixes #29946

维护者速读

  • 改了什么: 一次请求的记录页路由 POST /data/:object/:id/page 的关联列表检查,不再用路由里私有的 lookup/master_detail 名单加裸读 reference,改问 spec 的唯一目标裁判 referenceTargetOf(引擎的 $expand 和删除级联读的就是它)。user 字段按类型指向 sys_user,所以 sys_user 页上按 owner/assignee 的关联列表重新返回 200 和行。
  • 为什么改: 18.3.0 回归(duly#185):路由由 feat(rest,console): one request opens a record page #27713 新建,18.2.0 没有这个文件,控制台按普通列表读逐个取关联列表,不做类型检查;18.3.0 起整页打开报 400 invalid_relationship,而该列表自己的行查询 { [field]: id } 本可以照常回答。
  • 风险与回滚: 只放宽回 18.2.0 的可读范围,不引用页对象的字段仍是定位 400(别的对象页上的 user、指向别处的 lookup、别的对象上的 tree、读不了的 reference);错误文案改为列出四种类型。@objectstack/rest patch。回滚 = revert 本 PR。
  • 先看: packages/rest/src/record-page-routes.ts 的 referenceOf 与 invalid_relationship 一处;测试 record-page-routes.test.ts 的 [#29946] describe。
  • 两分钟验证: pnpm --filter @objectstack/rest exec vitest run src/record-page-routes.test.ts,预期 32 passed。
  • 需要你做的: 无,席位评审并落地。

What changes

  • @objectstack/rest, record-page-routes.ts. The related-list check drops the route's private RELATIONSHIP_TYPES set (lookup, master_detail) and its raw reference read. referenceOf now asks the spec's one arbiter, referenceTargetOf from @objectstack/spec/data (the reference value types lookup, master_detail, user, tree; a user field's target is fixed to sys_user by its type, whether or not its carrier spells it). That is the same judgement the engine's $expand gate and referential delete walk read (engine.ts deleteWalkTargetOf answers referenceTargetOf for a user field), so the route and the related list's row query ({ [field]: id }, or $contains on a multi-valued field) cannot disagree about which fields relate a child to its parent.
  • An unreadable reference carrier (object- or array-valued, never parsed) throws inside the arbiter. The route catches that and keeps the located 400 invalid_relationship it answered before, not a thrown 500.
  • The refusal message names the four kinds: '<field>' on '<child>' is not a lookup, master-detail, user or tree field that references '<object>'. Code, status and location are unchanged.
  • Reading, tree: it comes with the arbiter. A tree field relates a child to its own object only (its reference is self-referential by contract), and the row query follows it the same way, so a related list of a record's children on its own page is now accepted; a tree on another object's page is still refused. There is no separate owner field type in FieldType (measured field.zod.ts), so nothing else changes.
  • Nothing new is exported. The url action at record_header from the card's second paragraph is a separate domain:ui card and is not touched.

Why

Card #29946, from duly#185: a record page on sys_user with a related list of duly_task keyed by owner (a Field.user) answered 400 VALIDATION_FAILED: 'owner' on 'duly_task' is not a lookup or master-detail field that references 'sys_user'. on 18.3.0, so the console's full-page record view showed "could not load this record".

Where it came from: record-page-routes.ts did not exist at @objectstack/rest@18.2.0 (the contents API answers 404 at that tag). #27713 (0027c4a2b8, 2026-10-30) created the route with RELATIONSHIP_TYPES = new Set(['lookup', 'master_detail']) and the raw reference read; #28682 and #28991 kept the check as it was. On 18.2.0 the console read each related list through the ordinary list read of the child filtered by the field, which applies no relationship-type check, so the page rendered.

Clause-②: no
Metadata-protocol: none

Verification

  • record-page-routes.test.ts, new [#29946] describe through the real route handler:
    • a related list on sys_user keyed by a bare { type: 'user' } field, a user field with reference: 'sys_user', and a multi-valued user field answers 200 with each list's rows; the reads are { owner: 'u9' }, { assignee: 'u9' }, { reviewers: { $contains: 'u9' } };
    • a user field on the account page is still 400 invalid_relationship at related.0.field, with the exact new message, and nothing is read;
    • a lookup relates only to its own reference, and a tree only to its own object;
    • an unreadable reference carrier is the located 400, not a 500.
    • The existing pin (title, owner as a lookup to another object, nope are all 400) is unchanged and green.
  • Ablations, each run once and restored:
    1. Old check back (!new Set(['lookup','master_detail']).has(def.type) || …): 2 failed, a related list on sys_user keyed by a user field answers 200 … and a lookup keeps relating only to its own reference, and a tree to its own object, both AssertionError: expected 400 to be 200.
    2. Accept any user field regardless of target (… && def.type !== 'user'): 1 failed, a user field on a page that is not sys_user is still a located 400 …, AssertionError: expected 200 to be 400.
    3. Restored: 32 passed.
  • @objectstack/rest^... closure build (26 tasks): exit 0. @objectstack/rest pnpm test: 383 files, 7159 passed / 332 skipped, exit 0. tsc --noEmit: exit 0. check:test-typecheck: exit 0.
  • pnpm lint: exit 0.
  • node scripts/pm/dispatch-gates.mjs --commands at the head rebased on origin/main 40fa4335: 59 derived commands, 58 exit 0, 1 exit 3 (prerequisite, nothing measured): pnpm check:dual-build-cjs-loads reads a whole-repo build and only the rest closure was built; it reads no path this diff touches. check-plugin-teardown-shape.mjs --self-test first exited 3 (its fixture commit 621a4876 was not in the shallow clone) and is exit 0 (48 cases) after fetching that commit. dispatch-gates --ran: ✓ 59 derived famil(ies) accounted for — 58 run, 1 NOT-MEASURED. CI's Build Core and type-check lanes are the jobs the tool lists as not measurable locally.
  • Serial: no open PR edits packages/rest/src/record-page-routes.ts (scanned every open PR's file list before opening).
  • Not run locally: the record-page.dogfood.test.ts suite (the dogfood regression gate runs on main's push battery).

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (spec untouched)
  • 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: n/a.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the 维护者速读 section is the seat's Chinese summary for the maintainer).
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WGufzijwvEjcKQArbvuX9p

…y a user field

The related-list check in record-page-routes.ts kept a private
lookup/master_detail list and read `reference` raw, so a related list on
sys_user keyed by a `user` field answered 400 invalid_relationship though
its row query follows that field (18.3.0 regression, duly#185). It now asks
the spec's one arbiter, referenceTargetOf, the judgement the engine's
$expand and delete walk read. A field that does not reference the page's
object is still a located 400.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzijwvEjcKQArbvuX9p
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Nov 3, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5

Contract review

Served-tier: TIER_DEFAULT
Head-sha: e90f4f4b10740ec77b4d1f0b531ecc42a5693569
Local-runs: none

Face: declaration (Clause-②: no, Metadata-protocol: none). No published schema and no governed path in the committed tree (3 files). Read: the full diff, card #29946 and its triage audit 6610340522, referenceTargetOf / referenceCarrierOf (packages/spec/src/data/field-value.zod.ts), and the dev report (/mnt/project-files/issue-29946/os-dev-report.md).

① Derived judgments

  • The fix reuses the one arbiter. The private RELATIONSHIP_TYPES set and the raw reference read are gone. The related-list check asks referenceTargetOf, which answers for the reference value types (lookup, master_detail, user, tree), fixes user to sys_user by its type, and returns undefined for every other type. That is the judgement $expand and the referential delete walk read, so the route and the row query can no longer disagree. Correct.
  • The refusal is kept where it belongs. A user field on a non-sys_user page, a lookup to another object, a tree on another object, and an unreadable carrier (the arbiter throws; caught here as "not a relationship") each still answer the located 400 invalid_relationship. The message now names user and tree. Correct.
  • tree comes along (flagged by the dev): a tree field references its own object, so a related list of children keyed by the parent field on the same object's page is a real relationship the row query follows. Accepted; it is part of the arbiter, not a second private list.
  • Regression origin: 18.2.0 had no such route check (each related list was read through the list read); feat(rest,console): one request opens a record page #27713 introduced it. Stated in the changeset.
  • Pins: the card's reproduction through the real route (a related list on sys_user keyed by a user field answers with its rows), a user field on another page still 400, and lookup/master-detail unchanged. Ablations went red (2, then 1) and were restored, 32/32.

② Semver level

'@objectstack/rest': patch. No exported surface moves (referenceOf is module-private); the route accepts again what 18.2.0 served. Consistent with both declaration lines.

③ Boundary flags

  • No open_questions. The second defect in the card body (a url action at record_header) is the domain:ui card triage split off; it is not touched here.
  • The order's attribution block named Fixes #29891 (the seat's template error); the dev wrote Fixes #29946. Correct.

Implemented-by: claude/project-thread-nzsli6
Reviewed-by: session_01WGufzibcN9oHa8kDbTTnL5

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5

席位核对

Verdict: ACCEPT at head e90f4f4b10740ec77b4d1f0b531ecc42a5693569 · 2026-11-03T18:20Z

  • Shape: 3 files (the rest record-page route's related-list check, its route test, and one patch changeset). No published schema, no governed path ⇒ not Tier H. TIER_DEFAULT review PASS 6612013793 at this head.
  • Premise measured: an 18.3.0 regression from feat(rest,console): one request opens a record page #27713 (Upgrade duly to @objectstack 18.3.0, with the full browser check duly#185). The dev reproduced it through the real route: a related list on sys_user keyed by a user field answered 400 invalid_relationship.
  • Gates: rest tests, tsc, test-typecheck and lint pass. dispatch-gates 58/59 exit 0; dual-build-cjs-loads is NOT-MEASURED because it needs a full build, and CI runs it. Ablations went red and were restored. Landing waits for every check on this head to be green or an expected skip.
  • Accepted deviations: harness-pinned branch claude/project-thread-nzsli6; Fixes #29946 in place of the order's mistyped number. The signed trailer is appended by the seat through the relay.
  • Landing: p1 release-blocker. On all-green at this head, the seat sends pr_ready + automerge_enable through the relay.

@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/rest, touching 6 documentable anchor(s).

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

  • content/docs/frontend/layout.mdx (via /:object/:id/page (route, bridged from symbol referenceOf — its route source's handler names it))
  • content/docs/frontend/plugins/plugin-detail.mdx (via /:object/:id/page (route, bridged from symbol referenceOf — its route source's handler names it))
  • content/docs/frontend/renderers.mdx (via /:object/:id/page (route, bridged from symbol referenceOf — its route source's handler names it))
  • content/docs/getting-started/quick-reference.mdx (via /:object/:id/page (route, bridged from symbol referenceOf — its route source's handler names it))
  • content/docs/protocol/objectui/concept.mdx (via /:object/:id/page (route, bridged from symbol referenceOf — its route source's handler names it))
  • content/docs/protocol/objectui/layout-dsl.mdx (via /:object/:id/page (route, bridged from symbol referenceOf — its route source's handler names it))
  • content/docs/protocol/objectui/record-alert.mdx (via /:object/:id/page (route, bridged from symbol referenceOf — its route source's handler names it))
  • content/docs/ui/apps.mdx (via /:object/:id/page (route, bridged from symbol referenceOf — its route source's handler names it))
  • content/docs/ui/print-pages.mdx (via /:object/:id/page (route, bridged from symbol referenceOf — its route source's handler names it))
  • content/docs/ui/views.mdx (via /:object/:id/page (route, bridged from symbol referenceOf — its route source's handler names it))

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

  • content/docs/releases/v17/17-6.mdx (via /:object/:id/page (route, bridged from symbol referenceOf — its route source's handler names it))
  • content/docs/releases/v18/18-0.mdx (via /:object/:id/page (route, bridged from symbol referenceOf — its route source's handler names it))
  • content/docs/releases/v18/18-3.mdx (via /:object/:id/page (route, bridged from symbol referenceOf — its route source's handler names it))

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
  • 1 anchor(s) matched too much of the corpus to be a work list: master_detail (literal, 40 pages)
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 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; 103 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 — 19 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 3b8c724cf21fe2b39b2c635c6ef2a2448fa952fb → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 3b8c724cf21fe2b39b2c635c6ef2a2448fa952fb

⚠️ 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 3b8c724cf21fe2b39b2c635c6ef2a2448fa952fb → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…nd-object read census

record-page-routes.ts now calls referenceTargetOf, so the second-object
read-exposure pin needs it classified. It reads no record of the target:
the target is the addressed object, read behind the route's own get
exposure gate.

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

Copy link
Copy Markdown
Contributor Author

domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 3ca0ddde4314f0bbda3df23c6d92f14b70cb7891
Local-runs: none

Face: declaration (Clause-②: no, Metadata-protocol: none). Delta review of PASS 6612013793 at e90f4f4b: one commit, one file, packages/core/src/security/second-object-read-exposure.pin.test.ts (+9). Everything else carries over unchanged.

① Derived judgments

  • The ledger row is required and correctly graded. The pin requires every caller of referenceTargetOf to be classified, and record-page-routes.ts is a new caller, which is why Test Core 4/8 went red at e90f4f4b (that red run is the ablation). not-a-served-read is right: the route compares the field's target with the page's own object only to admit the entry or answer the 400, and reads no record of the target. The addressed record is getData behind the route's get exposure gate, and the related rows are the child's findData behind the child's list exposure, as read at e90f4f4b.
  • No source, changeset or exported surface moves.

② Semver level

Unchanged: '@objectstack/rest': patch. A test ledger row ships nothing.

③ Boundary flags

  • None new. The dev ran core test:repo (6 files, 94 passed) and rest test:repo (5 files, 288 passed) locally.

Implemented-by: claude/project-thread-nzsli6
Reviewed-by: session_01WGufzibcN9oHa8kDbTTnL5

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5

席位核对

Verdict: ACCEPT at head 3ca0ddde4314f0bbda3df23c6d92f14b70cb7891 · 2026-11-03T18:56Z

  • Supersedes ACCEPT 6612077816 at e90f4f4b. The head moved by one dev push: a second-object-read-exposure ledger row for the new referenceTargetOf caller, which fixed the Test Core 4/8 red. Delta review PASS 6612763022 at this head; the rest of 6612077816 carries over.
  • Landing: p1 release-blocker. On all-green at this head, the seat sends pr_ready + automerge_enable through the relay.

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

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

1 participant