From 0acd55c3c22b4534943898136a7c0c63a01d8fa8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 18:44:22 +0000 Subject: [PATCH 1/2] feat(spec): type the engine-registration road into datasource introspection (#11493) IDataDriver gains optional introspectSchema?(): Promise; IDataEngine gains optional introspectDatasource?(datasource): Promise. ObjectQL.introspectDatasource() tightens Promise to the spec type and drops its as-any driver probe (compiled JS unchanged). service-datasource's plugin deletes its structural DataEngineLike re-declaration and types the 'data' service with the real contract; its dead getDatasourceDriver fallback probe is respelled to the declared getDriverByName member. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Rxnd8cyFnoU8V5y21PaTsy --- .changeset/eleven-introspect-contract.md | 5 + .changeset/eleven-introspect-engine.md | 5 + .changeset/eleven-introspect-plugin.md | 5 + packages/objectql/src/engine.ts | 17 ++- .../services/service-datasource/src/plugin.ts | 26 ++-- .../spec/src/contracts/data-driver.test.ts | 128 ++++++++++++++++++ packages/spec/src/contracts/data-driver.ts | 31 +++++ .../spec/src/contracts/data-engine.test.ts | 67 +++++++++ packages/spec/src/contracts/data-engine.ts | 26 ++++ skills/objectstack-data/references/_index.md | 2 + 10 files changed, 296 insertions(+), 16 deletions(-) create mode 100644 .changeset/eleven-introspect-contract.md create mode 100644 .changeset/eleven-introspect-engine.md create mode 100644 .changeset/eleven-introspect-plugin.md diff --git a/.changeset/eleven-introspect-contract.md b/.changeset/eleven-introspect-contract.md new file mode 100644 index 0000000000..53f6e8c0ac --- /dev/null +++ b/.changeset/eleven-introspect-contract.md @@ -0,0 +1,5 @@ +--- +'@objectstack/spec': minor +--- + +The engine-registration road into datasource introspection now meets the compiler (#11493, extending the #11123 ruling from the `DatasourceDriverHandle` seam): `IDataDriver` gains an optional `introspectSchema?(): Promise` member, and `IDataEngine` gains an optional `introspectDatasource?(datasource: string): Promise` member. Both are typed with the spec's one introspection shape (`IntrospectedSchema`, `@objectstack/spec/contracts`). Drivers and engines without introspection stay conformant — the members are optional — while a driver that DOES implement `introspectSchema` with a mis-shaped result (a column flag spelled `isPrimary`, a bare `{ tables }` with no `dialect`/`introspectedAt`) now fails compile at the offending field instead of surfacing at runtime as a federated table whose records cannot be located. diff --git a/.changeset/eleven-introspect-engine.md b/.changeset/eleven-introspect-engine.md new file mode 100644 index 0000000000..c3db78e862 --- /dev/null +++ b/.changeset/eleven-introspect-engine.md @@ -0,0 +1,5 @@ +--- +'@objectstack/objectql': patch +--- + +`ObjectQL.introspectDatasource()` declares its real return type — the spec's `IntrospectedSchema` (the new `IDataEngine.introspectDatasource?` contract member) — instead of an untyped `Promise`, and the driver lookup inside it drops its `as any` now that `IDataDriver` declares `introspectSchema?`. Type-level only; runtime behaviour is byte-identical (#11493). diff --git a/.changeset/eleven-introspect-plugin.md b/.changeset/eleven-introspect-plugin.md new file mode 100644 index 0000000000..3a41edfd34 --- /dev/null +++ b/.changeset/eleven-introspect-plugin.md @@ -0,0 +1,5 @@ +--- +'@objectstack/service-datasource': patch +--- + +`ExternalDatasourceServicePlugin` types the `'data'` service with the real engine contract (`IDataEngine`, `@objectstack/spec/contracts`) and deletes its private structural `DataEngineLike` re-declaration — the workaround the untyped `IDataEngine.introspectDatasource()` forced (#11493). The introspection fallback branch now probes `getDriverByName?` (the registry member the contract declares) instead of `getDatasourceDriver?`, a spelling no engine in either repository ever had, so the degradation path is reachable for the first time. diff --git a/packages/objectql/src/engine.ts b/packages/objectql/src/engine.ts index f02cf6cff3..bca94dd3d6 100644 --- a/packages/objectql/src/engine.ts +++ b/packages/objectql/src/engine.ts @@ -58,7 +58,7 @@ import type { FlowFunctionEffect } from '@objectstack/spec/automation'; // Imported from spec directly rather than through `@objectstack/core`'s // re-export block: that block is labelled backward-compatibility, and this // contract is new (#5945). -import type { IScopedContext, IScopedObjectRepository } from '@objectstack/spec/contracts'; +import type { IScopedContext, IScopedObjectRepository, IntrospectedSchema as SpecIntrospectedSchema } from '@objectstack/spec/contracts'; import { IDataDriver, IDataEngine, @@ -12239,9 +12239,18 @@ export class ObjectQL implements IObjectQLEngine { * * @throws if the datasource has no registered driver, or the driver does * not support introspection. - */ - async introspectDatasource(datasource: string): Promise { - const driver = this.drivers.get(datasource) as any; + * + * [#11493] The return is the spec's ONE introspection shape — the + * `IDataEngine.introspectDatasource?` contract member this method + * implements — not the untyped `Promise` it declared while + * `IDataDriver` was silent about `introspectSchema`. The `as any` on the + * driver lookup went in the same stroke: the member is on the driver + * contract now, so the duck-typed probe below is a typed read. Runtime is + * deliberately byte-identical — both throws and the delegation are + * unchanged. + */ + async introspectDatasource(datasource: string): Promise { + const driver = this.drivers.get(datasource); if (!driver) { throw new Error(`[ObjectQL] Datasource '${datasource}' has no registered driver to introspect.`); } diff --git a/packages/services/service-datasource/src/plugin.ts b/packages/services/service-datasource/src/plugin.ts index 83ca163a57..80d5d74f98 100644 --- a/packages/services/service-datasource/src/plugin.ts +++ b/packages/services/service-datasource/src/plugin.ts @@ -1,7 +1,7 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import type { Plugin, PluginContext } from '@objectstack/core'; -import type { IntrospectedSchema } from '@objectstack/spec/contracts'; +import type { IDataEngine, IntrospectedSchema } from '@objectstack/spec/contracts'; import { ExternalDatasourceService, type ExternalDatasourceServiceConfig, @@ -10,15 +10,17 @@ import { type Logger, } from './external-datasource-service.js'; -/** - * Minimal surfaces the plugin needs from the data engine + metadata service. - * Kept structural so the plugin doesn't hard-depend on concrete classes. - */ -interface DataEngineLike { - /** Resolve a driver by datasource name and introspect its live schema. */ - introspectDatasource?: (datasource: string) => Promise; - getDatasourceDriver?: (datasource: string) => { introspectSchema?: () => Promise } | undefined; -} +// The structural `DataEngineLike` re-declaration that used to live here is +// DELETED (#11493, part of the fix by the maintainer ruling): the `'data'` +// service's real contract (`IDataEngine`, `@objectstack/spec/contracts`) now +// declares `introspectDatasource?` with the spec return type, so this plugin +// no longer needs a private engine type to recover `IntrospectedSchema` from +// an untyped `Promise`. Its second member, `getDatasourceDriver?`, matched NO +// engine in either repository (measured 2026-08-24: zero references outside +// this file) — the fallback branch below probed it and could never fire. The +// probe is respelled to the member the contract actually declares +// (`getDriverByName?`, [#4251]), which makes the degradation reachable for +// the first time instead of silently dead. interface MetadataServiceLike { get: (type: string, name: string) => Promise; @@ -61,14 +63,14 @@ export class ExternalDatasourceServicePlugin implements Plugin { } async init(ctx: PluginContext): Promise { - const engine = safeGetService(ctx, 'data'); + const engine = safeGetService(ctx, 'data'); const metadata = safeGetService(ctx, 'metadata'); const introspect: ExternalDatasourceServiceConfig['introspect'] = this.options.introspect ?? (async (datasource: string) => { if (engine?.introspectDatasource) return engine.introspectDatasource(datasource); - const driver = engine?.getDatasourceDriver?.(datasource); + const driver = engine?.getDriverByName?.(datasource); if (driver?.introspectSchema) return driver.introspectSchema(); throw new Error( `Cannot introspect datasource '${datasource}': no driver introspection available.`, diff --git a/packages/spec/src/contracts/data-driver.test.ts b/packages/spec/src/contracts/data-driver.test.ts index 0f74855c67..4698cad35e 100644 --- a/packages/spec/src/contracts/data-driver.test.ts +++ b/packages/spec/src/contracts/data-driver.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect } from 'vitest'; import type { DriverQuery, IDataDriver } from './data-driver'; +import type { IntrospectedSchema } from './schema-diff-service'; import type { QueryAST } from '../data/query.zod'; import type { DriverOptions } from '../data/driver.zod'; @@ -270,4 +271,131 @@ describe('IDataDriver', () => { expect(unknownOperator.where).toBeTruthy(); }); }); + + // =========================================================================== + // introspectSchema — the engine-registration road meets the compiler (#11493) + // =========================================================================== + // + // #11381 typed the host-factory road (`DatasourceDriverHandle.introspectSchema`, + // option C of the #11123 ruling). This block pins the OTHER documented road: a + // driver implementing `IDataDriver` and handed to `IDataEngine.registerDriver()`. + // Reverse-verified against the pre-#11493 contract (measured 2026-08-24): with + // the interface silent about `introspectSchema`, the mis-shapes below compiled + // GREEN — an extra member rides along unchecked — which is the gap #11493 + // closes. As with the DriverQuery pins above, every directive here is resolved + // by tsc: reverting the member makes each `@ts-expect-error` unused, and an + // unused directive is itself an error, so `pnpm --filter @objectstack/spec + // typecheck` goes red on regression in either direction. + + describe('introspectSchema (#11493)', () => { + /** The declared return type, read off the CONTRACT rather than re-spelled. */ + type DriverIntrospection = Awaited>>; + + const base: IDataDriver = { + name: 'introspecting', + version: '1.0.0', + supports: {}, + connect: async () => {}, + disconnect: async () => {}, + checkHealth: async () => true, + execute: async () => ({}), + find: async () => [], + findOne: async () => null, + create: async () => ({ id: '1' }), + update: async () => ({ id: '1' }), + upsert: async () => ({ id: '1' }), + delete: async () => true, + count: async () => 0, + bulkCreate: async () => [], + bulkUpdate: async () => [], + bulkDelete: async () => {}, + beginTransaction: async () => ({}), + commit: async () => {}, + rollback: async () => {}, + syncSchema: async () => {}, + dropTable: async () => {}, + }; + + it('is optional — a driver without introspection stays conformant', () => { + // `base` above declares no `introspectSchema` and satisfies `IDataDriver` + // at its declaration; introspection is a capability, not an obligation. + expect(base.introspectSchema).toBeUndefined(); + }); + + it('declares exactly the spec introspection shape, not a lookalike', () => { + // Mutual extends: the member's return IS `IntrospectedSchema` — a revert + // to `unknown` (or a drift to a private re-spelling) resolves `Exact` to + // `never` and this line goes red naming the contract. + type Exact = DriverIntrospection extends IntrospectedSchema + ? (IntrospectedSchema extends DriverIntrospection ? 'exact' : never) + : never; + const exact: Exact = 'exact'; + expect(exact).toBe('exact'); + }); + + it('accepts the spec shape, and a shape that EXTENDS it (the driver-sql pattern)', () => { + const conforming: IDataDriver = { + ...base, + introspectSchema: async () => ({ + dialect: 'postgres', + introspectedAt: '2026-08-24T00:00:00.000Z', + tables: { + wh_order: { + name: 'wh_order', + columns: [{ name: 'id', type: 'uuid', nullable: false, primaryKey: true }], + }, + }, + }), + }; + // Extra facts ride along: driver-sql's table-level `primaryKeys` / + // `foreignKeys` and per-column `isUnique` / `maxLength` live on declared + // types that EXTEND the spec contract, and assignability admits them on + // any non-literal value. What the contract refuses is a wrong spelling + // of a DECLARED key, never a richer driver. + const extendedResult = { + dialect: 'postgres', + introspectedAt: '2026-08-24T00:00:00.000Z', + tables: { + wh_order: { + name: 'wh_order', + columns: [{ name: 'id', type: 'uuid', nullable: false, primaryKey: true, isUnique: true, maxLength: 36 }], + primaryKeys: ['id'], + foreignKeys: [], + }, + }, + }; + const extended: IDataDriver = { ...base, introspectSchema: async () => extendedResult }; + expect(typeof conforming.introspectSchema).toBe('function'); + expect(typeof extended.introspectSchema).toBe('function'); + }); + + it('refuses the retired isPrimary spelling at the offending field', () => { + // The defect class this seam actually shipped: primary-key membership + // spelled `isPrimary`, which no consumer reads — the federated table's + // records silently could not be located or updated. + // @ts-expect-error - primary-key membership is spelled `primaryKey`, never `isPrimary` + const misSpelled: DriverIntrospection = { dialect: 'postgres', introspectedAt: 'now', tables: { t: { name: 't', columns: [{ name: 'id', type: 'uuid', nullable: false, isPrimary: true }] } } }; + expect(misSpelled).toBeTruthy(); + }); + + it('refuses a bare { tables } with no dialect / introspectedAt envelope', () => { + // @ts-expect-error - `dialect` and `introspectedAt` are REQUIRED on the spec schema + const bareTables: DriverIntrospection = { tables: {} }; + expect(bareTables).toBeTruthy(); + }); + + it('refuses a mis-shaped implementation where it is OFFERED, on the registerDriver road', () => { + // Exactly what a pre-#11493 driver author shipped: the whole driver value, + // with an `introspectSchema` answering the pre-spec shape. Against the + // silent contract this assignment compiled green (the measured gap); + // declared, tsc refuses it at the member. + const preFixResult = { tables: { t: { name: 't', columns: [{ name: 'id', type: 'uuid', nullable: false, isPrimary: true }] } } }; + const author: IDataDriver = { + ...base, + // @ts-expect-error - the pre-spec result shape no longer satisfies the declared member + introspectSchema: async () => preFixResult, + }; + expect(author).toBeTruthy(); + }); + }); }); diff --git a/packages/spec/src/contracts/data-driver.ts b/packages/spec/src/contracts/data-driver.ts index 2789172d7f..fd63a5a2d7 100644 --- a/packages/spec/src/contracts/data-driver.ts +++ b/packages/spec/src/contracts/data-driver.ts @@ -2,6 +2,7 @@ import type { DriverOptions, DriverCapabilities } from '../data/driver.zod.js'; import type { QueryAST } from '../data/query.zod.js'; +import type { IntrospectedSchema } from './schema-diff-service.js'; /** * DriverQuery — the query AST as a **driver** receives it: {@link QueryAST} @@ -363,6 +364,36 @@ export interface IDataDriver { */ getSchemaSyncStats?(): { created: number; existing: number }; + /** + * Introspect the live physical schema this driver is connected to + * (ADR-0015): table names, columns, and primary-key membership, as the + * spec's ONE introspection shape — {@link IntrospectedSchema}. + * + * The return type is CONTRACTUAL, and it is declared here for the same + * reason `DatasourceDriverHandle.introspectSchema` was typed by #11381 + * (option C of the #11123 ruling): a custom driver has TWO documented roads + * into the same runtime read — the host-factory handle, and direct + * `IDataEngine.registerDriver()` — and until #11493 only the first was + * reachable by a compiler. A driver author implementing THIS interface had + * no signature to mis-match against, so a column flag spelled `isPrimary`, + * or a bare `{ tables }` with no `dialect`/`introspectedAt`, compiled clean + * and surfaced only as a federated table whose records silently could not + * be located or updated (absorbed by the PR #11001 runtime shim). Declared + * here, `tsc` refuses the mis-shape at the offending field on either road. + * + * Extra facts a richer driver carries stay legal — driver-sql's table-level + * `primaryKeys` / `foreignKeys`, per-column `isUnique` / `maxLength` ride + * on declared types that EXTEND the spec contract, and assignability + * admits them. What is refused is a WRONG spelling of a declared key, + * which is the defect class this seam has actually shipped. + * + * Optional: introspection is a capability, not an obligation — drivers + * without it (memory, mongodb today) simply omit the member and stay + * conformant. The engine's `introspectDatasource()` answers their absence + * with a named error rather than a guess. + */ + introspectSchema?(): Promise; + /** Drop the underlying table or collection (destructive) */ dropTable(object: string, options?: DriverOptions): Promise; diff --git a/packages/spec/src/contracts/data-engine.test.ts b/packages/spec/src/contracts/data-engine.test.ts index d73c62e1a2..5356061b24 100644 --- a/packages/spec/src/contracts/data-engine.test.ts +++ b/packages/spec/src/contracts/data-engine.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect } from 'vitest'; import type { IDataEngine, WriteObservabilityOptions } from './data-engine'; import type { IDataDriver } from './data-driver'; +import type { IntrospectedSchema } from './schema-diff-service'; import { EngineUpdateOptionsSchema, DataEngineInsertOptionsSchema, @@ -353,4 +354,70 @@ describe('Data Engine Contract', () => { // in 17.0.0 (#4484) — it built an IDataDriver whose only job was to satisfy a // required method no production code ever called. }); + + // =========================================================================== + // introspectDatasource — typed on the contract, not re-declared by consumers + // (#11493, extending the #11123 ruling to the engine-registration seam) + // =========================================================================== + // + // Reverse-verified against the pre-#11493 contract (measured 2026-08-24): + // with the member undeclared, an engine answering a non-spec shape compiled + // green, and the one in-tree consumer (service-datasource's plugin) carried + // a private structural `DataEngineLike` to recover the spec return type. + // Every directive below is resolved by tsc; reverting the member makes it + // unused, and an unused directive is itself an error. + + describe('introspectDatasource (#11493)', () => { + type EngineIntrospection = Awaited>>; + + const minimalEngine: IDataEngine = { + find: async () => [], + findOne: async () => null, + insert: async (_obj, data) => data, + update: async (_obj, data) => data, + delete: async () => ({ deleted: 0 }), + count: async () => 0, + aggregate: async () => [], + }; + + it('is optional — an engine without a named-driver registry stays conformant', () => { + // `minimalEngine` satisfies `IDataEngine` at its declaration with no + // registry members at all — same posture as `getDriverByName?` ([#4251]). + expect(minimalEngine.introspectDatasource).toBeUndefined(); + }); + + it('declares exactly the spec introspection shape', () => { + // Mutual extends: a revert to `Promise` — the shape that forced + // the consumer-side re-declaration — resolves `Exact` to `never`. + type Exact = EngineIntrospection extends IntrospectedSchema + ? (IntrospectedSchema extends EngineIntrospection ? 'exact' : never) + : never; + const exact: Exact = 'exact'; + expect(exact).toBe('exact'); + }); + + it('accepts an engine that answers the spec shape', () => { + const introspecting: IDataEngine = { + ...minimalEngine, + introspectDatasource: async (_datasource) => ({ + dialect: 'postgres', + introspectedAt: '2026-08-24T00:00:00.000Z', + tables: {}, + }), + }; + expect(typeof introspecting.introspectDatasource).toBe('function'); + }); + + it('refuses an engine that answers a non-spec shape at the member', () => { + // The pre-#11493 posture: `{ tables }` alone, no envelope — absorbed at + // runtime by the consumer-side shim, invisible to every compiler. + const bareTables = { tables: {} }; + const misShapen: IDataEngine = { + ...minimalEngine, + // @ts-expect-error - the untyped pre-#11493 result no longer satisfies the declared member + introspectDatasource: async (_datasource: string) => bareTables, + }; + expect(misShapen).toBeTruthy(); + }); + }); }); diff --git a/packages/spec/src/contracts/data-engine.ts b/packages/spec/src/contracts/data-engine.ts index 5eac69116f..8e0b739c65 100644 --- a/packages/spec/src/contracts/data-engine.ts +++ b/packages/spec/src/contracts/data-engine.ts @@ -11,6 +11,7 @@ import { DroppedFieldsEvent, } from '../data/index.js'; import type { IDataDriver } from './data-driver.js'; +import type { IntrospectedSchema } from './schema-diff-service.js'; /** * In-process write-observability hooks for `insert`/`update` (#3407). @@ -262,4 +263,29 @@ export interface IDataEngine { */ getDefaultDriverName?(): string | undefined; getDriverByName?(name: string): IDataDriver | undefined; + + /** + * Introspect a datasource's live remote schema (ADR-0015): resolve the + * driver registered under `datasource` and delegate to its + * `introspectSchema()` capability. Implementations throw when the + * datasource has no registered driver, or its driver does not offer + * introspection — absence is answered with a named error, never a guess. + * + * Optional for the same reason as the registry pair above: only an engine + * that owns a named-driver registry can resolve a datasource to a driver; + * test fakes and remote/virtual engines simply omit it. + * + * [#11493] Declared because the binding is evidenced, exactly as [#4251] + * asks: ObjectQL has implemented this method since ADR-0015, and the + * external-datasource service reads it off the `'data'` service — while + * this contract stayed silent, that consumer had to re-declare a private + * structural engine type to recover the spec return type from the + * implementation's untyped `Promise`. The return type is the spec's ONE + * introspection shape ({@link IntrospectedSchema}), the same contract + * #11381 put on `DatasourceDriverHandle.introspectSchema` — this member + * extends that ruling (#11123's population/channel argument) to the + * engine-registration seam, so both roads into the runtime read now meet + * a compiler. + */ + introspectDatasource?(datasource: string): Promise; } diff --git a/skills/objectstack-data/references/_index.md b/skills/objectstack-data/references/_index.md index 83bb9accae..64562c6777 100644 --- a/skills/objectstack-data/references/_index.md +++ b/skills/objectstack-data/references/_index.md @@ -21,6 +21,7 @@ from `node_modules` — there is no local copy in the skill bundle. - `node_modules/@objectstack/spec/src/api/errors.zod.ts` — Standardized Error Codes Protocol - `node_modules/@objectstack/spec/src/automation/flow-function.zod.ts` — The contract for a **named handler function a `script` node invokes** — +- `node_modules/@objectstack/spec/src/data/driver-sql.zod.ts` — SQL Dialect Enumeration - `node_modules/@objectstack/spec/src/data/driver.zod.ts` — Common Driver Options - `node_modules/@objectstack/spec/src/data/driver/common.zod.ts` — Shared building blocks for the per-driver `datasource.config` shapes (#4410). - `node_modules/@objectstack/spec/src/data/driver/config-registry.zod.ts` — The driver-id → `datasource.config` shape registry (#4410). @@ -42,6 +43,7 @@ from `node_modules` — there is no local copy in the skill bundle. - `node_modules/@objectstack/spec/src/shared/identifiers.zod.ts` — System Identifier Schema - `node_modules/@objectstack/spec/src/shared/protection.zod.ts` — Package-level metadata protection (ADR-0010 §3.7 — Phase 4.3) - `node_modules/@objectstack/spec/src/shared/suggestions.zod.ts` — "Did you mean?" Suggestion Utilities +- `node_modules/@objectstack/spec/src/system/deploy-bundle.zod.ts` — Deploy Bundle Protocol - `node_modules/@objectstack/spec/src/ui/action-params.zod.ts` — The action DISPATCH contract: what the platform validates on the way in, and - `node_modules/@objectstack/spec/src/ui/action.zod.ts` — Action Parameter Schema - `node_modules/@objectstack/spec/src/ui/bulk-action.zod.ts` — Bulk Action Schemas From eab6d131ba4c791fc57c76c8ff4ae487300a3c41 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 19:03:18 +0000 Subject: [PATCH 2/2] =?UTF-8?q?test(spec):=20pin=20introspectDatasource=20?= =?UTF-8?q?type-level=20=E2=80=94=20no=20new=20engine=20double?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit check:engine-double-contract counts IDataEngine literals in this file against a shrink-only baseline; the pins read the member type off the contract instead, so the double census is untouched. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Rxnd8cyFnoU8V5y21PaTsy --- .../spec/src/contracts/data-engine.test.ts | 55 +++++++++---------- 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/packages/spec/src/contracts/data-engine.test.ts b/packages/spec/src/contracts/data-engine.test.ts index 5356061b24..1dc5355442 100644 --- a/packages/spec/src/contracts/data-engine.test.ts +++ b/packages/spec/src/contracts/data-engine.test.ts @@ -367,23 +367,24 @@ describe('Data Engine Contract', () => { // Every directive below is resolved by tsc; reverting the member makes it // unused, and an unused directive is itself an error. + // Deliberately NO new engine double in this block: every pin below reads the + // MEMBER type off the contract instead of standing up another `IDataEngine` + // literal (this file's doubles are counted by `check:engine-double-contract` + // against a shrink-only baseline, and a pin block is not a reason to grow + // it). The value-level optionality evidence already exists above: every + // pre-existing minimal `IDataEngine` literal in this file omits + // `introspectDatasource` and compiles. describe('introspectDatasource (#11493)', () => { - type EngineIntrospection = Awaited>>; - - const minimalEngine: IDataEngine = { - find: async () => [], - findOne: async () => null, - insert: async (_obj, data) => data, - update: async (_obj, data) => data, - delete: async () => ({ deleted: 0 }), - count: async () => 0, - aggregate: async () => [], - }; + type Member = IDataEngine['introspectDatasource']; + type EngineIntrospection = Awaited>>; it('is optional — an engine without a named-driver registry stays conformant', () => { - // `minimalEngine` satisfies `IDataEngine` at its declaration with no - // registry members at all — same posture as `getDriverByName?` ([#4251]). - expect(minimalEngine.introspectDatasource).toBeUndefined(); + // Same posture as `getDriverByName?` ([#4251]): the member's type admits + // `undefined`, so the minimal literals above satisfy the contract without + // it. A revert to a REQUIRED member resolves `Optional` to `never`. + type Optional = undefined extends Member ? 'optional' : never; + const optional: Optional = 'optional'; + expect(optional).toBe('optional'); }); it('declares exactly the spec introspection shape', () => { @@ -396,27 +397,21 @@ describe('Data Engine Contract', () => { expect(exact).toBe('exact'); }); - it('accepts an engine that answers the spec shape', () => { - const introspecting: IDataEngine = { - ...minimalEngine, - introspectDatasource: async (_datasource) => ({ - dialect: 'postgres', - introspectedAt: '2026-08-24T00:00:00.000Z', - tables: {}, - }), - }; - expect(typeof introspecting.introspectDatasource).toBe('function'); + it('accepts an implementation that answers the spec shape', () => { + const introspect: NonNullable = async (_datasource: string) => ({ + dialect: 'postgres', + introspectedAt: '2026-08-24T00:00:00.000Z', + tables: {}, + }); + expect(typeof introspect).toBe('function'); }); - it('refuses an engine that answers a non-spec shape at the member', () => { + it('refuses an implementation that answers a non-spec shape', () => { // The pre-#11493 posture: `{ tables }` alone, no envelope — absorbed at // runtime by the consumer-side shim, invisible to every compiler. const bareTables = { tables: {} }; - const misShapen: IDataEngine = { - ...minimalEngine, - // @ts-expect-error - the untyped pre-#11493 result no longer satisfies the declared member - introspectDatasource: async (_datasource: string) => bareTables, - }; + // @ts-expect-error - the untyped pre-#11493 result no longer satisfies the declared member + const misShapen: NonNullable = async (_datasource: string) => bareTables; expect(misShapen).toBeTruthy(); }); });