Skip to content

Commit eabffe3

Browse files
committed
docs(audits): #14423 loadMany consumer census (step 1)
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
1 parent 1e057bc commit eabffe3

1 file changed

Lines changed: 1 addition & 1 deletion

File tree

docs/audits/2026-09-loadmany-consumer-census.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ or by timing the real implementation. Scripts are committed under `scripts/audit
101101

102102
| Loader | `listNames` (via `list()`) | `loadManyKeyed` | Cost of `listNames()` + N × by-name `load`/`loadDiagnosed`, vs `loadMany()` |
103103
|---|---|---|---|
104-
| `FilesystemLoader` (`packages/metadata/src/loaders/filesystem-loader.ts`) | Yes, `:366` — a SEPARATE glob, names only, no file bodies read. | Yes, `:220` — shares the same `loadManyEntries()` walk as `loadMany` (`:159-164`), so the two never disagree on which bodies exist. | **Measured, not guessed: ratio 0.99 (statistically noise) for 50 items** (`scripts/audits/14423-filesystemloader-cost-probe.mjs`). One extra name-only glob, then per name up to `resolvableExtensions().length` (today: `json`/`yaml`/`yml`/`ts`/`js`, so ≤5) `fs.access` stats via `findFile()` (`:568-584`) plus one `fs.readFile` — all **local** I/O, no network round trip, and the measurement shows it does not move wall time. |
104+
| `FilesystemLoader` (`packages/metadata/src/loaders/filesystem-loader.ts`) | Yes, `:366` — a SEPARATE glob, names only, no file bodies read. | Yes, `:220` — shares the same `loadManyEntries()` walk as `loadMany` (`:159-164`), so the two never disagree on which bodies exist. | **Measured, not guessed: ratio 0.99-1.88 across 5 repeated runs of 50 items** (`scripts/audits/14423-filesystemloader-cost-probe.mjs`) — sub-2x, millisecond-scale, dominated by shared-box scheduling noise (this container runs several parallel agents), qualitatively different from `DatabaseLoader`'s exactly-reproducible, linearly-growing N+1 below. One extra name-only glob, then per name up to `resolvableExtensions().length` (today: `json`/`yaml`/`yml`/`ts`/`js`, so ≤5) `fs.access` stats via `findFile()` (`:568-584`) plus one `fs.readFile` — all **local** I/O, no network round trip. |
105105
| `DatabaseLoader` (`packages/metadata/src/loaders/database-loader.ts`) | Yes, `:1062` — its OWN separate query (`_find(..., {fields:['name']})`, its own `listCache`). | Yes, `:988` — shares `readTypeRows()` (`:936`) **and its cache** (`loadManyCache`) with `loadMany` (`:970`): **empirically confirmed 1 query for `loadMany` alone, 1 query for `loadManyKeyed` alone — identical** (`scripts/audits/14423-databaseloader-cost-probe.mjs`). | **Measured: real N+1.** `listNames()` (1 `find`) + N × by-name `load`/`loadDiagnosed` (each its own `findOne`) = **1 + N round trips**, vs `loadMany`/`loadManyKeyed`'s constant **1**, for the SAME 5-item fixture: `{"find":1,"findOne":5}`. This is the loader where "listNames + per-name load" is genuinely, measurably costlier than the alternative — `loadManyKeyed` reuses the exact query `loadMany` already issues, at zero extra cost, where "listNames + load" does not. |
106106
| `MemoryLoader` (`packages/metadata/src/loaders/memory-loader.ts`) | Yes, `:97``Map.keys()`. | Yes, `:73``Map` iteration, same backing store as `loadMany` (`:55-62`). | **Zero, by construction** — every method here is a synchronous in-memory `Map` operation; there is no I/O of any kind to save or spend. |
107107
| `RemoteLoader` (`packages/metadata/src/loaders/remote-loader.ts`) | Yes, `:113` — but `list(type)` is implemented as `(await this.loadMany(type)).map(i => i.name)`, i.e. it is **already exactly as expensive as `loadMany`** (one full HTTP `GET` of every body), plus mapping. | **No** — the only shipped loader without it (matches the loader-interface's own docblock: *"a loader that cannot produce keys — `RemoteLoader`, whose wire format carries bodies only — simply does not declare it"*, `loader-interface.ts:85-86`). | **Measured by reading the two implementations together, not timed (no live remote fixture in-repo): worst of the four.** `listNames()` costs one full `loadMany`-equivalent fetch (via its own `list()`), THEN a `load()` HTTP `GET` per name — **1 + N HTTP round trips**, strictly worse than plain `loadMany`'s **1**. And because `list()` reads `i.name` off the body, a nameless item pollutes the names array with `undefined` rather than being cleanly dropped or keyed — see the out-of-scope finding below. |

0 commit comments

Comments
 (0)