Skip to content

fix(rest): the app metadata bundle serves its objects in the object list's order - #29752

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

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-t365pj

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Warren · project thread

Fixes #29680

Clause-②: no
Metadata-protocol: none

Claim: 6593195648 (seat domain:server#1). Triage designation 6593258467: one cross-domain PR in domain:server covering the rest fix and the two app-bundle.zod.ts order sentences. This unblocks #29552 leg 1, whose PR waits on this one (Blocked-by).

维护者速读

  • 改了什么:应用元数据包(GET /meta/app/{appId}/bundle)里的 objects 改按对象列表的顺序返回,不再是"导航先、反向关联后"。集合不变,只是顺序。spec 里该响应的文档注释和 objects 描述同步改成对象列表顺序,参考页按生成器重新生成。
  • 为什么改:Console: a console-created (package-less) app's record route reads the whole /meta/page list, and its root the whole object registry #29552 第一步让控制台的记录页从包里取对象范围。记录页的相关列表按拿到的对象顺序排,一个导航对象又引用另一个导航对象时(联系人→客户),页签顺序和从对象列表取时不同。
  • 风险与回滚:导航顺序与对象列表顺序不一致的应用,包里 objects 的顺序会变;今天没有读者依赖这个顺序(界面按名字建索引)。导航顺序本就与列表一致的包逐字节不变。回滚即 revert。
  • 先看:packages/rest/src/meta-app-bundle-routes.ts 的 inObjectListOrder 和调用处;packages/spec/src/api/app-bundle.zod.ts 的两句契约文字;meta-app-bundle-routes.test.ts 新增的 rest(app bundle): serve the bundle's objects in the object list's order, not navigation-reach first #29680 用例。
  • 两分钟验证:pnpm --filter @objectstack/rest exec vitest run src/meta-app-bundle-routes.test.ts,预期 1 个文件、19 个用例全部通过。
  • 需要你做的:无,席位落地(spec 路径,先过契约复审)。

What changes

  • packages/rest/src/meta-app-bundle-routes.ts. The bundle built its objects as members('object', [...reach.objects, ...inbound]). inboundRelationObjects skips the reach targets, so an inbound object that is also a navigation object (contact referencing account) kept its navigation place. The route now serves the same names through a new exported inObjectListOrder(list, names): every name the object list holds, at its first position in that list (the list objectList() already reads), then the names the list does not hold, in their own (navigation) order. The set is unchanged, so views and pages, which derive from the served set, do not move. Nothing else in the bundle changes.
  • packages/rest/src/rest-route-ledger.ts. The route's ledger note now reads "objects: navigation plus one hop of inbound relationships, in object-list order".
  • packages/spec/src/api/app-bundle.zod.ts. Docs only, no key and no shape change. The docblock's objects bullet said "in navigation order; then ... in registry order"; it now says the navigation objects and their inbound relations come "all in the object list's order; a name the object list does not hold follows the listed ones, in navigation order". The objects .describe() says "in the object list's order". content/docs/references/api/app-bundle.mdx was regenerated with gen:schema + gen:docs; no other tracked artefact moved.
  • Changeset .changeset/29680-app-bundle-object-list-order.md: @objectstack/rest patch, @objectstack/spec patch (the docblock ships in dist).

Readers of the bundle's objects array (checked before editing): ui/packages/data-objectstack/src/app-bundle.ts indexAppBundle indexes every member by type and name into a Map; packages/client meta.getAppBundle passes the body through. No reader relies on reach-first order. Navigation reads the app's own entries, not this array.

Tests

packages/rest/src/meta-app-bundle-routes.test.ts:

  • The three reach-first pins on the shared CRM fixture (admin bundle, undetermined-visibility bundle, enableCache: false bundle) now expect account, contact, secret_deal, note, task, deal_line. That fixture is the card's case: contact is a navigation object that references account.
  • inObjectListOrder unit case, including names the list does not hold (kept after, in their own order).
  • An app whose navigation names contact then account (one references the other): objects come back account, contact, note, task; views and pages are the same set as before.
  • CONTROL: an app whose navigation already follows the list (account only): the served objects are byte-equal to the reach-first derivation's item reads (account, contact, note).
Ablation: reach-first again, once, restored

Through scripts/ablation-replace.mjs (wrap mode, restore trap) under os-verify-lock, on commit 8c59a53. The test imports the route from src (relative import), so no build leg applies.

ablation-replace: anchor   "members('object', inObjectListOrder(listed, [...reach.objects, ...inbound]))"  x1 (before)
ablation-replace: replace  "members('object', [...reach.objects, ...inbound])"  x0 -> x1
ablation-replace: ok mutation landed: anchor 1 -> 0, blob 76947cadad64 -> ca6f86bfc9db
     × admin: every reached member, objects in the object list's order (#29680), the body parsing against the declared shape
     × two navigation objects where one references the other come back in object-list order
     × a member served under an undetermined field visibility serves the bundle no-store with no validator
     × `metadata.enableCache: false` sends no validator and answers a matching If-None-Match in full, as the item reads do
AssertionError: expected [ 'account', 'secret_deal', …(4) ] to deeply equal [ 'account', 'contact', …(4) ]
      Tests  4 failed | 15 passed (19)
ablation-replace: ok restored: blob == HEAD (76947cadad64) and `git diff HEAD` is empty

The CONTROL stayed green under the mutation: reach-first and list order agree on it.

Acceptance notes

  • The order sentence in the spec contract is the only contract text that pinned reach-first; it changed in this PR with triage's designation, so the server and its declared contract agree.
  • check:dev-prereqs (local-only) exits 1 on this container because 35 workspace packages (mostly ui/**) are not built; packages/spec/dist was rebuilt from this head before the gates.

Verification

All on head 03d2096 (after merging origin/main 0eb97fa), under os-verify-lock where heavy:

Local gates
node scripts/pm/dispatch-gates.mjs --ran FILE --repo objectstack-ai/objectstack
Run reconciliation — 103 derived, 103 run, 0 NOT-MEASURED, 0 UNRUN.
✓ dispatch-gates --ran: 103 derived famil(ies) accounted for — 103 run, 0 NOT-MEASURED
pnpm lint :: exit 0
pnpm --filter @objectstack/rest typecheck :: exit 0
pnpm --filter @objectstack/rest test :: exit 0      Test Files 381 passed (381) · Tests 7133 passed | 332 skipped
pnpm --filter @objectstack/spec typecheck :: exit 0
pnpm --filter @objectstack/spec test :: exit 0      Test Files 803 passed (803) · Tests 23792 passed | 1 todo
pnpm check:dev-prereqs :: exit 1                    workspace not fully built (local state, see Acceptance notes)

An earlier gate run on 8c59a53 caught an apostrophe that broke the single-quoted ledger note (check-comment-mask-corpus, check:doc-authoring and two siblings refused to parse the file); commit 092ed98 fixed it.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind (inObjectListOrder).
  • 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 maintainer brief is Chinese by the dispatch contract).
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JQwxbWHJEZNQyk4aybUZm1


Generated by Claude Code

The bundle built its objects as navigation reach, then inbound relations,
so a navigation object that references another navigation object sat ahead
of its place in the object list. Serve the same set in the object list's
order; names the list does not hold keep their navigation order after it.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JQwxbWHJEZNQyk4aybUZm1
…s order

The AppMetadataBundleResponseSchema docblock and its `objects` description
said navigation order, then registry order. State the object list's order
the route now serves, with unlisted names after in navigation order.
Regenerated the API reference page; no shape change.

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

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/rest, @objectstack/spec, touching 13 documentable anchor(s).

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

  • content/docs/api/client-sdk.mdx (via getItem (sdk, the bare tail of client method meta.getItem, bound to GET /api/v1/meta/:type/:name; the bare tail of client method meta.getItem, bound to GET /meta/:type/:name), meta.getItem (sdk, the route ledger binds it to GET /api/v1/meta/:type/:name, selected by route anchor /meta/:type/:name; the route ledger binds it to GET /meta/:type/:name))
  • content/docs/api/wire-format.mdx (via /meta/object/{name} (route, a path literal in AppMetadataBundleResponseSchema))
  • content/docs/concepts/metadata-lifecycle.mdx (via /meta/object/{name} (route, a path literal in AppMetadataBundleResponseSchema))
  • content/docs/data-modeling/formulas.mdx (via /meta/object/{name} (route, a path literal in AppMetadataBundleResponseSchema))
  • content/docs/deployment/production-readiness.mdx (via /meta/object/{name} (route, a path literal in AppMetadataBundleResponseSchema))
  • content/docs/frontend/data-objectstack.mdx (via getItem (sdk, the bare tail of client method meta.getItem, bound to GET /api/v1/meta/:type/:name; the bare tail of client method meta.getItem, bound to GET /meta/:type/:name), meta.getItem (sdk, the route ledger binds it to GET /api/v1/meta/:type/:name, selected by route anchor /meta/:type/:name; the route ledger binds it to GET /meta/:type/:name), /app/:appId/bundle (route, bridged from symbol inObjectListOrder — its route source's handler names it))
  • content/docs/frontend/troubleshooting.mdx (via getItem (sdk, the bare tail of client method meta.getItem, bound to GET /api/v1/meta/:type/:name; the bare tail of client method meta.getItem, bound to GET /meta/:type/:name))
  • content/docs/protocol/kernel/http-protocol.mdx (via /meta/object/{name} (route, a path literal in AppMetadataBundleResponseSchema))
  • content/docs/protocol/kernel/metadata-service.mdx (via meta.saveItem (sdk, the route ledger binds it to PUT /api/v1/meta/:type/:name, selected by route anchor /meta/:type/:name; the route ledger binds it to PUT /meta/:type/:name), saveItem (sdk, the bare tail of client method meta.saveItem, bound to PUT /api/v1/meta/:type/:name; the bare tail of client method meta.saveItem, bound to PUT /meta/:type/:name))
  • content/docs/protocol/objectql/state-machine.mdx (via /meta/object/{name} (route, a path literal in AppMetadataBundleResponseSchema))
  • content/docs/ui/doc-pages.mdx (via getItem (sdk, the bare tail of client method meta.getItem, bound to GET /api/v1/meta/:type/:name; the bare tail of client method meta.getItem, bound to GET /meta/:type/:name))
  • content/docs/ui/forms.mdx (via /meta/object/{name} (route, a path literal in AppMetadataBundleResponseSchema))

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

  • content/docs/releases/v17/17-0.mdx (via /meta/object/{name} (route, a path literal in AppMetadataBundleResponseSchema))
  • content/docs/releases/v17/17-1.mdx (via /meta/object/{name} (route, a path literal in AppMetadataBundleResponseSchema))
  • content/docs/releases/v17/17-3.mdx (via deleteItem (sdk, the bare tail of client method meta.deleteItem, bound to DELETE /api/v1/meta/:type/:name), meta.deleteItem (sdk, the route ledger binds it to DELETE /api/v1/meta/:type/:name, selected by route anchor /meta/:type/:name), meta.saveItem (sdk, the route ledger binds it to PUT /api/v1/meta/:type/:name, selected by route anchor /meta/:type/:name; the route ledger binds it to PUT /meta/:type/:name), saveItem (sdk, the bare tail of client method meta.saveItem, bound to PUT /api/v1/meta/:type/:name; the bare tail of client method meta.saveItem, bound to PUT /meta/:type/:name))
  • content/docs/releases/v17/17-7.mdx (via /meta/object/{name} (route, a path literal in AppMetadataBundleResponseSchema))

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: /meta/:type/:name (route, 40 pages)
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 61 of 218 client-bound route-ledger rows — the other 157 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 157: 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; 102 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 — 171 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 c76c0e042ea1efc9ccb7b209a82dd8f0fc119953 → packageMentionDocs.

Which tree this was computed on

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

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

⚠️ 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 c76c0e042ea1efc9ccb7b209a82dd8f0fc119953 → 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:server#1 · os-warren · session_01JQwxbXsK9j25sd7eVwW96G

Contract-review-claim: session_01JQwxbXsK9j25sd7eVwW96G · head 03d2096 · 2026-11-03T03:22Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-warren · session_01JQwxbXsK9j25sd7eVwW96G

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 03d20969e341ad9566aae4c738fefc6abee8b365
Local-runs: none
Scope: packages/spec/src/api/app-bundle.zod.ts + packages/rest/src/meta-app-bundle-routes.ts (the route the docblock describes: inObjectListOrder, inboundRelationObjects, appNavigationReach, addOnce, the objectList() source), packages/rest/src/rest-server.ts metaListSource and packages/metadata-protocol/src/protocol-meta-list-reads.ts (which list "the object list" is), packages/rest/src/meta-collection-routes.ts and packages/rest/src/meta-item-read-gate.ts createMetaListAnswer (whether GET /meta/object reorders), content/docs/references/api/app-bundle.mdx (regenerated from the hits file), ui/packages/data-objectstack/src/app-bundle.ts and packages/client/src/index.ts meta.getAppBundle (the bundle's readers), packages/spec/src/api/app-bundle.test.ts (pins nothing on the sentences). Remainder of the diff (rest route and tests, route-ledger note, changeset) is the lane seat's ACCEPT at TIER_DEFAULT.

① Derived judgments

  • Accept set of AppMetadataBundleResponseSchema: unchanged — right. The hits file moves only the module docblock's objects bullet and the objects .describe() string. Keys, z.array(GetMetaItemResponseSchema), .strict(), the two exported types and lazySchema wrapping are byte-identical to main. packages/spec/src/api/app-bundle.test.ts is untouched and still passes on the shape alone. No exports map change; no generated JSON schema on main carried the old sentence (searched the whole tree: only the zod file and the mdx did).
  • Public surface of GET /meta/app/{appId}/bundle: the ORDER of objects changes; the set does not — right, and declared. At head the route serves members('object', inObjectListOrder(listed, [...reach.objects, ...inbound])) where listed is the same objectList() read that inboundRelationObjects already consumed. inObjectListOrder keeps every name once (addOnce), so the served set, and the views/pages derived from served, cannot move. The bytes change for every bundle whose reach-first derivation differs from list order; the spec now states the new order, so route and contract agree.
  • Which list "the object list" is — right. The route reads metaListSource(p, env).listMetaItems('object'), which is p.getMetaItemsForExecution; GET /meta/object serves p.getMetaItems. Both bodies (protocol-meta-list-reads.ts) call readFlattenedMetaItems(canonicalizeMetaRequestType(request), audience) with the same request, differing only in serving decoration and redaction, so the enumeration order is one. createMetaListAnswer reorders only view lists under ?object= (step 4); the object list is filtered per caller, never resorted. So the console's registry order (its GET /meta/object) and the bundle's order agree, which is what card rest(app bundle): serve the bundle's objects in the object list's order, not navigation-reach first #29680 asked for.
  • New export inObjectListOrder from packages/rest/src/meta-app-bundle-routes.ts: module-internal — not a published surface. packages/rest/src/index.ts does not re-export the module (same posture as appNavigationReach and inboundRelationObjects).
  • Readers of objects order — the PR's claim checked and right. ui/packages/data-objectstack/src/app-bundle.ts indexes members into Map by type then name; packages/client meta.getAppBundle passes the body through. Nothing read in the tree depends on reach-first position.
  • Prose nit for the lane seat (changeset and PR body, not a hits file): "a bundle whose navigation already follows the object list is served byte for byte as before" is slightly over-broad. The exact unchanged class is "a bundle whose reach-first derivation already equals list order"; a navigation in list order whose inbound object sits EARLIER in the list than a navigation object still moves. The CONTROL test is a member of the exact class, so the test is right; only the sentence is loose.

② Semver level

  • .changeset/29680-app-bundle-object-list-order.md: @objectstack/rest patch, @objectstack/spec patch. Matches the diff. rest is a behaviour fix on a served route with no API shape change (patch per AGENTS.md Post-Task 3); spec ships only docblock and .describe() text, which land in dist and in the generated reference, so a patch is owed and nothing above patch is.
  • Clause-②: no — right (the PR body and the changeset both carry it, line-initial, no arm). The schema accepts exactly what it accepted on main; nothing widens or narrows. Metadata-protocol: none — right: no key an author writes is added, removed or re-meant.
  • Gate verdicts on head 03d20969: 62 check-runs at the third read — 24 success, 8 skipped, 30 still in_progress, 0 failures. Completed green include Check Changeset, Governed Surface Queue Guard, PR-body guards, Lint · PM dispatch-gates, Lint · ESLint, Lint · docs & code guards, Build Docs, Spec property liveness, Check Documentation Links, Flag docs affected by code changes. Still running: Build Core, Test Core (1-8/8), Test UI (1-16/16), Type Check · workspace, Type Check · consumer gates, Type Check UI, Build & E2E UI, Live E2E UI. This record does not wait on them: the lane seat's ACCEPT and any auto-merge arm require the full set green (contract-review.md ②), and a red among them re-opens this head.

③ Boundary flags

  • open_questions: [] in the os-dev-report — nothing to answer.
  • Deviation 1 (the order's design-report path not on this project's mount; the card body's quoted measurement used instead): acceptable — the card body carries the measured before/after and the cause line, and this review reads the same.
  • Deviation 2 (file surface widened to app-bundle.zod.ts, the regenerated mdx, rest-route-ledger.ts, a @objectstack/spec changeset): authorized by triage designation 6593258467 and recorded in claim amendment 6594006607; the PR's file list holds exactly those paths and nothing beyond.
  • Deviation 3 (PR body template fold): prose, no contract effect.
  • check:dev-prereqs exit 1 locally (unbuilt ui/** workspace): local state only; the derived families it would have covered are the in-progress CI runs named in ②.
  • Landing flag for the lane seat: content/docs/references/api/app-bundle.mdx is a generated file, so landing-operations.md §D serial relay applies at landing, as triage and the claim amendment already noted. The PR is still a draft.
  • PR author is objectstack-fleet[bot]; no outside-author note is owed.
  • Escalations: none.

④ Design judgment

packages/spec/src/api/app-bundle.zod.ts judged against the ruling it implements (triage 6593258467: two order sentences stating the object-list order the fix serves, docs only) and against its first consumer, the route at head.

  • The docblock bullet against the route — holds, clause by clause. (a) "every object an object navigation entry names (top-level navigation, each area's navigation, and children at any depth)" = appNavigationReach walking a.navigation, each areas[].navigation, and e.children recursively, taking type === 'object' entries' objectName. (b) "every object with a lookup or master_detail field whose reference is one of those" = inboundRelationObjects over RELATIONSHIP_TYPES = {lookup, master_detail} with wanted.has(field.reference), against reach.objects only. (c) "all in the object list's order" = inObjectListOrder's first loop over listed, which holds both the navigation names and the inbound names. (d) "a name the object list does not hold follows the listed ones, in navigation order" = the second loop over names = [...reach.objects, ...inbound]; every inbound name came from listed, so the only names reaching the tail are navigation names absent from the list, and they come out in reach.objects order. The clause is reachable (the by-name read resolves a legacy short name the list does not carry), so it is not dead text. (e) "One hop" = inbound judged against reach.objects, never against inbound. (f) The retained sentence "A name appears once per list, at its first position" = addOnce in both loops and in appNavigationReach. Tried to break it with: a duplicate name in listed (first position wins, matches "at its first position"); a navigation name absent from the list (tail, navigation order); an inbound object that is also a navigation object (the card's case: placed by list position, contact no longer rides reach-first); an empty navigation (empty objects, the sentence degenerate but true). No gap found.
  • The .describe() against the route — holds. "The objects the app's navigation reaches and the objects whose relationship fields reference them, in the object list's order" states the set and the order the route serves. It omits the tail clause, which the docblock carries; a summary string omitting the edge is acceptable since the module docblock is the full contract and the generated page shows both.
  • The regenerated mdx against the hits file — byte-exact. Extracted the .describe() string from the zod source (concatenated literal, escapes resolved) and compared it with the objects row of the generated table: identical. Normalized the docblock bullet and the mdx bullet: identical. The page still carries the AUTO-GENERATED banner. No other tracked artefact on main held the old sentence, so nothing else owed regeneration.
  • Against the scenario it is accepted against (card rest(app bundle): serve the bundle's objects in the object list's order, not navigation-reach first #29680 and Console: a console-created (package-less) app's record route reads the whole /meta/page list, and its root the whole object registry #29552 leg 1): the spec now promises the order the console's registry read serves, and the two orders are one enumeration (① third bullet). A console deriving related lists from the bundle's objects therefore sees the registry's order, which is the card's "Wanted".
  • should-fix (non-blocking): the bullet says "the object list" without naming which list. A spec reader has no route to tie it to; the tree ties it to the protocol's object list, the one GET /meta/object serves (per-caller filtered, never reordered). One parenthetical such as "(the order GET /meta/object lists them in)" in the docblock bullet, regenerated into the mdx, would close that. Docs only, no shape change, can ride a later docs PR.
  • must-fix: none.

Implemented-by: claude/project-thread-t365pj
Reviewed-by: session_01JQwxbXsK9j25sd7eVwW96G

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-warren · session_01JQwxbXsK9j25sd7eVwW96G

席位核对

ACCEPT · domain:server#1 · head 03d20969e341ad9566aae4c738fefc6abee8b365 · 2026-11-03T03:43Z · contract review PASS 6594718171 at CONTRACT_REVIEW_TIER (schema face: packages/spec/src/api/app-bundle.zod.ts, fable subagent, independent of the dev); the remainder (rest route, its tests, route-ledger note, changeset, regenerated mdx) read by the seat at TIER_DEFAULT

  1. Shape: draft, base main, Fixes #29680. 6 files, +109/-19: meta-app-bundle-routes.ts (new inObjectListOrder, which orders the navigation-plus-inbound set over the same objectList() read), its test file, a rest-route-ledger.ts note, the app-bundle.zod.ts docblock bullet plus objects .describe(), the regenerated app-bundle.mdx, and one changeset (@objectstack/rest + @objectstack/spec patch, Clause-②: no, Metadata-protocol: none). Cross-domain single PR as designated by triage 6593258467, with claim amendment 6594006607. Not governed, under 3000 lines.
  2. Premise measured: a navigation object that also points at another kept its navigation place, so the bundle's related lists ran in a different order from the object list (report 6594601135; ablation red). The review traced "the object list" to the same readFlattenedMetaItems enumeration as GET /meta/object, so the console's registry order and the bundle now agree.
  3. Gates on this head: check-runs pinned to 03d20969: 65 total, 57 success, 8 skipped, 0 failed. Dev local: dispatch-gates --ran 103/103 exit 0, pnpm lint 0, rest 7133 and spec 23792 tests pass.
  4. Accepted deviations: the spec half (docblock, .describe(), mdx) rides this PR by triage designation. Seat-read prose: the changeset says a bundle whose navigation already follows the object list is served "byte for byte as before". The exact class is "the reach-first order already equals the list order" (review nit), and the seat accepts the wording as a summary. The regenerated mdx matches the .describe() byte for byte (review ④).
  5. Out of scope: review should-fix, non-blocking: the docblock could name GET /meta/object as "the object list". It is an acceptance note with no card, because the sentence is already accurate. No other generated artefact or open PR touches app-bundle.mdx, so this is a normal landing, not a §D relay.
  6. Landing: not governed and under 3000 lines ⇒ the seat lands it (pr_ready + automerge_enable). At MERGED: landing record on rest(app bundle): serve the bundle's objects in the object list's order, not navigation-reach first #29680 (closed by Fixes), pm:dispatched removed; Console: a console-created (package-less) app's record route reads the whole /meta/page list, and its root the whole object registry #29552 leg 1 is unblocked.

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 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.

rest(app bundle): serve the bundle's objects in the object list's order, not navigation-reach first

1 participant