Skip to content

feat(rest): one request loads an app's metadata — GET /meta/app/{appId}/bundle, per caller, with an ETag - #28287

Merged
objectstack-fleet[bot] merged 13 commits into
mainfrom
claude/project-thread-hw8j7d
Oct 31, 2026
Merged

objectstack-fleet[bot] merged 13 commits into
mainfrom
claude/project-thread-hw8j7d

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

domain:spec#1 · os-tesla · session_01L9qibwM9YxDuiRDQYmA1Yd

Requested by Tesla · project thread

Part of #27627
Claim: 6538108232
Clause-②: yes (widening: a new route, its response shape and a new client method; no existing route, key or answer changes)
Metadata-protocol: added
Skills-docs: none edited. No skill teaches which metadata requests a console sends while it works inside an app, and the API skill's spec index (skills/objectstack-api/references/_index.md) names no api/ schema file, so the new api/app-bundle.zod.ts needs no row. Named, not edited.

维护者速读

  • 改了什么: 新增 GET /api/v1/meta/app/{appId}/bundle,一次请求返回控制台在一个应用里要用的元数据:应用本身和导航、导航能到达的对象及其一跳内的入向关联对象、这些对象的视图、页面,以及导航点名的仪表盘。响应带按调用者计算的弱 ETag(与其他 /meta 读取共用 feat(rest,plugin-hono-server,cloud-connection): ETag + 304 on metadata lists, uncached item reads, runtime/config and localization; db/meta Server-Timing on every timed response #28320 的同一个验证器),带 If-None-Match 且内容没变就回 304;@objectstack/client 加 meta.getAppBundle(appId, { ifNoneMatch }),并顺手修好 meta.getCached 收到 304 时抛错、走不到自己 304 分支的问题(席位要求)。
  • 为什么改: PR 1(feat(rest,console): one request opens a record page #27713)把打开记录的数据请求合成了一个,但每打开一个对象仍要单独读一次元数据;云端每个请求本身就要 0.5–1.4 秒,元数据一次取齐、会话内只做 304 校验,打开记录才不再等元数据。
  • 风险与回滚: 包里每一项都以同一调用者身份走单项读取 GET /meta/{类型}/{名称} 的同一条链路 createMetaItemAnswer(同一个读取闸门、字段可见性屏蔽、语言翻译),没有第二份闸门;看不到的项不出现,看不到应用就按应用读取的原样拒绝整个请求。ETag 由共用验证器对发给这个调用者的响应体求哈希,两人只有拿到完全相同的内容才共用一个 ETag;字段可见性无法判定时改为 private, no-store 且不发 ETag。原有路由不变,回滚 = revert 本 PR。
  • 先看: packages/rest/src/meta-app-bundle-routes.ts(成员推导、逐项过闸门、验证器与 304),然后 packages/rest/src/meta-app-bundle-routes.test.ts 里“每一项与它自己的单项读取逐字节相同”那条。
  • 两分钟验证: cd packages/rest && npx vitest run src/meta-app-bundle-routes.test.ts → 15 个用例绿;把交给共用验证器的内容改成只有键名(与调用者无关)会让“ETag 等于响应体的哈希”“不同闸门得到不同 ETag”和“304/另一调用者的 ETag 回 200”三条变红。
  • 需要你做的: 无,席位落地。

What changes

  • The shape (AppMetadataBundleResponseSchema, @objectstack/spec/api, in packages/spec/src/api/app-bundle.zod.ts beside PR 1's record-page.zod.ts): { app, objects, views, pages, dashboards }, answered bare like the /meta item reads. app and every list entry are GetMetaItemResponseSchema, the envelope the member's own GET /meta/{type}/{name} answers. The module header states what each list holds and in which order, the absence rule and the cache contract.
  • The route (packages/rest/src/meta-app-bundle-routes.ts, registered inside the /meta umbrella on the per-item switch metadata.endpoints.item, so the anonymous deny and the type read admission wrap it like every /meta read):
    • Members. The app is read first. Its navigation is walked at every depth (top-level navigation, areas[].navigation, children): object entries name objects and their viewName, page entries name pages, dashboard entries name dashboards. Objects are the navigation's, then one hop of inbound relations (an object with a lookup or master_detail field whose reference is a reached object). Views are the navigation's named views plus every view that binds to a served object (deriveViewContainerObject); pages are the navigation's plus every page whose object is a served object; dashboards are the navigation's.
    • One gate. Every member is createMetaItemAnswer over p.getMetaItem({ type, name }) with the rendered read's policy { arms: 'all', app: 'gate' } and the member type's own field-visibility posture: the same absence check, read gate, doc locale collapse, field mask and translation the item read runs. A member the chain refuses or finds absent is left out, with nothing in the body naming it. An app the caller may not read, or an unknown app, answers exactly what GET /meta/app/{appId} answers. A field-visibility evaluation fault answers the item read's 503.
    • Validator. The /meta reads' shared one from feat(rest,plugin-hono-server,cloud-connection): ETag + 304 on metadata lists, uncached item reads, runtime/config and localization; db/meta Server-Timing on every timed response #28320: the body is sent by sendMetaRepresentation(req, res, body, metaRepresentationCaching(body, undetermined), metadata.enableCache !== false), so the ETag is the weak representationEtag of the served body, computed after every gate and therefore per caller by construction. Cache-Control: private, no-cache, Vary: Accept-Language, and a matching If-None-Match (compared weakly) answers 304 with no body. A member served under an undetermined field visibility is one the item chain issued no etag; its private, no-store becomes the whole bundle's, with no ETag. metadata.enableCache: false sends no validator, as it does for the item reads. The route has no validator rule of its own.
    • Server-Timing. The member reads run inside measureServerTiming('meta', …, 'Metadata reads'), so a meta span sits beside db and total under the existing gate. Opening the header to every caller is out of scope.
    • Closed query set. refuseUnknownQueryParams with an empty set: any query parameter is a 400 before anything is read.
  • rest-server.ts: metaItemAnswerSources takes the member's type as an optional fifth argument (defaulting to req.params.type, so the item routes are unchanged), and the bundle route is registered after the item reads.
  • The client (packages/client/src/index.ts): meta.getAppBundle(appId, { ifNoneMatch }) answers AppBundleCacheResponse, { notModified: false, data, etag } on a 200 and { notModified: true, etag } on a 304. It follows the spelling of the existing conditional read meta.getCached (ifNoneMatch in, notModified and etag: { value, weak } out). The private fetch gains an opt-in admitNotModified so a 304 is answered instead of thrown. meta.getCached passes it too, which fixes its unreachable 304 branch (see Acceptance notes).
  • Ledger and censuses: a GET /api/v1/meta/app/:appId/bundle row in rest-route-ledger.ts (sdk, meta.getAppBundle, AppMetadataBundleResponseSchema); the new module on REST_SERVER_REGISTRAR_FILES and REST_SERVER_META_ROUTE_FILES; its canonical-query (0) and docblock (16) floors; the mount-table pin's metadata.endpoints.item and api.enableMetadata rows; a check-route-envelope MODULES row, ratcheted on [tracking] Envelope-position convergence line for packages/rest's flat dialect — the live ratchet owner #9559 like record-page-routes.ts.
  • Generated: api-surface/, authorable-surface/, declaration-map/, export-origins/, json-schema.manifest/, content/docs/references/api/app-bundle.mdx and the reference indexes, the strictness-ledger count, llms.txt counts.
  • Changeset .changeset/27627-app-metadata-bundle.md, minor for @objectstack/spec, @objectstack/rest and @objectstack/client.

Tests

  • packages/spec/src/api/app-bundle.test.ts (3): the shape takes the app and four lists of item envelopes; each list's element is GetMetaItemResponseSchema itself; a missing list, an entry without its envelope and an undeclared key are refused.
  • packages/rest/src/meta-app-bundle-routes.test.ts (15), on a real RestServer mount with a CRM app (navigation with a gated object, a page, two dashboards one gated, and an area) and a gated payroll app:
    • the navigation walk and the inbound-relation hop, and the empty closed query set;
    • the admin's bundle holds every member in order and parses against the named schema;
    • every member is byte-equal to that caller's own GET /meta/{type}/{name}, for both callers;
    • a caller without read gets a bundle without the item: the member's bundle has no gated object, no object reached only through it, no gated view and no gated dashboard, and no member names them (the app's own read still lists the gated dashboard in its navigation, as GET /meta/app/crm does);
    • a Server-Timing meta mark is recorded;
    • the ETag is exactly representationEtag of the served body, weak, under private, no-cache;
    • differing gates give different ETags, and the same caller twice gives the same one;
    • an unchanged bundle answers 304 (with the tag as sent, and with its bare opaque form), while another caller's ETag or a changed store answers 200 with a new ETag;
    • metadata.enableCache: false sends no validator and answers a matching If-None-Match in full;
    • an undetermined field-visibility posture serves private, no-store with no ETag;
    • an unknown app gets the same refusal as GET /meta/app/{unknown} (status and body), and only the app is read; a gated app gets the app read's 403;
    • an unknown query parameter is a 400 with nothing read.
  • packages/client/src/meta-app-bundle.test.ts (3): the URL is encoded and the 200 answers data and etag; ifNoneMatch is sent and a 304 answers notModified instead of throwing; a 403 still rejects with the server's status and code; meta.getCached answers a 304 as { notModified: true } with the validator it sent, and a 404 still rejects. Ablation: dropping the opt-in from getCached turns that 304 test red, and the restore brings back HEAD's blob.
  • Ablation of the per-caller fold: node scripts/ablation-replace.mjs swapped metaRepresentationCaching(body, undetermined) for metaRepresentationCaching(Object.keys(body), undetermined), a validator input that no longer depends on what this caller was served. 3 of 15 went red (the representationEtag pin, "two callers whose gates differ are answered two validators", and "an unchanged bundle answers 304 … another caller's validator, or a changed store, is answered in full"). The tool restored the blob to HEAD, and the rerun is 15/15 green. (On the earlier head, before the shared helper, the same ablation on the route's own validator turned 2 of 14 red.)

Acceptance notes

  • Validator, a declared deviation from the order's "fold the same dimensions": the ETag hashes the served body after every gate instead of folding the per-dimension fingerprints. Two callers share it exactly when they are served the same body, which the fold could only approximate. Spec seat 1 confirmed this rule. Since feat(rest,plugin-hono-server,cloud-connection): ETag + 304 on metadata lists, uncached item reads, runtime/config and localization; db/meta Server-Timing on every timed response #28320 landed, the bundle uses that PR's shared helper rather than its own copy of the rule, so the ETag is weak W/"…" like every other /meta read's. The members' bytes did not change.
  • Runtime dispatcher: the bundle is served by the RestServer route only. The runtime dispatcher's /meta catch-all does not serve it, the same as PR 1's record-page route.
  • Dashboards come from the navigation only. A dashboard has no object binding to follow, so no second rule adds them.
  • Deviation, added at spec seat 1's request: meta.getCached's 304 is fixed here. Its 304 branch in packages/client was unreachable, because the private fetch threw on any non-ok status before the branch ran, so a matching If-None-Match rejected instead of answering notModified. It now passes the same admitNotModified opt-in as getAppBundle. The one consumer, useMetadata's cached arm in @objectstack/client-react, already branches on notModified, and its suite is 34/34.
  • Out of scope per the order: the console consumer (ui seat 2, which is bound to this shape), opening Server-Timing to every caller, and cloud#3378.

Verification

Head 170b86ad, merged with main at f49b4787 (which brings #28320).

  • Tests: route 15/15; full packages/rest suite 361 files, 7148 passed; client suite 687/687 and client-react 34/34 at 1e7a1b10 (the client is unchanged since); dogfood authz-probe-blind-spot and authz-conformance 70/70 at 28ac68c2.
  • Typecheck: tsc --noEmit in packages/rest, packages/client and packages/spec, all exit 0.
  • Generated artifacts: the spec header's wording change regenerated content/docs/references/api/app-bundle.mdx (check:generated --fix, which rebuilt the spec dist first, then exit 0); check:route-envelope and its self-test exit 0.
  • Derived gates on 170b86ad: node scripts/pm/dispatch-gates.mjs --commands named the same 125 commands as before. Every one was run with stdin closed and its exit recorded, and all 125 exit 0. The ones that read built output, plus check:docs and check:generated, first recorded exit 3, which means "not measured", not a finding. They were rerun after the spec, client and client-react rebuilds, the page regeneration, and a pnpm install --frozen-lockfile for the workspace link the merged main added to @objectstack/cloud-connection. --ran reconciliation: "125 derived, 125 run, 0 NOT-MEASURED, 0 UNRUN", exit 0. The earlier run on 28ac68c2 was also 125 of 125. Its check:doc-authoring caught a real #27627 in the ledger row's note: string, and that fix is in this head.

Generated by Claude Code

`AppMetadataBundleResponseSchema` (`@objectstack/spec/api`) is the answer
of `GET /api/v1/meta/app/{appId}/bundle`: the app and the objects, views,
pages and dashboards its navigation reaches, each member the envelope its
own `GET /meta/{type}/{name}` answers. Generated artifacts regenerated.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibwM9YxDuiRDQYmA1Yd
@github-actions

github-actions Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/client, @objectstack/rest, @objectstack/spec, touching 53 documentable anchor(s). ⚠️ 7 changed file(s) yielded no anchor (packages/spec/api-surface/api.json, packages/spec/authorable-surface/api.json, packages/spec/declaration-map/api.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

34 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 22ce3d13fec94dab9df67edccd9cc71277a99a68.

⛔ 14 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 7 changed file(s) yielded no anchor (packages/spec/api-surface/api.json, packages/spec/authorable-surface/api.json, packages/spec/declaration-map/api.json, …) — pages documenting those are invisible to this run
  • 3 cross-cutting symbol(s) contributed no route anchor: resolveProtocol (28 routes), responseSchema (6 routes), resolveObjectMasker (4 routes)
  • 1 anchor(s) matched too much of the corpus to be a work list: master_detail (literal, 39 pages)
  • 8 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 212 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 175 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 22ce3d13fec94dab9df67edccd9cc71277a99a68 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 22ce3d13fec94dab9df67edccd9cc71277a99a68

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

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibwM9YxDuiRDQYmA1Yd
@github-actions github-actions Bot added size/l and removed size/m labels Oct 31, 2026
@github-actions github-actions Bot added size/xl and removed size/l labels Oct 31, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract-review-claim: session_01L9qibw58ehQ2gRHP2h2Puz · head 1e7a1b104ea5f7afccee6cb52f21b1c5fc01c501 · 2026-10-31T04:41Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 1e7a1b104ea5f7afccee6cb52f21b1c5fc01c501
Local-runs: none
Scope: packages/spec/src/api/app-bundle.zod.ts (AppMetadataBundleResponseSchema, its header, the two exported types), packages/spec/src/api/index.ts, packages/rest/src/meta-app-bundle-routes.ts (whole: appNavigationReach, inboundRelationObjects, bundleValidator, BundleMemberFault, registerMetaAppBundleRoute), packages/rest/src/rest-server.ts (metaItemAnswerSources's fifth argument, the appBundleHost registration), packages/rest/src/rest-route-ledger.ts, packages/rest/src/rest-server-registrar-files.ts, packages/client/src/index.ts (AppBundleCacheResponse, meta.getAppBundle, the private fetch's admitNotModified and meta.getCached's one-line opt-in), scripts/check-route-envelope.mjs, the five test files, the five generated spec artifacts, content/docs/references/api (generated), the strictness-ledger count, .changeset/27627-app-metadata-bundle.md + packages/rest/src/meta-item-read-gate.ts on main (MetaItemAnswer, createMetaItemAnswer, META_UNDETERMINED_CACHE_CONTROL), packages/metadata-core/src/object-schema-fls.ts (normalizeIfNoneMatch), packages/rest/src/meta-item-history-routes.ts (the literal third segments under :type/:name), scripts/pm/check-governed-merges.mjs (the governed register), PR #28320's file list

Inputs: card #27627 (body and all fifteen comments: the epic filing, spec seat 3's PR 1 landing 6533483101, spec seat 1's two claims 6538102504 / 6538108232 and their corrections 6538175648 / 6538237411 / 6538251138 / 6539077865, ui seat 2's notes 6538126238 / 6539052883, the two dev reports 6539053683 (Target 4, PR #28320) and 6539061874 (this PR)), PR #28287 (body, the docs-drift comment, the file list of 26 files, the diff of 1636 lines against main read whole, the commits from merge-base 5374967), the 54 check-runs on this head (read 2026-10-31T05:10Z: all 54 runs completed, none failed, none cancelled, none in progress; success: Build & E2E UI, Build Core, Build Docs, Build UI Deps, Check Changeset, Check Documentation Links, Dogfood Regression Gate, Flag docs affected by code changes, Governed Surface Queue Guard, Lint & Repo Gates, Lint · CI, release & engine gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Lint · scoped · tooling self-tests, Live E2E UI, PR-body guards (claim · duplicate · Part-of · single-writer path), Plan Test Core shards, Spec property liveness, Test Core, Test Core (all 8), Test UI, Type Check UI, Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates, Type Check · workspace, TypeScript Type Check, filter; skipped: 18.x upgrade harness (opt-in), Auto Label, Check PR Size, Dogfood Regression Gate (matrix shard/3), Dogfood Verify CLI, Lint UI, Packed-tarball smoke (opt-in), Schedule gate · skip when a green push run covers the commit, Temporal Conformance (live PG + MySQL), Test UI (matrix shard/16), UI Published Artifacts), and the tree at this head and on main for the items the Scope line names. The head is mergeable and clean against today's main (merge-tree clean; none of the files main moved since the merge-base is in this diff). The diff touches no governed surface in the register, so no Tier H gate applies.

① Derived judgments

  • The route delivers Target 2 as the card and the claim state it - right. GET {metaPath}/app/:appId/bundle registers through the per-item registrar inside the /meta umbrella, so the anonymous deny and the type admissions wrap it; the query set is closed and empty (refuseUnknownQueryParams first, pinned: a 400 before any read). The app is read first; appNavigationReach walks navigation, every area's navigation and children at any depth, collecting objectName / viewName, pageName, dashboardName once each in order; inboundRelationObjects adds one hop of lookup / master_detail referrers from the whole-type object list, from either stored shape of fields; views and pages bound to a served object join the navigation's by deriveViewContainerObject and page.object; dashboards are the navigation's. Every rule in the spec header has a pinned case.
  • One gate, no copy - right, and this is the judgment that matters. Every member, the app included, is createMetaItemAnswer over getMetaItem({ type, name }) under the rendered read's policy and the member type's own masker (resolved once per type per request), with metaItemAnswerSources handed the member's type. A refuse of any reason, or the bare not-found the item read serves as absent, leaves the member out; the app's refuse is sent by sendMetaReadRefusal and its absence by sendMetaItemAbsent, so an unknown or gated app answers exactly what GET /meta/app/{appId} answers (pinned byte for byte, status and body, and only the app read runs). A mask fault on any member is the whole answer (sendFieldVisibilityFault), never an absence. The byte-equality test runs every member of both callers' bundles against that caller's own item read.
  • The validator is per caller by construction - right. The strong ETag is the SHA-256 of the JSON this response sends, after every gate, mask and translation, so it folds every dimension the item routes fold and the locale; Cache-Control: private, no-cache and Vary: Accept-Language are set; If-None-Match is compared through normalizeIfNoneMatch, which strips both the quotes and a W/ prefix, and a match answers 304 with no body. When any served member carries the undetermined cacheControl, the bundle goes out private, no-store with no validator, which is the item read's own arm. Pinned: differing gates give differing tags, one caller twice gives one, another caller's tag or a changed store answers 200 with a new tag, the no-store arm; the ablation that hashed the keys alone went red on exactly the two per-caller cases.
  • No path shadows and nothing shadowed - right. The literal bundle sits under app/:appId, three segments below /meta; every per-item sub-resource on main is :type/:name plus a literal (history, audit, publish, rollback, diff, published), and no route on main has a parameter third segment, so the new path matches only itself and the item read, the writes and the sub-resources keep their matches.
  • metaItemAnswerSources's fifth argument - right. It defaults to req.params.type, so the item routes are unchanged; only the bundle passes a type.
  • The client - right. meta.getAppBundle(appId, { ifNoneMatch }) follows meta.getCached's spelling (notModified, etag: { value, weak }), encodes the id, and opts into the private fetch's admitNotModified so a 304 is the answer and not a thrown failure; every other call keeps the throw (the flag defaults off). A refusal still rejects with the server's status and code (pinned). The head's last commit also passes the opt-in from meta.getCached, so its existing 304 branch, unreachable on main because the transport threw first, now runs and answers { notModified: true } as its own docblock always said; pinned with two added cases (the 304 answer with the validator sent, and a refusal still rejecting), and named in the changeset.
  • Server-Timing - the member reads run under measureServerTiming('meta', …), so a meta span sits beside db and total under the existing gate; pinned once. Opening the header to every caller is Target 4's, not this PR's.
  • Ledgers, censuses and generated artifacts - the route-ledger row (sdk, meta.getAppBundle, AppMetadataBundleResponseSchema, answered bare), both registrar lists, the mount-table pin rows, the canonical-query and docblock floors, and the check-route-envelope MODULES row (2 bare responses, 0 ok / 0 err, ratcheted on [tracking] Envelope-position convergence line for packages/rest's flat dialect — the live ratchet owner #9559 like record-page-routes.ts) are all present; the spec surface artifacts, the strictness-ledger count, llms.txt and the generated reference page follow. Skills-docs: none edited holds: no skill names an api/ schema file.
  • Check-runs as gate verdicts - read as above; every run on this head completed and none is red, cancelled or in progress at the read.

② Semver level

  • @objectstack/spec, @objectstack/rest, @objectstack/client all minor; Clause-②: yes (widening); Metadata-protocol: added - accepted. A new route, a new exported response schema and a new client method, with no existing route, key or answer changed; Metadata-protocol: added is the right declaration for a new shape on the wire, and the lane's precedent for an added protocol shape is minor. The private fetch's third parameter is not on the public surface.

③ Boundary flags

  • Serial overlap with feat(rest,plugin-hono-server,cloud-connection): ETag + 304 on metadata lists, uncached item reads, runtime/config and localization; db/meta Server-Timing on every timed response #28320 (Target 4, same seat) on one file - both add a MODULES row to scripts/check-route-envelope.mjs; the rows sit in different places, so the merge should be clean, but whichever lands second merges main and re-runs check:route-envelope. The body also commits to moving this route onto feat(rest,plugin-hono-server,cloud-connection): ETag + 304 on metadata lists, uncached item reads, runtime/config and localization; db/meta Server-Timing on every timed response #28320's shared body-hash validator (@objectstack/types representation-validator.ts) if that lands first; until then the two rules are the same rule written twice, which is a named follow-up, not a defect.
  • Name approval owed to the maintainer - AppMetadataBundleResponseSchema and meta.getAppBundle are new public names on the spec and client surfaces; the route path is the card body's own (Target 2). The seat records the approval when it lands.
  • The runtime dispatcher's /meta twin does not serve the bundle - the same posture as PR 1's record-page route; a deployment that serves metadata through the dispatcher alone gets no bundle. Named in the body; the console consumer (ui seat 2) must read it through the RestServer mount.
  • Dashboards come from the navigation only - by design, as a dashboard has no object binding; the spec header says so.
  • meta.getCached's 304 now reaches its caller - on main a matching If-None-Match rejected with a thrown 304 before the method's own branch ran; with this head it answers { notModified: true }. A consumer that relied on the throw was relying on a defect, and the method's docblock always promised the branch, so this rides the minor bump as a fix; the changeset names it.
  • The console half is bound to this shape - ui seat 2 lands after this PR; nothing in this diff touches the console.

④ Design judgment

  • Nothing blocking, and no should-fix on the diff. Tried: an app whose navigation names an object the caller may not read (the gate has already pruned the entry, so nothing is reached through it, pinned); an object reached only through a gated object (absent, pinned); a view bound to a served object but gated itself (absent, pinned); a member whose fetch throws something other than the bare not-found (the whole request fails through handleRouteError, never a silent absence); a list read that throws (the same); a runtime without Web Crypto (served with no validator rather than a weaker one); If-None-Match with a weak prefix (matches). Deriving the member set from the app the caller is served, and answering each member through the one item chain, is the right shape: the bundle can never say more than the item reads would.

Verdict reasoning. The route is the item reads composed, as the claim demanded: one gate, one mask, one translation, per member, pinned byte for byte against the single reads for two callers, and a refused app or an unjudgeable member is the whole answer. The validator is the served bytes, so it is per caller without a second fold. The route sits where it shadows nothing, the ledgers and censuses are filled, the client opts into 304 narrowly, and semver is minor with the protocol addition declared. The overlap with #28320 is one file and a named helper move.

Implemented-by: claude/project-thread-hw8j7d
Reviewed-by: session_01L9qibw58ehQ2gRHP2h2Puz

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract-review-claim: session_01L9qibw58ehQ2gRHP2h2Puz · head 170b86ad19fb0e32fa0fc1462ece83ab32ed28d0 · 2026-10-31T06:44Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 170b86ad19fb0e32fa0fc1462ece83ab32ed28d0
Local-runs: none
Scope: packages/rest/src/meta-app-bundle-routes.ts (the send site: undetermined, sendMetaRepresentation, metaRepresentationCaching; bundleValidator and the normalizeIfNoneMatch import removed; the module header), packages/rest/src/meta-app-bundle-routes.test.ts (the validator pins), packages/rest/src/rest-server-docblock-position.test.ts (the floor 16 to 15), scripts/check-route-envelope.mjs (the row 0 / 0 / 0), packages/spec/src/api/app-bundle.zod.ts (the header's wording), content/docs/references/api/app-bundle.mdx (regenerated), .changeset/27627-app-metadata-bundle.md (wording, and the meta.getCached bullet) + the files of record 6539412174, unchanged by the delta

Inputs: the seat's hand-off in its thread naming this head and the seven points of the switch, PR #28287 (body, all comments, the file list of 26 files, the thirteen commits from the merge-base b4ccbc6), my record 6539412174 (PASS on 1e7a1b1, with the overlap flag on #28320), the 54 check-runs on this head (read 2026-10-31T07:11Z: all 54 runs completed, none failed, none cancelled, none in progress; success: Build & E2E UI, Build Core, Build Docs, Build UI Deps, Check Changeset, Check Documentation Links, Dogfood Regression Gate, Flag docs affected by code changes, Governed Surface Queue Guard, Lint & Repo Gates, Lint · CI, release & engine gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Lint · scoped · tooling self-tests, Live E2E UI, PR-body guards (claim · duplicate · Part-of · single-writer path), Plan Test Core shards, Spec property liveness, Test Core, Test Core (all 8), Test UI, Type Check UI, Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates, Type Check · workspace, TypeScript Type Check, filter; skipped: 18.x upgrade harness (opt-in), Auto Label, Check PR Size, Dogfood Regression Gate (matrix shard/3), Dogfood Verify CLI, Lint UI, Packed-tarball smoke (opt-in), Schedule gate · skip when a green push run covers the commit, Temporal Conformance (live PG + MySQL), Test UI (matrix shard/16), UI Published Artifacts), and the tree on main for the helper this head adopts (packages/rest/src/meta-representation-validator.ts: metaRepresentationCaching keeps a tier-2 private, no-store with no validator, else private, no-cache and representationEtag; sendMetaRepresentation sends nothing cache-related when enableCache is false and a validator was issued, else the header, the ETag and a bodiless 304 on ifNoneMatchHits) and for the item chain that builds every member (meta-item-read-gate.ts :3739: a served member is spread from metaRepresentationCaching(served, masked.cacheControl), so a member's etag is undefined exactly when its cacheControl is the tier-2 private, no-store). This is a delta review of record 6539412174: the PR-only diff at 1e7a1b1 and the PR-only diff at this head were compared line by line with index and hunk-position lines stripped (206 lines of interdiff), and the merge commit f49b478 was read in combined form, which shows no hunk: the merge of main (#28320 among 23 commits) resolved nothing by hand. The judgments of record 6539412174 on every line the two commits after the merge did not touch stand.

① Derived judgments

  • The delta is the merge, one refactor and one regeneration; the members are unchanged - right. appNavigationReach, inboundRelationObjects, BundleMemberFault, the member build through metaItemAnswerSources, the refusal paths and the schema are byte-identical in both PR-only diffs; the interdiff is the send site, the validator pins, the docblock floor, the route-envelope row, and the wording of the header, the reference page and the changeset.
  • The bundle now sends through the /meta reads' one rule - right, and it is the rule feat(rest,plugin-hono-server,cloud-connection): ETag + 304 on metadata lists, uncached item reads, runtime/config and localization; db/meta Server-Timing on every timed response #28320 made shared after record 6539412174 flagged the overlap. The dev's own SHA-256 validator and its If-None-Match compare are gone; sendMetaRepresentation issues the weak W/"…" ETag of the served body (representationEtag), under private, no-cache, and answers a matching If-None-Match with a bodiless 304 compared weakly (RFC 9110 section 13.1.2, pinned both with and without the W/ prefix). The per-caller folding record 6539412174 judged (the gate, the mask, the runtime view, the intake reason, the locale) is unchanged in substance: the hash is still over the body served to this caller, so two callers share a validator only when they are sent the same body.
  • The tier-2 posture is kept, now by the member's missing validator - right. undetermined is the cacheControl of the first member issued no etag; by the item chain's own construction that is a member served private, no-store and nothing else, so the whole bundle keeps private, no-store with no validator, as record 6539412174 required and the pin keeps.
  • metadata.enableCache: false is now honoured - right, and new: the first head always sent a validator; this head sends none and no Cache-Control, answering a matching If-None-Match in full, as the item reads do. Pinned. A behaviour change on an unreleased route, in the direction of the deployment's own declared setting.
  • The counts and ledgers follow - right. One module-level helper fewer, so the docblock floor is 15 and the sum 95; the route-envelope row is responses: 0, ok: 0, err: 0 because the module builds no response body itself; scripts/check-route-envelope.mjs is the one file this PR and feat(rest,plugin-hono-server,cloud-connection): ETag + 304 on metadata lists, uncached item reads, runtime/config and localization; db/meta Server-Timing on every timed response #28320 both touched, and the merge took both rows cleanly.
  • The spec header, the reference page and the changeset say "weak" and "body" - right; app-bundle.mdx is regenerated from the header. The changeset's added bullet on meta.getCached answering { notModified: true } through the same opt-in is the client change record 6539412174 already accepted, now named in the entry.
  • Check-runs as gate verdicts - read as above; every run on this head completed and none is red, cancelled or in progress at the read. The head is mergeable and clean against today's main.

② Semver level

  • @objectstack/spec minor, @objectstack/rest minor, @objectstack/client minor; Clause-②: yes; Metadata-protocol: added - unchanged by the delta and right, as record 6539412174 judged.

③ Boundary flags

④ Design judgment

  • The delta removes a second validator implementation in favour of the one the /meta family shares, keeps the one exception the family has (tier 2) by the family's own signal, and gains the deployment switch the family honours. Fewer lines, one rule, nothing lost. Nothing to add to record 6539412174's ④.

Verdict reasoning. The merge resolved nothing by hand; the refactor replaces the bundle's own validator with the shared helper #28320 landed, keeping the per-caller rule, the tier-2 exception and adding the cache switch, each pinned; members, schema and client are unchanged; the semver stands; every check on the head is green or skipped. The prior PASS carries to this head.

Implemented-by: claude/project-thread-hw8j7d
Reviewed-by: session_01L9qibw58ehQ2gRHP2h2Puz

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Maintainer ruling recorded · 2026-10-31T09:33Z · head 170b86ad19fb0e32fa0fc1462ece83ab32ed28d0.

  • Names AppMetadataBundleResponseSchema and meta.getAppBundle — Tesla (os-tesla) answered 「同意」 at 2026-10-31T09:30Z in the contract-review lane thread, to the lane's ask that named these two for this PR beside the delta PASS 6540468220.
  • Nothing is held for the maintainer any more; the seat lands per the PASS record's landing terms, re-reading the checks on the landing head.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-tesla · session_01L9qibwb6hBqszPTYJobitm

维护者速读

席位核对 · ACCEPT · domain:spec seat 1 (#6017) · 2026-10-31T09:35Z

Head 170b86ad19fb0e32fa0fc1462ece83ab32ed28d0. Card #27627 (PR 2, Part of). Claim 6538108232, correction 6538237411, os-dev-report 6539061874. The maintainer's rulings, verbatim: 「27627 插队」, 「按 pm-dispatch 审过的pr你可以自主落地」, and 「同意」 on the names (6541747154).

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 31, 2026 09:37
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 31, 2026 09:37
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 31, 2026
Merged via the queue into main with commit 8441a01 Oct 31, 2026
55 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-hw8j7d branch October 31, 2026 10:00
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/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant