diff --git a/.changeset/strictness-ledger-recursive-coverage.md b/.changeset/strictness-ledger-recursive-coverage.md new file mode 100644 index 0000000000..dbf2f8e51b --- /dev/null +++ b/.changeset/strictness-ledger-recursive-coverage.md @@ -0,0 +1,13 @@ +--- +'@objectstack/spec': patch +--- + +Datasource unknown-key guidance no longer promises validation that does not happen, and the #4001 strictness ledger sees nested schema directories. + +Two related corrections to the #4001 unknown-key campaign, both about a check reporting more coverage than it had. + +**The `config` prescription was false.** When `DatasourceSchema` rejects a connection detail written at the top level, it prescribed: "Move it to `config: { host: … }`; the driver's own configSchema validates it there." Nothing validates it there — `DriverDefinitionSchema.configSchema` is a `z.record` that both bundled driver specs set to `{}`, and no consumer reads it. The message therefore took an author who had made a recoverable mistake at a place that now catches it and pointed them at a slot where the same mistake is silent again: `config: { hostname: … }` is dropped and the datasource connects on localhost. The guidance now names the per-driver shape to write against (`PostgresConfigSchema` / `MongoConfigSchema` / `MemoryConfigSchema`) instead of promising a gate. Enforcement is tracked in #4410. The same false claim is removed from `data/driver/mongo.zod.ts`, whose header advertised that the Platform validates `datasource.config` with it. + +No authorable key changed — this is error-message and documentation text only. + +**The ledger gate's coverage walk was one level deep.** `check:strictness-ledger` promises that every `*.zod.ts` with sites in a triaged directory carries a classification row. It listed each directory non-recursively, so `data/driver/` — three per-driver config files, nine authorable sites — was invisible to it while it printed "no undeclared schema files". The walk is now recursive (nested files declare as `driver/postgres.zod.ts`), those three files are classified in the ledger, and `scripts/strictness-ledger.test.ts` pins the recursion — necessary because with the rows in place the gate itself passes either way and cannot catch its own regression. diff --git a/content/docs/releases/v17.mdx b/content/docs/releases/v17.mdx index e79efdc0be..a555a889c2 100644 --- a/content/docs/releases/v17.mdx +++ b/content/docs/releases/v17.mdx @@ -412,8 +412,12 @@ schema to the two highest-risk authorable surfaces, per the triage in - **Datasources** — `DatasourceSchema` with its `pool` / `healthCheck` / `ssl` / `retryPolicy` blocks, the ADR-0015 `external` federation settings and their `validation` policy, `DatasourceCapabilities`, and `DriverDefinitionSchema`. - `config` and `readReplicas` stay **open** records: their shape is per-driver - and the driver's own `configSchema` validates them. That openness is why the + `config` and `readReplicas` stay **open** records: their shape is per-driver. + Nothing validates *inside* them — an earlier version of this note said the + driver's own `configSchema` did, which was wrong; the per-driver schemas exist + (`PostgresConfigSchema` and siblings) but nothing parses `config` against + them, tracked as #4410. So a misspelling one level *down* is still silent + today. That openness is why the top level had to close — a connection key written one level too high (`host` next to `driver` instead of inside `config`) was stripped, and the datasource then connected on driver defaults rather than failing. Those keys now diff --git a/docs/audits/2026-07-unknown-key-strictness-ledger.md b/docs/audits/2026-07-unknown-key-strictness-ledger.md index 2654d2afb5..4056de004c 100644 --- a/docs/audits/2026-07-unknown-key-strictness-ledger.md +++ b/docs/audits/2026-07-unknown-key-strictness-ledger.md @@ -101,6 +101,18 @@ dropped at parse, and nothing failed. now aliases to `expanded`. Note where this one was found: not in a tenant project, but in first-party platform metadata that had been shipping for releases. +7. **This campaign's own fix signposted the way into the failure mode it + exists to kill.** The strict rejection on a misplaced `host` prescribed: + "Move it to `config: { host: … }`; the driver's own configSchema validates + it there." False twice over — `DriverDefinitionSchema.configSchema` is a + `z.record` that both bundled driver specs set to `{}`, and nothing in the + repo reads it. So an author who made a *recoverable* mistake at a place that + now catches it was directed, with the platform's authority, at a slot where + the same mistake is silent again: `config: { hostname: … }` is stripped and + the datasource connects on localhost — #4001's original bug verbatim, one + level down. Corrected to name the per-driver shape instead of promising a + gate; enforcement is #4410. **A wrong instruction is worse than none**, and + worst for an AI author, whose only signal is whether the parse complained. This is the empirical argument for the ratchet: the inference "no metadata in the repo carries unknown keys" was **false three times over**, and only the @@ -146,7 +158,7 @@ tightening (the #4001 "sharing-rule lesson": candidates, not verdicts). | `notification.zod.ts` / `offline.zod.ts` / `report.zod.ts` | 3 ea | authorable (p) | | | `sharing.zod.ts` | 2 | authorable (p) | public-sharing config | -### `data/` — 149 sites +### `data/` — 158 sites | File | Sites | Class | Note | |---|---|---|---| @@ -157,7 +169,8 @@ tightening (the #4001 "sharing-rule lesson": candidates, not verdicts). | `field.zod.ts` | 11 | authorable | partially strict | | `filter.zod.ts` / `query.zod.ts` | 11+5 | open | query dialect — user data flows through; validated semantically elsewhere. `query.zod.ts` dropped one site in #4196: `FieldNodeSchema`'s nested-select object form was declared-but-inert and narrowed to `z.string()`, so the union's second member is gone. Four more left in #4286 with the `joins`/`windowFunctions` removals: `JoinNodeBaseSchema`, `WindowFunctionNodeSchema`, and `WindowSpecSchema`'s two blocks (outer + `frame`) were deleted with their clusters. Class unchanged | | `driver-nosql.zod.ts` / `driver.zod.ts` / `driver-sql.zod.ts` | 10+9+2 | wire | driver capability contracts | -| `datasource.zod.ts` | 9 | authorable | **strict as of #4001 data step** — all 9: `DatasourceSchema` (+ `pool` / `healthCheck` / `ssl` / `retryPolicy`), `ExternalDatasourceSettingsSchema` (+ `validation`), `DatasourceCapabilities`, `DriverDefinitionSchema`. `config` + `readReplicas` stay `z.record` by construction (per-driver shapes; the driver's own `configSchema` validates them) — which is precisely why the top level had to close: a connection key written one level too high was stripped, and the datasource then connected on driver defaults instead of failing | +| `datasource.zod.ts` | 9 | authorable | **strict as of #4001 data step** — all 9: `DatasourceSchema` (+ `pool` / `healthCheck` / `ssl` / `retryPolicy`), `ExternalDatasourceSettingsSchema` (+ `validation`), `DatasourceCapabilities`, `DriverDefinitionSchema`. `config` + `readReplicas` stay `z.record` by construction (per-driver shapes — see the `driver/` row below). This row used to add "the driver's own `configSchema` validates them"; **it does not, and never did** — corrected, and the gap is #4410. Which is precisely why the top level had to close: a connection key written one level too high was stripped, and the datasource then connected on driver defaults instead of failing | +| `driver/memory.zod.ts` / `driver/mongo.zod.ts` / `driver/postgres.zod.ts` | 6+1+2 | authorable | The per-driver shapes for the `config` slot — what an author actually writes under `datasource.config` (`host`, `port`, `filename`, pool sizes). **Undeclared here until the coverage walk went recursive** (see below): a subdirectory was invisible to the gate, so these nine sites sat outside the map while the map reported full coverage. Authorable by the rule, but they are **contract-only exports today** — nothing parses `datasource.config` against them and both `*DriverSpec.configSchema` literals are `{}` (#4410). Strictness here would therefore enforce nothing; this row is blocked on #4410 giving it a parse site, not on a verification pass | | `analytics.zod.ts` | 8 | mixed (p) | | | `document.zod.ts` | 8 | wire (p) | | | `hook.zod.ts` / `hook-body.zod.ts` | 6+2 | mixed | **strict as of #4001 data step** for the AUTHORING shapes: `HookSchema` (+ `retryPolicy`) and both body branches (`ExpressionBodySchema` / `ScriptBodySchema`). `HookContextSchema` and its `session` / `provenance` / `user` blocks are the RUNTIME shape the engine hands a handler — they stay tolerant, and must: strictness there would make an engine-internal enrichment (as `provenance` was in #3712) a breaking change for anyone parsing a context they were given. The file's old blanket `authorable (p)` was too wide — verification split it | @@ -220,6 +233,32 @@ tightening (the #4001 "sharing-rule lesson": candidates, not verdicts). strict close-out on what it actually reports — which is the whole point of having built it. Nothing more to do here until there is field data. + **This wait has a decision point, deliberately.** "Wait for field data" with + no way to tell when it has arrived is how a ratchet stops without anyone + choosing to stop it — and this file would go on describing an in-flight + campaign either way. So the wait is discharged by an answerable question, not + by a date: *has `lintUnknownAuthoringKeys` reported an unknown key on any + surface outside this repo yet?* Three outcomes, each with a next action: + - **Findings exist** → they are the close-out worklist. Tighten the shapes + they name first; that is the evidence the whole layer was built to produce. + - **Zero findings, and the layer is reaching real authors** → the tail is + cheaper than feared and the remaining directories can be batched by class + rather than one shape at a time. + - **Zero findings because nothing is reporting back** → then the layer is not + instrumented, and *that* is the next task, not more strictness. This is the + outcome to actually check for: it is indistinguishable from success at a + glance, which is this campaign's own subject matter. + + Whoever reads this next: answer the question and record the answer here, even + if the answer is "still nothing". A wait that is never re-examined is + indistinguishable from an abandoned one. + +2. `studio/` is the largest untouched authorable block — 27 sites, **0 strict**, + and all three files still carry a provisional `(p)` from the original triage. + Not blocked on field data (Studio-written JSON is our own producer, so the + downstream risk is the lowest on the board); it is simply unstarted. If the + step-1 question comes back "nothing is reporting", start here instead. + Done in step 2: `security/rls.zod.ts` + `security/sharing.zod.ts` strict; `PositionSchema` strict with the protection envelope declared (closing the known sibling gap below). @@ -303,9 +342,11 @@ checkable, so this map cannot go stale in silence again: matches means schemas were added or removed under a `Class` verdict nobody re-examined. Touching a file forces you back through this ledger. - **Coverage.** Every `*.zod.ts` in a triaged directory that HAS sites must have - a row. A new one is undeclared surface. Zero-site files (pure enum/token - modules like `data/date-macros.zod.ts`) are skipped — there is nothing to - classify — and become reportable the day they grow their first `z.object(`. + a row. A new one is undeclared surface. The walk is **recursive**; nested files + are declared by their path relative to the section directory + (`driver/postgres.zod.ts`). Zero-site files (pure enum/token modules like + `data/date-macros.zod.ts`) are skipped — there is nothing to classify — and + become reportable the day they grow their first `z.object(`. - **Section totals**, and that any row claiming "strict as of" names a file that really contains `.strict()`. @@ -336,5 +377,23 @@ precisely the intended behaviour: the file arrived, so someone had to classify it. It is now a row. Every existing count survived that merge unchanged, so the failure was exactly as narrow as it should have been. +**And then the gate turned out to have the ledger's own disease.** Its coverage +walk listed each triaged directory exactly one level deep, so `data/driver/` — +three per-driver connection-config files, nine sites — was invisible to the check +whose entire promise is "no undeclared surface". The gate printed *"no undeclared +schema files"* while nine authorable sites sat outside the map. Fixed by making +the walk recursive; the three files are now a row. + +Read that next to this file's own opening argument — *a map that drifts is worse +than no map, because it is followed*. The same asymmetry applies one level up, +and harder: **a gate that under-reports is worse than no gate, because it +converts "I should classify this" into "it is already classified."** No gate +leaves a reader suspicious; a green gate retires their suspicion. That is the +identical shape to the silent strip this whole campaign is about — a success +signal covering an omission — reproduced in the instrument built to detect it. +So when a check claims coverage, prove it sees something it is supposed to see +before trusting the green: this one was verified by watching it go red on +`data/driver/` and green again only once the rows existed. + Long tail stays gated on a verification pass per shape — never a one-shot -"make all ~453 sites strict" (ADR-0054 ratchet; #4001's own recommendation). +"make all ~500 sites strict" (ADR-0054 ratchet; #4001's own recommendation). diff --git a/packages/spec/scripts/check-strictness-ledger.mts b/packages/spec/scripts/check-strictness-ledger.mts index ba9cf0f7f1..0c256a7d41 100644 --- a/packages/spec/scripts/check-strictness-ledger.mts +++ b/packages/spec/scripts/check-strictness-ledger.mts @@ -23,10 +23,14 @@ // touching a file forces you back through the ledger. // 2. Coverage. Every `*.zod.ts` under a triaged directory that HAS `z.object(` sites // must appear in that directory's table. A new one is undeclared surface — -// exactly what the ledger exists to prevent. Files with zero sites (pure enum / -// token modules like `data/date-macros.zod.ts`) are skipped: the ledger classifies -// sites, and they have none to classify. This is not a hole — the day such a file -// grows its first `z.object(` it becomes undeclared and this gate says so. +// exactly what the ledger exists to prevent. The walk is RECURSIVE; nested files +// are declared by their path relative to the section directory +// (`driver/postgres.zod.ts`). It was not recursive at first, and `data/driver/` +// sat undeclared behind that — see the ledger's note on it. Files with zero sites +// (pure enum / token modules like `data/date-macros.zod.ts`) are skipped: the +// ledger classifies sites, and they have none to classify. This is not a hole — +// the day such a file grows its first `z.object(` it becomes undeclared and this +// gate says so. // 3. Section totals. `### \`ui/\` — 192 sites` must equal the sum of its rows. // Cheap, and it catches a row edited without updating the header. // 4. Strictness claims. A row whose note says "strict as of" must name a file that @@ -48,6 +52,8 @@ import fs from 'node:fs'; import path from 'node:path'; import url from 'node:url'; +import { countSites, listSchemaFiles } from './lib/strictness-ledger'; + const HERE = path.dirname(url.fileURLToPath(import.meta.url)); const SPEC = path.resolve(HERE, '..'); const REPO = path.resolve(SPEC, '../..'); @@ -79,11 +85,6 @@ function parseCounts(cell: string, fileCount: number): number[] | null { return nums.length === fileCount ? nums : null; } -/** `z.object(` occurrences — the ledger's own stated counting method. */ -function countSites(file: string): number { - return (fs.readFileSync(file, 'utf-8').match(/z\.object\(/g) ?? []).length; -} - const md = fs.readFileSync(LEDGER, 'utf-8').split('\n'); const rows: Row[] = []; const sectionTotals = new Map(); @@ -169,7 +170,10 @@ for (const row of rows) { for (const [d, declared] of declaredByDir) { const dirPath = path.join(SRC, d); if (!fs.existsSync(dirPath)) continue; - const onDisk = fs.readdirSync(dirPath).filter((f) => f.endsWith('.zod.ts')); + // Recursive — see lib/strictness-ledger.ts for why that is load-bearing. + // Nested files are declared by their path relative to the section directory + // (`driver/postgres.zod.ts`), which the row parser already accepts. + const onDisk = listSchemaFiles(dirPath); // Zero-site files carry nothing to classify (see the header note). They become // reportable the moment they grow a `z.object(`. const missing = onDisk.filter((f) => !declared.has(f) && countSites(path.join(dirPath, f)) > 0); diff --git a/packages/spec/scripts/lib/strictness-ledger.ts b/packages/spec/scripts/lib/strictness-ledger.ts new file mode 100644 index 0000000000..927d25a76b --- /dev/null +++ b/packages/spec/scripts/lib/strictness-ledger.ts @@ -0,0 +1,39 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Shared pieces of the #4001 strictness-ledger gate + * (`../check-strictness-ledger.mts`), extracted so the gate and its regression + * test cannot drift apart on the one property that already broke silently: + * whether the coverage walk descends into subdirectories. + */ + +import fs from 'node:fs'; +import path from 'node:path'; + +/** `z.object(` occurrences — the ledger's own stated counting method. */ +export function countSites(file: string): number { + return (fs.readFileSync(file, 'utf-8').match(/z\.object\(/g) ?? []).length; +} + +/** + * Every `*.zod.ts` under `dir`, **recursively**, as `/`-separated paths relative + * to `dir` (so a nested file reads `driver/postgres.zod.ts` — exactly how the + * ledger declares it). + * + * The recursion is the whole point of this function existing. The gate's first + * version listed each triaged directory one level deep, which made + * `data/driver/` — three per-driver connection-config files, nine authorable + * sites — invisible to the check whose entire promise is "no undeclared + * surface". It printed "no undeclared schema files" and was believed. + * + * A gate that under-reports is worse than no gate: it converts "I should + * classify this" into "it is already classified". Keep the walk recursive, and + * see `strictness-ledger.test.ts`, which fails if it stops being. + */ +export function listSchemaFiles(dir: string): string[] { + return fs + .readdirSync(dir, { recursive: true }) + .map((f) => String(f).split(path.sep).join('/')) + .filter((f) => f.endsWith('.zod.ts')) + .sort(); +} diff --git a/packages/spec/scripts/strictness-ledger.test.ts b/packages/spec/scripts/strictness-ledger.test.ts new file mode 100644 index 0000000000..0e9f6fe70d --- /dev/null +++ b/packages/spec/scripts/strictness-ledger.test.ts @@ -0,0 +1,73 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Regression cover for the #4001 strictness-ledger gate's coverage walk. + * + * The gate protects the ledger from going stale. Nothing protected the gate, + * and it shipped with a one-level-deep directory listing that hid + * `data/driver/` — nine authorable sites — while reporting "no undeclared + * schema files". The failure was invisible for the same reason the silent key + * strip this whole campaign is about is invisible: a success signal covering an + * omission. + * + * So these tests assert the gate can see the thing it once could not, rather + * than only that it passes today. A green check that has never been shown to go + * red proves nothing. + * + * MEASURED, and the reason this file is not redundant with the gate: with the + * `data/driver/` rows now present in the ledger, reverting the walk to + * non-recursive leaves `check:strictness-ledger` **passing** — a one-level walk + * finds nothing missing once nothing nested is missing. The gate cannot detect + * its own regression. These tests can, and did (`descends into subdirectories` + * is the one that fails). Deleting them silently restores the blind spot. + */ + +import fs from 'node:fs'; +import path from 'node:path'; +import url from 'node:url'; +import { describe, expect, it } from 'vitest'; + +import { countSites, listSchemaFiles } from './lib/strictness-ledger'; + +const HERE = path.dirname(url.fileURLToPath(import.meta.url)); +const SPEC = path.resolve(HERE, '..'); +const SRC = path.join(SPEC, 'src'); +const LEDGER = path.resolve(SPEC, '../../docs/audits/2026-07-unknown-key-strictness-ledger.md'); + +/** The directories the ledger triages file-by-file. */ +const TRIAGED = ['ui', 'data', 'automation', 'security', 'studio'] as const; + +describe('strictness-ledger coverage walk', () => { + it('descends into subdirectories', () => { + const files = listSchemaFiles(path.join(SRC, 'data')); + + // The exact surface the non-recursive walk hid. If this ever fails because + // the files moved, re-point it at whatever nested schema exists — do not + // delete it, or the blind spot comes back unobserved. + expect(files).toContain('driver/postgres.zod.ts'); + expect(files).toContain('driver/mongo.zod.ts'); + expect(files).toContain('driver/memory.zod.ts'); + }); + + it('returns paths relative to the directory, as the ledger declares them', () => { + for (const file of listSchemaFiles(path.join(SRC, 'data'))) { + expect(file.startsWith('/')).toBe(false); + expect(file).not.toContain('\\'); + } + }); + + it.each(TRIAGED)('declares every sited schema file under %s/', (dir) => { + const ledger = fs.readFileSync(LEDGER, 'utf-8'); + const dirPath = path.join(SRC, dir); + + const undeclared = listSchemaFiles(dirPath) + .filter((f) => countSites(path.join(dirPath, f)) > 0) + // Backticked exactly as the triage tables write it. + .filter((f) => !ledger.includes(`\`${f}\``)); + + // A second, independent statement of the gate's coverage rule — deliberately + // not sharing the gate's table parser, so a bug in that parser cannot make + // both agree on a wrong answer. + expect(undeclared).toEqual([]); + }); +}); diff --git a/packages/spec/src/data/datasource.zod.ts b/packages/spec/src/data/datasource.zod.ts index a5015edbc1..f21e5c4018 100644 --- a/packages/spec/src/data/datasource.zod.ts +++ b/packages/spec/src/data/datasource.zod.ts @@ -20,8 +20,9 @@ import { strictUnknownKeyError } from '../shared/suggestions.zod'; * * TWO ESCAPE HATCHES STAY OPEN, and must: * - `config` is per-driver by construction (a sqlite `filename` and a - * postgres `host`/`port` share no shape), so it stays `z.record`. The - * driver's own `configSchema` is what validates it. + * postgres `host`/`port` share no shape), so it stays `z.record`. + * NOTHING VALIDATES INSIDE IT TODAY — see {@link belongsInConfig}, which + * used to claim otherwise. Tracked as #4410. * - `readReplicas` carries the same per-driver config objects. * * That openness is exactly why the TOP level had to close. Before this, a @@ -63,10 +64,31 @@ const SSL_KEYS = ['enabled', 'rejectUnauthorized', 'ca', 'cert', 'key'] as const /** Keys the datasource `retryPolicy` block declares (drift-guarded by datasource.test.ts). */ const DATASOURCE_RETRY_POLICY_KEYS = ['maxRetries', 'baseDelayMs', 'maxDelayMs', 'backoffMultiplier'] as const; -/** A connection detail written one level too high — it belongs inside `config`. */ +/** + * A connection detail written one level too high — it belongs inside `config`. + * + * This prescription stops at *where to put it* and deliberately does NOT promise + * that the move gets validated. It used to: the sentence read "the driver's own + * configSchema validates it there", and that was false twice over — + * {@link DriverDefinitionSchema}'s `configSchema` is a `z.record` that both + * bundled driver specs set to `{}`, and nothing in this repo reads it (#4410). + * + * Which made this the worst line in the module: it took an author who had made a + * recoverable mistake at a place that now catches it, and pointed them — with the + * platform's authority — at a slot where the same mistake is silent again. + * `config: { hostname: … }` is stripped in silence and the datasource connects on + * localhost, which is #4001's original bug verbatim, one level down. A wrong + * instruction is worse than none, and worst of all for an AI author, whose only + * check on "did that work?" is whether the parse complained. + * + * Naming the per-driver schema is the honest form: it is the shape to write + * against, and a reader can check themselves against it even while nothing + * enforces it. Restore a validation claim here only when #4410 makes one true. + */ const belongsInConfig = (key: string) => `\`${key}\` is a driver connection detail — it belongs inside \`config\`, not at the top ` - + `level. Move it to \`config: { ${key}: … }\`; the driver's own configSchema validates it there.`; + + `level. Move it to \`config: { ${key}: … }\`, matching your driver's config shape ` + + `(\`PostgresConfigSchema\` / \`MongoConfigSchema\` / \`MemoryConfigSchema\` in \`data/driver/\`).`; const driverDefinitionUnknownKeyError = strictUnknownKeyError({ surface: 'this driver definition', diff --git a/packages/spec/src/data/driver/mongo.zod.ts b/packages/spec/src/data/driver/mongo.zod.ts index d2971f7775..c41c6a00a6 100644 --- a/packages/spec/src/data/driver/mongo.zod.ts +++ b/packages/spec/src/data/driver/mongo.zod.ts @@ -5,9 +5,14 @@ import { DriverDefinitionSchema } from '../datasource.zod'; /** * MongoDB Standard Driver Protocol - * - * Defines the strict schema for MongoDB connection and capabilities. - * This is used by the Platform to validate `datasource.config` when `driver: 'mongo'`. + * + * Describes the MongoDB connection settings and capabilities. + * + * CONTRACT ONLY — nothing parses `datasource.config` against this. This block + * used to claim it was "used by the Platform to validate `datasource.config` + * when `driver: 'mongo'`", which was never true: the config slot is a `z.record` + * and this schema has no consumer (#4410). It is the shape to author against, + * not a gate that runs. Say "validates" here again only once #4410 lands. */ // ========================================================================== @@ -64,7 +69,10 @@ export const MongoDriverSpec = DriverDefinitionSchema.parse({ label: 'MongoDB', description: 'Official MongoDB Driver for ObjectStack. Supports rich queries, aggregation, and atomic updates.', icon: 'database', - configSchema: {}, // Will be populated with JSON Schema version of MongoConfigSchema at runtime + // Empty, and nothing fills it. This comment used to promise the field would be + // "populated with a JSON Schema version of MongoConfigSchema at runtime" — no + // such code exists here or in any consumer (#4410). + configSchema: {}, capabilities: { transactions: true, // Query