Repository navigation
feat(rest): one request loads an app's metadata — GET /meta/app/{appId}/bundle, per caller, with an ETag - #28287
Conversation
`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
📓 Docs Drift CheckThis PR changes 3 package(s): 34 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 14 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 175 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwM9YxDuiRDQYmA1Yd
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwM9YxDuiRDQYmA1Yd
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwM9YxDuiRDQYmA1Yd
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwM9YxDuiRDQYmA1Yd
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwM9YxDuiRDQYmA1Yd
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwM9YxDuiRDQYmA1Yd
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwM9YxDuiRDQYmA1Yd
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwM9YxDuiRDQYmA1Yd
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwM9YxDuiRDQYmA1Yd
|
domain:spec#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract-review-claim: |
|
domain:spec#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract reviewServed-tier: 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 ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
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: VERDICT: PASS |
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwM9YxDuiRDQYmA1Yd
…dator Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwM9YxDuiRDQYmA1Yd
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwM9YxDuiRDQYmA1Yd
|
domain:spec#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract-review-claim: |
|
domain:spec#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract reviewServed-tier: 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 ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
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: VERDICT: PASS |
|
domain:spec#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Maintainer ruling recorded · 2026-10-31T09:33Z · head
|
|
domain:spec#1 · os-tesla · session_01L9qibwb6hBqszPTYJobitm 维护者速读
席位核对 · ACCEPT ·
|
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 noapi/schema file, so the newapi/app-bundle.zod.tsneeds 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 分支的问题(席位要求)。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
AppMetadataBundleResponseSchema,@objectstack/spec/api, inpackages/spec/src/api/app-bundle.zod.tsbeside PR 1'srecord-page.zod.ts):{ app, objects, views, pages, dashboards }, answered bare like the/metaitem reads.appand every list entry areGetMetaItemResponseSchema, the envelope the member's ownGET /meta/{type}/{name}answers. The module header states what each list holds and in which order, the absence rule and the cache contract.packages/rest/src/meta-app-bundle-routes.ts, registered inside the/metaumbrella on the per-item switchmetadata.endpoints.item, so the anonymous deny and the type read admission wrap it like every/metaread):navigation,areas[].navigation,children):objectentries name objects and theirviewName,pageentries name pages,dashboardentries name dashboards. Objects are the navigation's, then one hop of inbound relations (an object with alookupormaster_detailfield whosereferenceis 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 whoseobjectis a served object; dashboards are the navigation's.createMetaItemAnsweroverp.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 whatGET /meta/app/{appId}answers. A field-visibility evaluation fault answers the item read's 503./metareads' 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 bysendMetaRepresentation(req, res, body, metaRepresentationCaching(body, undetermined), metadata.enableCache !== false), so theETagis the weakrepresentationEtagof the served body, computed after every gate and therefore per caller by construction.Cache-Control: private, no-cache,Vary: Accept-Language, and a matchingIf-None-Match(compared weakly) answers304with no body. A member served under an undetermined field visibility is one the item chain issued no etag; itsprivate, no-storebecomes the whole bundle's, with noETag.metadata.enableCache: falsesends no validator, as it does for the item reads. The route has no validator rule of its own.measureServerTiming('meta', …, 'Metadata reads'), so ametaspan sits besidedbandtotalunder the existing gate. Opening the header to every caller is out of scope.refuseUnknownQueryParamswith an empty set: any query parameter is a 400 before anything is read.rest-server.ts:metaItemAnswerSourcestakes the member's type as an optional fifth argument (defaulting toreq.params.type, so the item routes are unchanged), and the bundle route is registered after the item reads.packages/client/src/index.ts):meta.getAppBundle(appId, { ifNoneMatch })answersAppBundleCacheResponse,{ notModified: false, data, etag }on a200and{ notModified: true, etag }on a304. It follows the spelling of the existing conditional readmeta.getCached(ifNoneMatchin,notModifiedandetag: { value, weak }out). The privatefetchgains an opt-inadmitNotModifiedso a304is answered instead of thrown.meta.getCachedpasses it too, which fixes its unreachable304branch (see Acceptance notes).GET /api/v1/meta/app/:appId/bundlerow inrest-route-ledger.ts(sdk,meta.getAppBundle,AppMetadataBundleResponseSchema); the new module onREST_SERVER_REGISTRAR_FILESandREST_SERVER_META_ROUTE_FILES; its canonical-query (0) and docblock (16) floors; the mount-table pin'smetadata.endpoints.itemandapi.enableMetadatarows; acheck-route-envelopeMODULES row, ratcheted on [tracking] Envelope-position convergence line for packages/rest's flat dialect — the live ratchet owner #9559 likerecord-page-routes.ts.api-surface/,authorable-surface/,declaration-map/,export-origins/,json-schema.manifest/,content/docs/references/api/app-bundle.mdxand the reference indexes, the strictness-ledger count,llms.txtcounts..changeset/27627-app-metadata-bundle.md,minorfor@objectstack/spec,@objectstack/restand@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 isGetMetaItemResponseSchemaitself; 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 realRestServermount with a CRM app (navigation with a gated object, a page, two dashboards one gated, and an area) and a gated payroll app:GET /meta/{type}/{name}, for both callers;GET /meta/app/crmdoes);metamark is recorded;ETagis exactlyrepresentationEtagof the served body, weak, underprivate, no-cache;metadata.enableCache: falsesends no validator and answers a matchingIf-None-Matchin full;private, no-storewith no ETag;GET /meta/app/{unknown}(status and body), and only the app is read; a gated app gets the app read's 403;packages/client/src/meta-app-bundle.test.ts(3): the URL is encoded and the 200 answers data and etag;ifNoneMatchis sent and a 304 answersnotModifiedinstead of throwing; a 403 still rejects with the server's status and code;meta.getCachedanswers a 304 as{ notModified: true }with the validator it sent, and a 404 still rejects. Ablation: dropping the opt-in fromgetCachedturns that 304 test red, and the restore brings back HEAD's blob.node scripts/ablation-replace.mjsswappedmetaRepresentationCaching(body, undetermined)formetaRepresentationCaching(Object.keys(body), undetermined), a validator input that no longer depends on what this caller was served. 3 of 15 went red (therepresentationEtagpin, "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
ETagis weakW/"…"like every other/metaread's. The members' bytes did not change.RestServerroute only. The runtime dispatcher's/metacatch-all does not serve it, the same as PR 1's record-page route.meta.getCached's304is fixed here. Its304branch inpackages/clientwas unreachable, because the privatefetchthrew on any non-okstatus before the branch ran, so a matchingIf-None-Matchrejected instead of answeringnotModified. It now passes the sameadmitNotModifiedopt-in asgetAppBundle. The one consumer,useMetadata's cached arm in@objectstack/client-react, already branches onnotModified, and its suite is 34/34.Verification
Head
170b86ad, merged with main atf49b4787(which brings #28320).packages/restsuite 361 files, 7148 passed; client suite 687/687 andclient-react34/34 at1e7a1b10(the client is unchanged since); dogfoodauthz-probe-blind-spotandauthz-conformance70/70 at28ac68c2.tsc --noEmitinpackages/rest,packages/clientandpackages/spec, all exit 0.content/docs/references/api/app-bundle.mdx(check:generated --fix, which rebuilt the spec dist first, then exit 0);check:route-envelopeand its self-test exit 0.170b86ad:node scripts/pm/dispatch-gates.mjs --commandsnamed 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, pluscheck:docsandcheck: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 apnpm install --frozen-lockfilefor the workspace link the merged main added to@objectstack/cloud-connection.--ranreconciliation: "125 derived, 125 run, 0 NOT-MEASURED, 0 UNRUN", exit 0. The earlier run on28ac68c2was also 125 of 125. Itscheck:doc-authoringcaught a real#27627in the ledger row'snote:string, and that fix is in this head.Generated by Claude Code