census(#14423 step 1): loadMany consumers, per-loader keyed-read cost, C3/C4 re-measured - #15033
Conversation
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…admany-consumer-census
Census-only, per maintainer ruling (issue #14423 comment 5528592646): enumerates every loadMany consumer, measures each shipped loader's listNames()/loadManyKeyed() availability and cost, re-measures C3/C4 from the pinned action-governance-scope-divergence fixture against a keyed-read shape, and establishes the unboundDeclarations BEFORE count. No shipped package behaviour changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…admany-consumer-census
Independent review — PASS; ready and armed. Two nits recorded, neither blocking.An independent reviewer (not the author) re-derived every load-bearing claim rather than confirming it. All 35 check runs on head The scope fence holds completely. ⭐ The two negatives — the reason this census matters — both reproduceC3 is not closed by keying, and the mechanism is a missing C4 is not closed either, for a different reason — it is upstream of any read method. ⇒ The ruling's own named default — move the audit to Everything else re-measured, not acceptedThree of the four probes reproduced identically (the C3/C4 probe byte-for-byte; Gates: 25 commands derived off merge base Two nits, recorded rather than fixed1 — the FilesystemLoader timing band is wider than the document quotes, and this one must travel to step 2. The doc records 0.99–1.88 over five runs of 50 items; the reviewer's own five runs on the same contended box gave 0.55, 1.10, 1.26, 1.31, 1.66 — dipping below 1.0, i.e. the keyed path measured faster at least once. That does not undermine the qualitative reading (sub-2×, local-I/O noise, categorically unlike 2 — a stale ref in the document header. Landing. Ready, auto-merge armed Generated by Claude Code |
Part of #14423
Step 1 (census) of the maintainer ruling on #14423, comment
5528592646.
Measurement only — no shipped package behaviour changes,
skip-changesetapplies.The identity fix itself (step 2) is a separate, later PR that reads this one.
The ruling this executes (verbatim)
Full detail, tables, and re-runnable measurement scripts:
docs/audits/2026-09-loadmany-consumer-census.md.Answer in one line
Keying alone (
listNames()+ by-nameload/loadDiagnosed) closes C2 and C6 but doesnot close C3 or C4, confirmed by running the real production code, not by inference.
Zero of today's 4 production
loadManyconsumers already key by the store key, so aloadManyreturn-shape change is the more invasive of the two shapes the ruling names.Recommendation for step 2: fix the audit's own declaration-discovery strategy (probe
already-known registered-handler names directly, mirroring what the router and the
registry rung already do) rather than widen
loadMany's published return shape — this isthe only shape measured here that also closes C3 and C4, and it touches none of the four
production consumers. Clause-②: no, provisionally, under that recommendation.
unboundDeclarationsBEFORE count: 0, reproducibly, for the population the fix canmove.
1. Every
loadManyconsumer (full table in the doc)4 production consumers of the published
IMetadataService.loadMany(), across 2 files —all 4 depend on
body.namefor identity, none already keys by the store key:packages/objectql/src/plugin.ts:2193(resyncAuthoredHooksNow) — keys a Map by thehook's own name; a nameless hook is silently excluded.
packages/objectql/src/plugin.ts:2586(resyncAuthoredActionsNow) — keys byobject-owner-plus-name; a nameless action is silently excluded.
packages/objectql/src/plugin.ts:2675(loadMetadataFromService, boot sync) — fallsback to the body's own
namefield wheneveridis absent; a nameless, id-less itemregisters under the key
undefined.packages/metadata/src/plugin.ts:1274(MetadataPlugin._loadFromFileSystem, bootload) — the sharpest of the four: a nameless item is never registered at all (an
ifguard on the body's own name skips the whole registration).Plus 1 internal, package-private fallback (
metadata-manager.ts:1131,admitLoaderItems) that already prefers the loader-level keyed method and only fallsback to
body.namekeying for a loader that cannot produce keys — precedent, in thesame file, for the direction step 2 is choosing between.
2. Per shipped loader — keyed-read availability and measured cost
listNamesloadManyKeyed{"find":1,"findOne":5}for 5 items) vs loadMany's constant 1loadManyplus a map, i.e. already as expensive as loadManyDatabaseLoader.loadManyKeyed()costing nothing extra overloadMany()(same sharedquery and cache) is the strongest cost argument in this census, and it argues for
whichever step-2 shape can reach that method rather than for the listNames-plus-N-loads
shape the ruling names as the default candidate.
3. C3 and C4 re-measured against the keyed shape
Reused the exact doubles the pinned fixture (
packages/runtime/src/action-governance-scope-divergence.test.ts,PR #14741, unmodified this round) uses, driven against the real, built
@objectstack/metadataand
@objectstack/corein a new, separate probe script — not a change to the fixture.MetadataManager.listNames()has no per-loader try/catch (unlike
loadMany()/list()), so it throws under the samedouble the fixture uses; the audit's own outer catch swallows it, landing at the exact
same net outcome as today (
standaloneset becomes empty either way). Swapping the readmethod relocates where the failure is swallowed; it does not change whether it is
swallowed. The mechanism that would close it, measured in the same script: probe the
already-known registered-handler name directly (
loadDiagnosed), skipping enumerationentirely — this never calls the failing list path at all.
ctx.getService('metadata')throws before any read method —loadMany,listNames,or
load/loadDiagnosed— is ever invoked. No choice of read method reaches this; thefix is a different accessor (reaching the scoped instance somehow), a materially larger
change than the identity fix, and (per the prior measurement round) a shape no in-repo
composition of
metadataactually registers today.4.
unboundDeclarationsBEFORE count0, reproducibly (
scripts/audits/14423-unbound-declarations-before-count.mjs) — astructural fact, not a sampling accident:
collectEngineActionDeclarationsfilters outany standalone action whose body has no name BEFORE
reconcileActionRegistrations(whichcomputes
unboundDeclarations) ever sees it, so a nameless orphan cannot reach thatwarning today under any input. A 5-nameless-plus-1-named control run reports exactly 1
(the named one), confirming the harness finds orphans in general and specifically drops
only the nameless ones. Any nameless orphan the identity fix newly admits is therefore
NET NEW to this warning's population, not a replacement.
5. Clause-② determination
No, under the recommendation in section 1 above — an audit-side fix using already
published
IMetadataServicemembers (load/loadDiagnosed) adds no exported symbol andno new key on any published payload. Reverses to yes, per the mechanical floor, if
step 2 instead widens
loadMany's published return shape or adds a new publicloadManyKeyedmember toIMetadataService.Out-of-scope finding, reported not filed
RemoteLoader.list()maps a nameless body's own name field into the returned name list,so a nameless remote item pollutes
MetadataManager.listNames()'s result with the valueundefined rather than being dropped or keyed cleanly. Unrelated to #14423 (this loader
cannot produce keys either way) and noticed incidentally while building the loader table
above. A targeted duplicate-search hit the fleet's shared rate limit twice in a row, so
per the no-blind-filing rule this is reported here for the PM to file rather than risked
as a duplicate.
Gates (25 commands derived by
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, no path arguments, re-derived on the final commit0d536d67e)24 of 25 green; the 25th (
node scripts/check-test-completeness.mjs) exits 3 by its owndesign when run locally with no saved
turbo run testlog — its own text says this isNOT MEASURED, not a finding, and that CI supplies the log it needs.
node scripts/pm/check-governed-merges.mjs --teston the final 5-file list: not governed, ordinary queue landing applies.
Full logs captured in the scratchpad during the run; happy to paste any one in full on
request.
🤖 Generated with Claude Code
https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
Generated by Claude Code