Skip to content

perf(metadata-protocol,rest,runtime): /meta/app judges its navigation on the execution face; diagnostics verdicts are memoized by content and schema - #25941

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-mvtze8
Oct 26, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-mvtze8

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

Closes #25783

Clause-②: yes (widening): the exported RestProtocol type of @objectstack/rest gains one optional member, getMetaItemsForExecution?. Measured on the built dist/index.d.ts at cb0a2fcf and at 22f886e1: rest's export list is unchanged (67 names) and, outside doc comments, that member is the only changed line; the @objectstack/metadata-protocol and @objectstack/runtime declarations are byte-identical.
Metadata-protocol: none

A read-path cost change. No accept set, served body, route parameter or error code changes, and nothing under packages/spec or ui/ moves. The one published type that changes is RestProtocol, by the optional member above (changeset .changeset/25783-rest-meta-app-execution-face.md); all three packages stay patch, as Metadata-protocol: none requires. The served bytes of six metadata types were hashed before and after and are identical (below).

What changes

1. GET /meta/app judges its navigation on the protocol's execution face (packages/rest/src/rest-server.ts, packages/runtime/src/domains/meta.ts).
The nav-servability gate (resolveNavServability, #24031) reads each object's enable to prune the nav entries whose object refuses list. It read them through loadObjectItems → metaListSource → the served getMetaItems, which stamps _diagnostics on every object, so each /meta/app read ran a full zod safeParse of every object definition only to read enable. Measured at 63 of 73 ms CPU per read. RestServer.metaListSource now asks getMetaItemsForExecution for object when the protocol has it: the same list, without the serving decorations (the #24031 / #24535 precedent for /data). The dispatcher's metaItemReadGateSources.listMetaItems does the same (seat amendment 6407727649), so /meta/app through the ${prefix}/* catch-all gets the same cut. A protocol without the execution face is asked getMetaItems as before. RestProtocol gains the optional getMetaItemsForExecution member; MetaDomainProtocol gains it in its existing partial pick of the protocol type. Book and doc lists stay on the served face (their readers want the served shape).

2. Diagnostics verdicts are memoized by schema and content (packages/metadata-protocol/src/metadata-diagnostics.ts).
computeMetadataDiagnostics keeps one bounded LRU (4096 entries) per schema object, held in a WeakMap, keyed by a SHA-256 of the stripped document. The key encodes everything the parse can tell apart: plain JSON documents key on JSON.stringify; a document holding undefined (a strict schema refuses an undeclared key whose value is undefined, and an array hole parses as undefined) keys on an encoding that spells undefined out; a document holding anything without a faithful key (a function, a class instance, NaN/Infinity, -0, nesting past 256) is parsed every time, as before. A registerMetadataTypeSchema replacement is a new schema object and starts from nothing. Each caller gets its own copy of the verdict. The inputs are only the schema and the document: no global zod error map is installed anywhere, and the node-config contract maps the refinements read are fixed after first build. This is not a cache of any read's answer; it sits below every merge, on a pure function of its own input, so a metadata change is a different document and a different key. protocol.ts is untouched (0 lines).

Measured

Showcase app (examples/app-showcase, 108 objects), local, signed in as the seeded admin. Main-thread CPU per request from /proc/PID/task/PID/stat over 40 serial requests after 5 warm-ups; shares from a V8 CPU profile at 250 µs sampling over 40 more. Shared box (4 cores), so read the ratios.

Endpoint CPU before (cb0a2fcf) CPU after (a8f08c97) computeMetadataDiagnostics share before → after
GET /api/v1/meta/app 73.25 ms 16.75 ms 59.7% → 1.3%
GET /api/v1/meta/object 85.75 ms 37.00 ms 50.7% → 15.6%
GET /api/v1/meta/view 13.25 ms 11.25 ms 11.8% → 3.1%

Served bytes, SHA-256 prefix of each GET /api/v1/meta/:type body, before vs after on the first read after boot and on a warm read:

Type Bytes Before After, cold After, warm
app 17,559 68ea5cb73ccd 68ea5cb73ccd 68ea5cb73ccd
object 694,011 3fe564c0fe63 3fe564c0fe63 3fe564c0fe63
view 23,954 71352ec4f6a6 71352ec4f6a6 71352ec4f6a6
flow 52,153 d35da2f62bcd d35da2f62bcd d35da2f62bcd
dashboard 12,075 ef3c4c852a3d ef3c4c852a3d ef3c4c852a3d
page 115,021 bf75d678c85d bf75d678c85d bf75d678c85d

Tests

  • packages/rest/src/meta-app-nav-servability-execution-read.test.ts (new): the real REST handlers over the real ObjectStackProtocolImplementation and SchemaRegistry, with the object schema behind a recording proxy (the rest-exposure-gate-execution-read.test.ts instrument). Unscoped and environment-scoped: the app list and the app by name prune the unlistable entry and keep the listable one with zero object diagnostics; FALLBACK, a protocol without the execution list face, gets the same prune and diagnoses every object (so the zeros are not vacuous); CONTROL, GET /meta/object still serves _diagnostics on every object. On cb0a2fcf's rest-server.ts the four zero rows fail (expected [ 'listed_widget', 'unlisted_widget' ] to deeply equal []).
  • packages/runtime/src/domains/meta-app-nav-object-execution-face.test.ts (new): through HttpDispatcher.dispatch(), /meta/app and /meta/app/:name read object on getMetaItemsForExecution and never on getMetaItems, with the same prune; without the execution face it falls back to getMetaItems. The two execution-face rows fail on cb0a2fcf's meta.ts.
  • packages/metadata-protocol/src/metadata-diagnostics.memo.test.ts (new): one parse per document per schema, a structurally equal copy and the decorated served body included; a changed document, a replaced schema each re-parse; a missing key vs the key holding undefined, null vs an array hole, null vs NaN each get their own verdict in both orders; a function, -0, a 300-deep nesting and a Date parse every time; callers get independent copies; the 4096 LRU bound evicts the least recently used. On the real object schema, a memo hit serves byte-identical decorateMetadataItem output to a fresh parse, for a valid and an invalid document. Mutation check: with a naive JSON.stringify key all six collision rows fail.
  • packages/rest/src/rest-exposure-gate-execution-read.test.ts (re-pinned): its recording proxy is now registered per test (beforeEach), because verdicts are memoized per schema object; five rows recorded zero parses under one shared proxy.

Full suites: @objectstack/metadata-protocol 264 files / 21,505 tests, @objectstack/rest 328 / 6,730, @objectstack/runtime 448 / 8,011, all green. typecheck green for the three packages (test layers included for rest and runtime).

Gates

node scripts/pm/dispatch-gates.mjs --commands derives 71 commands for this diff; all 71 ran with exit 0, and --ran reconciles 71 run, 0 NOT-MEASURED, 0 UNRUN. Four were red on the first pass and green after:

  • check-rest-log-spy-declared: the new rest test spied on console.warn without declaring OS_REST_LOG. The spies asserted nothing, so they are gone.
  • check:engine-double-contract: the new rest test carried a findOne double no path reached. Dropped, so the pinned ledger is unchanged.
  • check-plugin-teardown-shape --self-test: shallow clone missing the pinned fixture commit; fetched it.
  • check:dual-build-cjs-loads: prerequisite, seven packages had no dist/; built them.

pnpm lint green.

Acceptance notes

  • GET /meta/app lands at 16.75 ms against the card's ≤15 ms. What remains is not diagnostics (1.3%): the profile puts about 4.4 ms in artifact resolution (lookupArtifactItem / foldExtenders) and about 7.8 ms in fixed per-request overhead ((program), routing, auth). Neither is on this claim's file surface. The maintainer relaxed both targets to these numbers (ruling 6408692677, letter A).
  • GET /meta/object lands at 15.6% in computeMetadataDiagnostics against the card's ≤5%. Warm reads run no zod parse at all; the remaining 9.3 ms is the content key itself, one JSON.stringify plus SHA-256 over 694 KB of definitions (about 6 ms in an isolated bench) and the shape walk. A content key costs about one serialization, so content keying cannot reach 5%. The card's other route does: Studio opts in to _diagnostics with a query switch, and plain /meta/object reads skip them. That changes /meta's parameters and the served body for every caller that does not opt in, and about a dozen _diagnostics readers in ui/ would have to send the switch: a contract change, outside this claim (claim 6407697525). Reported to the seat as a finding; not dispatched (ruling 6408692677).
  • Deviation: the before/after servers were started with pnpm --filter @objectstack/example-showcase dev -- --fresh -p 38425 rather than pnpm dev, because check-dev-prereqs refused pnpm dev with 45 unbuilt packages in this worktree. The same command and port served both sides.

Generated by Claude Code

…he execution face; diagnostics verdicts are memoized by content and schema

WIP for #25783.

Claude-Session: https://claude.ai/code/session_019d4JoyFgaZCRubmNWh91uZ
Co-authored-by: Claude <noreply@anthropic.com>
…object read and the diagnostics verdict memo

- rest: GET /meta/app (list and by name) prunes an unlistable object nav entry with zero object
  diagnostics, unscoped and environment-scoped; a protocol with no execution list face falls back
  to the served list with the same prune; GET /meta/object still serves _diagnostics.
- runtime: the dispatcher's /meta/app reads `object` on getMetaItemsForExecution, never on the
  served face, and falls back to getMetaItems without it.
- metadata-protocol: one parse per document per schema; a hit serves the bytes a fresh parse
  serves on the real object schema; changed content, a replaced schema, undefined vs missing,
  a hole vs null and NaN vs null each get their own verdict; unkeyable documents parse every
  time; callers get independent copies; 4096-entry LRU bound.
- rest-exposure-gate-execution-read: the recording proxy is registered per test, since verdicts
  are memoized per schema object and content.
- changeset: patch for the three packages.

Claude-Session: https://claude.ai/code/session_019d4JoyFgaZCRubmNWh91uZ
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 26, 2026
@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/metadata-protocol, @objectstack/rest, @objectstack/runtime, touching 19 documentable anchor(s).

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

  • content/docs/concepts/metadata-lifecycle.mdx (via getMetaItems (literal, a string literal in getMetaItemsForExecution))
  • content/docs/deployment/environment-variables.mdx (via getMetaItems (literal, a string literal in getMetaItemsForExecution))
  • content/docs/kernel/services-checklist.mdx (via getMetaItems (literal, a string literal in getMetaItemsForExecution))

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

  • content/docs/releases/implementation-status.mdx (via RestServer (symbol, a top-level class))
  • content/docs/releases/v12.mdx (via RestServer (symbol, a top-level class))
  • content/docs/releases/v16.mdx (via RestServer (symbol, a top-level class))
  • content/docs/releases/v17/17-3.mdx (via RestServer (symbol, a top-level class))
  • content/docs/releases/v17/17-5.mdx (via RestServer (symbol, a top-level class))

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
  • the SDK route bridge reached 54 of 206 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 — 41 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 493a6c4a5f5b83d953f19092279abdf07b6f4017 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 493a6c4a5f5b83d953f19092279abdf07b6f4017

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

…tstack/rest changeset

`RestProtocol` is exported from the rest barrel, and it gains the optional
`getMetaItemsForExecution?` member: a widening of the published type surface.
Measured on the built dist/index.d.ts before and after: rest keeps its export
list and differs by that one member line outside doc comments; the
metadata-protocol and runtime declarations are byte-identical, so their
changeset keeps `Clause-②: no`. All three stay `patch`: under
`Metadata-protocol: none` the level axis refuses a `minor` entry.

Claude-Session: https://claude.ai/code/session_019d4JoyFgaZCRubmNWh91uZ
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 22f886e1d2be780b9b159b9fbcdfa98b0b4bcb45
Local-runs: none

Face: declaration. @objectstack/rest's changeset declares Clause-②: yes (widening); nothing under packages/spec, no governed path, no narrowing line. Card #25783, claim 6407697525 with amendments 6407727649, 6408853730 and 6409013936; ruling A 6408692677.

① Derived judgments

  • RestProtocol gains getMetaItemsForExecution?: RIGHT, a widening. It is optional and typed as MetadataProtocol['getMetaItems'], so no protocol has to provide anything new. The dev measured rest's dist/index.d.ts at cb0a2fcf and at this head: the export list is unchanged (67 names), and outside doc comments that member is the only changed line. It is the list twin of getMetaItemForExecution?, which perf(rest): GET /data/OBJECT no longer computes metadata diagnostics; the exposure gate reads getMetaItemForExecution #24666 (6464df3462) declared the same way.
  • The gates' object list now reads the execution face: RIGHT, nothing served changes. getMetaItemsForExecution and getMetaItems share readFlattenedMetaItems and differ only in the final serving decorations (_diagnostics, per-type redaction). So the set of items the nav-servability gate, the batch exposure gate and the by-name list fallback judge is the same set. Their readers take only enable and serve none of the list, which is the same posture as the by-name twin from perf(rest): GET /data/OBJECT no longer computes metadata diagnostics; the exposure gate reads getMetaItemForExecution #24666. The protocol's own warning against serving the execution face from a door holds, because no answer carries those items. book and doc stay on the served face. The request shape (type, optional environmentId) is the one getMetaItems takes. The runtime dispatcher's MetaDomainProtocol is not a barrel export: an internal type, no published surface.
  • computeMetadataDiagnostics memo: RIGHT, no accept-set change. The verdict is keyed by the schema object (a WeakMap, so a replaced schema starts empty) and by a SHA-256 of a faithful encoding of the stripped document. A document with no faithful key is parsed every time. Each caller gets its own copy, and the memo is bounded LRU at 4096 per schema. The accepted and refused documents are unchanged; only repeat parses are skipped. The dev hashed the served bytes of six types before and after, and they are SHA-256-identical. @objectstack/metadata-protocol and @objectstack/runtime declarations are byte-identical at both ends, so Clause-②: no holds for them.

② Semver level

All three packages are patch, which is right. Metadata-protocol: none makes patch the level (AGENTS.md Post-Task step 3; ruling A on #25105), and check-changeset-no-major refuses a minor under none. rest's own changeset carries Clause-②: yes (widening) with the before/after reading. The metadata-protocol and runtime changeset carries Clause-②: no. Both declaration lines are in the PR body, which the seat edited through the relay.

③ Boundary flags

  • open_questions[0] (the card's ≤15 ms and ≤5% targets): answered by ruling A 6408692677. The targets are relaxed to the measured numbers, and Closes #25783 is right.
  • The declaration flag the seat raised on bb46886e (Clause-②: no should be yes (widening)) is fixed on this head, which changes changesets only (git diff bb46886e..22f886e1: 2 .changeset files).
  • Check-runs on this head: 53 total, 41 success, 12 skipped, 0 failed, 0 in progress; mergeable clean.

Implemented-by: claude/project-thread-mvtze8
Reviewed-by: session_01TFnDdepdwyDJeM2kJ4GtMy

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:server seat 2 (#20966) · seat session session_01TFnDdepdwyDJeM2kJ4GtMy · 2026-10-26T06:03Z · head 22f886e1d2be780b9b159b9fbcdfa98b0b4bcb45 · card #25783 (claim 6407697525, amendments 6407727649 / 6408853730 / 6409013936, report 6408550085, ruling A 6408692677)

  1. Shape: 10 files across metadata-protocol, rest and runtime (the execution-face list read for the gates' object list, and the diagnostics verdict memo), their tests, and two patch changesets. rest declares Clause-②: yes (widening), which owes a TIER_DEFAULT record: PASS 6409368646, on this head. No published schema, no governed path.
  2. Premise, measured: both faces share readFlattenedMetaItems, so the gates judge the same items without the serving decorations. The memo is keyed by schema object and content hash and changes no verdict. The dev hashed six types' served bytes and they are SHA-256-identical. Showcase numbers: /meta/app 73.25 → 16.75 ms, /meta/object 85.75 → 37 ms. Ruling A relaxed the card's targets to these numbers.
  3. Gates: check-runs on this head: 53 total, 41 success, 12 skipped, 0 failed, 0 in progress; mergeable clean.
  4. Accepted deviations: the PR body was edited by the seat through the relay (Closes, ruling references, declaration lines). Devs do not patch bodies.
  5. Out-of-scope findings: B (Studio _diagnostics opt-in) and C (the /meta/app residue) were not dispatched under ruling A.
  6. Landing: Tier S, not over 3000 lines. The maintainer's standing landing word for this seat thread is on record (2026-10-26 05:53Z), so it lands now through the relay. Closes #25783.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 26, 2026 06:04
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 26, 2026 06:04
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 26, 2026
Merged via the queue into main with commit 1ac2bb5 Oct 26, 2026
54 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-mvtze8 branch October 26, 2026 06:44
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/l tests tooling

Projects

None yet

2 participants