Skip to content

Metadata API: no batch by-names read, so the Console roster gate costs one /meta/object request per name #29236

Open

Description

@objectstack-fleet

domain:ui#1 · zhuangjianguo · session_01QsR2jUJ19MstFvFhjgiYTu

Ruled: 6580261740 · letter A · 2026-11-02T14:56Z

Filing gate: ② out-of-scope finding, class a, from #29121 (PR #29229; report card comment 6574295250) · filed by domain:ui#1 · ⛔ Not a claim. No grade: triage sets it.

Business reason. After #29121, Console Home stops downloading the whole object registry. Instead it reads each object the app roster requires by name, one request per name: 8 requests on app-crm on every Home visit outside an app. A larger app means more round trips, and each one costs a full latency on a phone link. A single batch read would make it one request.

Measured (the #29121 dev, on the production Console with app-crm, at PR #29229 head d8b43ffa). The branch sends 28 requests in total, with no unscoped object or view list. 11 of them are by-name reads, 70 KB in all, and 8 of those are the roster gate's requiresObject union.

Wanted. The metadata list endpoint for a type (/meta/object, and generically /meta/:type) accepts a names filter, for example ?names=a,b,c, and answers only those items. The UI can then fold the roster gate's reads into one request. If this adds a query parameter to the published API, the change is a spec and API matter, so the reader decides the protocol declaration.

Reader. Server metadata API (domain:server); the Console adopts it as a follow-up in domain:ui.

Links back: #29121, PR #29229.

Activity

  1. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: p2 · pm:queue · 2026-11-02T11:07Z

    ⛔ Not a claim.


    Generated by Claude Code

  2. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1

    Claim: PM loop round 8 · card thread · 2026-11-02T11:40Z
    Session: session_01ADHkmQKwKFCp1m6Pb6Q8c1 (the seat); the card thread is named in a follow-up claim once it reports its branch
    Account: os-steve (the seat's linked user; the card's assignee)
    Branch: pending — the card thread's harness-pinned branch, written into a follow-up claim before the dev starts
    Worktree: the card thread's own container checkout
    Domain: domain:server
    Seat: domain:server#5
    Ruling-ref: no maintainer ruling on this card; triage grade p2 (6575623583: first check whether the existing list filter already serves a by-names read; a new query key is a protocol addition whose name goes to the maintainer before any code).
    File surface: measurement of the /api/v1/meta/:type list door (packages/rest, packages/metadata-protocol); at most one test pinning an existing filter spelling. ⛔ No new query key or route, no packages/spec/**: a new key stops with needs_decision.
    Container & model: S, mode:thread, model: the thread's default judgement tier, effort: medium (p2)
    Clause-②: no
    Metadata-protocol: none
    Responsibility: the code is the metadata list door · the Console roster gate reads each required object by name, one request per name, because no batch by-names read is known · reached by every Console Home visit outside an app
    Thread-read: 6575623583
    Premise: holds as filed (PR #29046, merged, refuses bracketed ?id[]= on GET /meta/app; whether another spelling selects by names is what the dev measures on origin/main 31826d3318)
    Serial constraints cleared: measurement only; a pin-only PR touches no source.
    Resume-from: none — fresh from origin/main

    State: pm:queue → pm:dispatched, assigned to os-steve, recorded by this claim.

  3. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1

    Claim: PM loop round 8 · card thread · branch named · 2026-11-02T11:45Z (supersedes 6576235563, which had the branch pending)
    Session: session_01ADHkmQKwKFCp1m6Pb6Q8c1 (the seat); the card thread is session_01ADHkmNqzH9zZjokhM7K3u3
    Account: os-steve (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-ddr31t (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
    Worktree: the card thread's own container checkout
    Domain: domain:server
    Seat: domain:server#5
    Ruling-ref: no maintainer ruling on this card; triage grade p2 (6575623583: first check whether the existing list filter already serves a by-names read; a new query key is a protocol addition whose name goes to the maintainer before any code).
    File surface: measurement of the /api/v1/meta/:type list door (packages/rest, packages/metadata-protocol); at most one test pinning an existing filter spelling. ⛔ No new query key or route, no packages/spec/**: a new key stops with needs_decision.
    Container & model: S, mode:thread, model: the thread's default judgement tier, effort: medium (p2)
    Clause-②: no
    Metadata-protocol: none
    Responsibility: the code is the metadata list door · the Console roster gate reads each required object by name, one request per name, because no batch by-names read is known · reached by every Console Home visit outside an app
    Thread-read: 6576235563
    Premise: holds as filed (PR #29046, merged, refuses bracketed ?id[]= on GET /meta/app; whether another spelling selects by names is what the dev measures on origin/main 31826d3318)
    Serial constraints cleared: measurement only; a pin-only PR touches no source.
    Resume-from: none — fresh from origin/main

    State: pm:queue → pm:dispatched, assigned to os-steve, recorded by this claim.

  4. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#5 · os-steve · session_01ADHkmNqzH9zZjokhM7K3u3
    os-dev-report

    {
      "issue": 29236,
      "status": "needs_decision",
      "branch": "claude/project-thread-ddr31t",
      "pr": null,
      "session": "session_01ADHkmNqzH9zZjokhM7K3u3",
      "premise_still_valid": true,
      "summary": "Measured on origin/main a19759ac (code) and a showcase boot of that tree: no existing spelling on the GET /meta/:type list door selects items by name for object or any type but app. The list door reads exactly five keys (META_QUERY_ARITY_DOORS.list: package, preview, object, include, id); id narrows only metaType app, to exactly one name; object narrows only view (by bound object, not by item name). Every other key is silently ignored (the route declares no closed set), so ?names= / ?name= / ?ids= / ?filter= / ?id[]= on /meta/object all answer the full 115-item, 746 KB list with 200; repeated ?id= is a 400 arity refusal on every type. The store read (getMetaItems -> readFlattenedMetaItems) loads the whole type list; GetMetaItemsRequestSchema has no names member. A batch by-names read therefore needs a new query key: stopping with needs_decision, no code, no PR.",
      "tests": "Showcase boot (examples/app-showcase `objectstack dev --seed-admin --fresh -p 38917`, signed in as the seeded admin), each GET /api/v1/meta/...: object -> 200 746514B n=115; object?id=showcase_account -> same 746514B n=115; object?name=, ?names=showcase_account,showcase_contact, ?ids=, ?id[]=, ?filter= -> each 200 746514B n=115; object?id=a&id=b -> 400 VALIDATION_ERROR (id supplied 2 times); view -> 200 24009B n=36, view?names=x and view?id=x -> same n=36; app -> n=3; app?id=showcase_app -> 200 7368B n=1; app?id=a&id=b -> 400; app?id[]=showcase -> 400 bracketed-id refusal; app?names=showcase -> n=3 (ignored). By-name reads for comparison: object/showcase_account 10174B, object/showcase_contact 5875B. App bundle /meta/app/showcase_app/bundle 301937B. Build: turbo build --filter=@objectstack/example-showcase... under os-verify-lock, VERDICT command-exit 0 (132s). Server killed by its recorded PID, port confirmed free.",
      "gates": "none: no diff, no PR (dispatch-gates not run: empty change set).",
      "dogfood_battery": "n/a: no PR",
      "mcp_calls": "2: mcp__github__issue_read get, mcp__github__issue_read get_comments (reads only)",
      "api_writes": "1: this os-dev-report comment on #29236 via post-stamped.mjs",
      "files_changed": [],
      "line_budget": "n/a: no diff",
      "deviations": [
        "Booted the showcase through its own package dev script (cd examples/app-showcase; pnpm dev --fresh -p 38917) instead of root `pnpm dev`: root dev's check-dev-prereqs refuses until all 106 workspace packages (35 ui/ packages) have dist; the showcase closure itself was built by turbo first. Measurement only, no code ran from an unbuilt package."
      ],
      "open_questions": [
        {
          "question": "Which spelling serves a batch by-names list read on GET /meta/:type (protocol addition, maintainer names it)?",
          "options": [
            "A ?names=a,b,c on every /meta/:type list: one comma-separated value (fits the existing arity rule that refuses a repeated key; machine names are snake_case so a comma never occurs in a name). Narrowing step in createMetaListAnswer after the per-caller gate and before the object mask, so masking and translation run only on the named items; empty value refused 400 like ?id=; unknown names simply absent (no 404). Pushed down to the store read as GetMetaItemsRequest.names so the protocol filters before decorating. Files: packages/spec/src/api/protocol.zod.ts (GetMetaItemsRequestSchema + gen:schema/gen:docs/api-surface artifacts), packages/rest/src/meta-item-read-gate.ts (META_QUERY_ARITY_DOORS.list, the chain step, the empty refusal), packages/rest/src/meta-collection-routes.ts (listRequest), packages/runtime/src/domains/meta.ts (dispatcher twin pass-through; meta-list-projection-parity.test.ts census), packages/metadata-protocol/src/protocol.ts (readFlattenedMetaItems filter), packages/client/src/index.ts (meta.getItems options). Size ~150-250 src lines + tests; minor changeset on spec/rest/metadata-protocol/client; Metadata-protocol: none (API, not authorable metadata) but Clause-2 wording to be set by the seat.",
          "B Same key, chain-only: narrow in createMetaListAnswer (rest + runtime parity), no spec/store change. Smaller (~60 src lines + tests) and fixes the wire cost (746 KB -> only the named items), but the server still reads the full type list per request.",
          "C Generalise the existing ?id= to every type and accept a comma list: reuses a known key but changes its meaning on app (today exactly one name) and collides with the bracketed-id refusal PR #29046 just shipped.",
          "D Add no key: the Console keeps one by-name read per required object (8 on app-crm)."
          ],
          "recommendation": "A, because it fixes both the round trips and the server-side full-list read, keeps one spelling for every type, fits the arity and empty-value rules the door already has, and leaves ?id='s just-shipped one-app meaning alone. If the maintainer wants the smallest step first, B now and the store push-down later is a clean split."
        }
      ],
      "security_findings": "none",
      "out_of_scope_findings": [
        "carrier: the names decision on this card · noted, not filed: GET /meta/:type declares no closed query-parameter set (AGENTS.md Route & surface ownership 5; existing routes convert per lane), so a misspelt filter (?names= today, or ?id= on any type but app) answers the full list with 200, indistinguishable from a real answer. Adding the new key is the natural moment to declare the list's closed set (refuseUnknownQueryParams with package, preview, object, include, id, the new key)."
      ]
    }

    Stamped 2026-11-02T11:53Z.


    Generated by Claude Code

  5. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1

    Needs decision · #29236 · 2026-11-02T14:37Z

    The dev measured first and stopped with needs_decision (report 6576480506, at 2026-11-02T11:53Z). No existing spelling on GET /meta/:type selects list items by name, except ?id= on app, which takes exactly one name. A batch by-names read therefore needs a new public query key. Naming a public API key is the maintainer's call. The card thread is parked on claude/project-thread-ddr31t with no diff and no PR.

    The question (maintainer):

    • A (seat recommends): add ?names=a,b,c on every /meta/:type list, as one comma-separated value. Narrow after the per-caller gate and push the filter down to the store read (GetMetaItemsRequest.names), so masking and translation run only on the named items. An empty value is refused with 400, and an unknown name is simply absent. This adds to the protocol: a spec, rest, metadata-protocol and client minor, about 150–250 source lines plus tests. The spec part goes to the spec lane first.
    • B: the same key, narrowed in the rest chain only, with no spec or store change (about 60 lines). It fixes the wire cost, but the server still reads the full list.
    • C: widen ?id= to every type and accept a list. This changes ?id='s meaning on app and collides with the bracketed-id refusal fix(rest)!: GET /meta/app refuses a bracketed id key (?id[]=, ?id[0]=) instead of answering every app #29046 just shipped.
    • D: add no key. The Console keeps one request per name (8 on app-crm).

    Why A: one spelling for every type. It fits the door's existing arity and empty-value rules, fixes both the round trips and the server's full-list read, and leaves ?id= alone. Adding the key is also the natural moment to declare the list door's closed set of query parameters (the dev's out-of-scope note).

    State: pm:dispatched → needs-user-decision. The card thread stays parked until the ruling.

  6. zhuangjianguo commented on Nov 2, 2026

    @zhuangjianguo
    Collaborator

    Brief-claimed-by: session_01QsR2jV5sBSzCYNEDgpYLJw · batch #458 · 2026-11-02T14:50Z


    Generated by Claude Code

  7. zhuangjianguo commented on Nov 2, 2026

    @zhuangjianguo
    Collaborator

    Ruling: batch #458 item 2 · letter A · maintainer 「同意」 2026-11-02T14:54Z

    director#1 · zhuangjianguo · session_01QsR2jV5sBSzCYNEDgpYLJw

    What the ruling binds

    • A — ?names=a,b,c on every /meta/:type list. One comma-separated value (the data routes' convention for fields, searchFields, orderby; it fits the door's arity rule that refuses a repeated key). The narrowing step runs after the per-caller gate and before the object mask, so masking and translation run only on the named items. An empty value is refused 400 like ?id=; an unknown name is simply absent (no 404). The filter is pushed down to the store read as GetMetaItemsRequest.names, so the protocol filters before decorating.
    • The same server PR declares the list door's closed set of query parameters (package, preview, object, include, id, names) with the refuseUnknownQueryParams pattern the sibling meta routes already use, so a misspelt filter answers 400 instead of the full list with 200. That is a narrowing; the seat states it on the changeset (Clause-②).
    • Not B (the server would still read the full type list), not C (?id= keeps its just-shipped one-app meaning, fix(rest)!: GET /meta/app refuses a bracketed id key (?id[]=, ?id[0]=) instead of answering every app #29046), not D.

    Workload of the letter

    • domain:spec one PR first: GetMetaItemsRequestSchema.names plus the gen:schema / gen:docs / api-surface artifacts (spec minor).
    • domain:server one PR Blocked-by it, on the parked card thread (claude/project-thread-ddr31t): packages/rest (META_QUERY_ARITY_DOORS.list, the chain step, the empty refusal, the closed set), packages/runtime/src/domains/meta.ts (dispatcher twin + parity census), packages/metadata-protocol (readFlattenedMetaItems filter), packages/client (meta.getItems options); ~150–250 source lines plus tests; rest / metadata-protocol / client minor. Metadata-protocol: none (an API key, not authorable metadata).
    • domain:ui follow-up card: the Home roster gate folds its by-name reads into one request.

    Execution

    1. Server seat 5 files the spec card and re-dispatches the parked thread once it lands.
    2. Label strokes are the lane seat's.

    Confidence gap carried from the brief: names follows the data routes' plural comma-list spelling; no external SDK user was consulted.


    Generated by Claude Code

  8. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1

    Ruling A (6580261740) received, and executed in order:

    1. Spec half: spec(api): add names to GetMetaItemsRequestSchema — a batch by-names read on every /meta/:type list (#29236 ruling A) #29441 (GetMetaItemsRequestSchema.names, spec minor), filed into the spec lane queue.
    2. Server half: this card, on the parked thread claude/project-thread-ddr31t. It is Blocked-by spec(api): add names to GetMetaItemsRequestSchema — a batch by-names read on every /meta/:type list (#29236 ruling A) #29441 and restarts once spec(api): add names to GetMetaItemsRequestSchema — a batch by-names read on every /meta/:type list (#29236 ruling A) #29441 merges. Its scope is the ruling's: the rest chain step after the per-caller gate, the empty-value 400, the list door's closed query set (a narrowing, declared on the changeset), the dispatcher twin and parity census, the store push-down, and meta.getItems in the client.
    3. ui follow-up: filed as its own card (below), Blocked-by this card.

    State: needs-user-decision → pm:dispatched.

  9. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1

    Release: session_01ADHkmQKwKFCp1m6Pb6Q8c1 · seat 5 signs off at the maintainer's wrap-up · to pm:blocked for the next domain:server seat
    Resume-from: none (the card thread pushed no branch; it stopped at measurement)
    Blocked-by: #29441

    The dev's needs_decision (6576480506) was ruled: director ruling A (6580261740), which the seat executed in 6580443235. The spec half is #29441 (GetMetaItemsRequestSchema.names, spec lane, still open). This card is the server half and waits on that merge, so it goes to pm:blocked, not to the decision box: there is no open question.

    Next: when #29441 merges, move this card to pm:queue. The next seat dispatches it under ruling A, serving ?names= on every /meta/:type list.

  10. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#2 · os-support-ai · session_01WGufzhJG3tKnksHVa1krBz

    Spec half of ruling A (6580261740): PR #29722 for #29441, contract review PASS (6594380969 on 064d4e01, delta on bb07931c), landing now. Two things the server PR on this card inherits; the next domain:server seat's claim should name both.

    1. Drop the marker in the serving change. GetMetaItemsRequestSchema.names ships with a describe that opens [PLANNED — not yet served] and ends "No list door reads it yet: until one does, a request carrying it is answered as if it were absent." A pin in packages/spec/src/api/protocol.test.ts asserts that prefix, so the PR that serves ?names= must, in the same change, remove the prefix and the trailing sentence, delete that one it(...), and run gen:docs (moves content/docs/references/api/protocol.mdx). That is one packages/spec edit outside the rest / runtime / metadata-protocol / client workload the ruling lists.
    2. Declaration. The spec PR declares Metadata-protocol: added (the authorable-surface/api.json shard moved by one key; check-changeset-no-major refuses none there, precedents feat(spec): the package export manifest declares withheldMaps, classified door for the unmanaged import #29521 and feat(spec): declare the whole-deployment export, every installed package as installable packages in one read (PackageExportBundleSchema) #29100). The server PR's own declaration is judged on its own diff; it is not inherited as none from the card body.

    Card stays pm:blocked until #29722 merges; nothing here changes the ruling.

  11. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#2 · os-support-ai · session_01WGufzhJG3tKnksHVa1krBz

    Blocker landed. #29441 closed: PR #29722 merged (6f094645), so GetMetaItemsRequestSchema.names is on main with the [PLANNED — not yet served] marker. This card's Blocked-by: #29441 (6591626191) is met; the pm:blocked → pm:queue flip and the pm:blocking sweep on #29441 are the triage sweep's. The serving PR carries the two items in 6594515725.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:serverDomain lane — seat card indexed by label:pm:seatenhancementNew feature or requestpm:blockedpriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions