Repository navigation
perf(metadata-protocol,rest,runtime): /meta/app judges its navigation on the execution face; diagnostics verdicts are memoized by content and schema - #25941
Conversation
…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>
📓 Docs Drift CheckThis PR changes 3 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 5 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 41 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 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
|
…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>
Contract reviewServed-tier: Face: declaration. ① Derived judgments
② Semver levelAll three packages are ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对ACCEPT ·
Generated by Claude Code |
Requested by Elon · project thread
Closes #25783
Clause-②: yes (widening): the exported
RestProtocoltype of@objectstack/restgains one optional member,getMetaItemsForExecution?. Measured on the builtdist/index.d.tsatcb0a2fcfand at22f886e1: rest's export list is unchanged (67 names) and, outside doc comments, that member is the only changed line; the@objectstack/metadata-protocoland@objectstack/runtimedeclarations 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/specorui/moves. The one published type that changes isRestProtocol, by the optional member above (changeset.changeset/25783-rest-meta-app-execution-face.md); all three packages staypatch, asMetadata-protocol: nonerequires. The served bytes of six metadata types were hashed before and after and are identical (below).What changes
1.
GET /meta/appjudges 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'senableto prune the nav entries whose object refuseslist. It read them throughloadObjectItems→metaListSource→ the servedgetMetaItems, which stamps_diagnosticson every object, so each/meta/appread ran a full zodsafeParseof every object definition only to readenable. Measured at 63 of 73 ms CPU per read.RestServer.metaListSourcenow asksgetMetaItemsForExecutionforobjectwhen the protocol has it: the same list, without the serving decorations (the #24031 / #24535 precedent for/data). The dispatcher'smetaItemReadGateSources.listMetaItemsdoes the same (seat amendment 6407727649), so/meta/appthrough the${prefix}/*catch-all gets the same cut. A protocol without the execution face is askedgetMetaItemsas before.RestProtocolgains the optionalgetMetaItemsForExecutionmember;MetaDomainProtocolgains 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).computeMetadataDiagnosticskeeps one bounded LRU (4096 entries) per schema object, held in aWeakMap, keyed by a SHA-256 of the stripped document. The key encodes everything the parse can tell apart: plain JSON documents key onJSON.stringify; a document holdingundefined(a strict schema refuses an undeclared key whose value isundefined, and an array hole parses asundefined) keys on an encoding that spellsundefinedout; 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. AregisterMetadataTypeSchemareplacement 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.tsis 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/statover 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.cb0a2fcf)a8f08c97)computeMetadataDiagnosticsshare before → afterGET /api/v1/meta/appGET /api/v1/meta/objectGET /api/v1/meta/viewServed bytes, SHA-256 prefix of each
GET /api/v1/meta/:typebody, before vs after on the first read after boot and on a warm read:68ea5cb73ccd68ea5cb73ccd68ea5cb73ccd3fe564c0fe633fe564c0fe633fe564c0fe6371352ec4f6a671352ec4f6a671352ec4f6a6d35da2f62bcdd35da2f62bcdd35da2f62bcdef3c4c852a3def3c4c852a3def3c4c852a3dbf75d678c85dbf75d678c85dbf75d678c85dTests
packages/rest/src/meta-app-nav-servability-execution-read.test.ts(new): the real REST handlers over the realObjectStackProtocolImplementationandSchemaRegistry, with theobjectschema behind a recording proxy (therest-exposure-gate-execution-read.test.tsinstrument). 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/objectstill serves_diagnosticson every object. Oncb0a2fcf'srest-server.tsthe 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): throughHttpDispatcher.dispatch(),/meta/appand/meta/app/:namereadobjectongetMetaItemsForExecutionand never ongetMetaItems, with the same prune; without the execution face it falls back togetMetaItems. The two execution-face rows fail oncb0a2fcf'smeta.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 holdingundefined,nullvs an array hole,nullvsNaNeach get their own verdict in both orders; a function,-0, a 300-deep nesting and aDateparse every time; callers get independent copies; the 4096 LRU bound evicts the least recently used. On the realobjectschema, a memo hit serves byte-identicaldecorateMetadataItemoutput to a fresh parse, for a valid and an invalid document. Mutation check: with a naiveJSON.stringifykey 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-protocol264 files / 21,505 tests,@objectstack/rest328 / 6,730,@objectstack/runtime448 / 8,011, all green.typecheckgreen for the three packages (test layers included for rest and runtime).Gates
node scripts/pm/dispatch-gates.mjs --commandsderives 71 commands for this diff; all 71 ran with exit 0, and--ranreconciles 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 onconsole.warnwithout declaringOS_REST_LOG. The spies asserted nothing, so they are gone.check:engine-double-contract: the new rest test carried afindOnedouble 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 nodist/; built them.pnpm lintgreen.Acceptance notes
GET /meta/applands 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/objectlands at 15.6% incomputeMetadataDiagnosticsagainst the card's ≤5%. Warm reads run no zod parse at all; the remaining 9.3 ms is the content key itself, oneJSON.stringifyplus 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_diagnosticswith a query switch, and plain/meta/objectreads skip them. That changes/meta's parameters and the served body for every caller that does not opt in, and about a dozen_diagnosticsreaders inui/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).pnpm --filter @objectstack/example-showcase dev -- --fresh -p 38425rather thanpnpm dev, becausecheck-dev-prereqsrefusedpnpm devwith 45 unbuilt packages in this worktree. The same command and port served both sides.Generated by Claude Code