From f69a8609278d3a8323e1658137a3ca6a37dea149 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 09:43:37 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat(spec):=20IDataEngine=20datasource-def?= =?UTF-8?q?=20contract=20catches=20up=20to=20the=20engine=20=E2=80=94=20ex?= =?UTF-8?q?ternal.credentialsRef=20accepted,=20listDatasourceDefs=20declar?= =?UTF-8?q?ed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Part of #12805 (report and regenerated artifacts follow). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LpRNHxWZgSUgVnFT9mQQo4 --- .../spec/src/contracts/data-engine.test.ts | 66 ++++++++++++++- packages/spec/src/contracts/data-engine.ts | 84 +++++++++++++++++-- 2 files changed, 138 insertions(+), 12 deletions(-) diff --git a/packages/spec/src/contracts/data-engine.test.ts b/packages/spec/src/contracts/data-engine.test.ts index 7ed1155342..e44720663c 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 { EngineDatasourceDef, IDataEngine, WriteObservabilityOptions } from './data-engine'; import type { IDataDriver } from './data-driver'; +import type { ServiceSlotContract } from './core-service-contracts'; import type { IntrospectedSchema } from './schema-diff-service'; import { EngineUpdateOptionsSchema, @@ -494,30 +495,87 @@ describe('Data Engine Contract', () => { }); }); - describe('datasource lifecycle members (#12248, #12010 via the #11833 ruling item 4)', () => { + describe('datasource lifecycle members (#12248, #12010, #12805 via the #11833 ruling item 4)', () => { type RegisterMember = IDataEngine['registerDatasourceDef']; + type ListMember = IDataEngine['listDatasourceDefs']; type MarkMember = IDataEngine['markDatasourceUnavailable']; type ClearMember = IDataEngine['clearDatasourceUnavailable']; - it('all three are optional — only engines owning a datasource registry answer', () => { + it('all four are optional — only engines owning a datasource registry answer', () => { type A = undefined extends RegisterMember ? 'optional' : never; + type L = undefined extends ListMember ? 'optional' : never; type B = undefined extends MarkMember ? 'optional' : never; type C = undefined extends ClearMember ? 'optional' : never; const a: A = 'optional'; + const l: L = 'optional'; const b: B = 'optional'; const c: C = 'optional'; - expect([a, b, c]).toEqual(['optional', 'optional', 'optional']); + expect([a, l, b, c]).toEqual(['optional', 'optional', 'optional', 'optional']); }); it('registerDatasourceDef takes the declarative def — name required, write gate keys optional', () => { const register: NonNullable = (_def) => {}; register({ name: 'warehouse' }); register({ name: 'warehouse', schemaMode: 'read-only', external: { allowWrites: false } }); + // #12805 — the accept set catches up to what the engine has retained + // since #12758: a fresh literal carrying the secrets-store handle + // compiles at THIS seam (before the catch-up it was refused with + // TS2353 here while the runtime accepted and kept the value). + register({ + name: 'warehouse', + schemaMode: 'read-only', + external: { allowWrites: false, credentialsRef: 'secrets/warehouse' }, + }); // @ts-expect-error - a datasource definition without a name registers nothing register({ schemaMode: 'read-only' }); + // The widening is exactly the ruled key, not an open door: an inline + // credential has no declared home on the def (secrets travel by + // REFERENCE — `credentialsRef`), so an undeclared `external` key stays + // refused. Pinned in BOTH directions, like the `kind` union below. + // @ts-expect-error - `credentials` (inline) is not a declared external key + register({ name: 'warehouse', external: { credentials: 'user:pass' } }); expect(typeof register).toBe('function'); }); + it('listDatasourceDefs answers the SAME declared def the register member takes — one shape, no drift', () => { + // Mutual extends pins that the write side and the read-back share ONE + // declaration (`EngineDatasourceDef`): a widening that reaches only one + // of the pair resolves either half to `never`. + type Registered = Parameters>[0]; + type Listed = ReturnType>[number]; + type Same = Registered extends Listed + ? (Listed extends Registered ? 'same' : never) + : never; + type Declared = Listed extends EngineDatasourceDef + ? (EngineDatasourceDef extends Listed ? 'declared' : never) + : never; + const same: Same = 'same'; + const declared: Declared = 'declared'; + expect(same).toBe('same'); + expect(declared).toBe('declared'); + }); + + it('a sys_secret sweep can read the handle through the data slot contract alone', () => { + // The #12804 consumer seam (#12805): "which code-declared datasources + // hold a `sys_secret` handle", typed against `ServiceSlotContract` + // for the data slot — naming neither the engine class nor a + // consumer-local structural re-declaration (the #11833 pattern). + const sweep = (engine: ServiceSlotContract<'data'>): string[] => + (engine.listDatasourceDefs?.() ?? []) + .filter((def) => def.external?.credentialsRef !== undefined) + .map((def) => def.name); + expect(typeof sweep).toBe('function'); + }); + + it('refuses an implementation answering nameless or inline-credential defs', () => { + // @ts-expect-error - every listed definition carries its name + const nameless: NonNullable = () => [{ schemaMode: 'read-only' }]; + // @ts-expect-error - the def carries a secrets-store REFERENCE, never an inline credential + const inline: NonNullable = () => [{ name: 'w', external: { credentials: 'user:pass' } }]; + expect(nameless).toBeTruthy(); + expect(inline).toBeTruthy(); + }); + it('markDatasourceUnavailable admits exactly the two declared kinds', () => { // `'blocked'` (host policy refused) vs `'failed'` (connect failed under // a degraded boot) is the framework#3828 distinction — the reason the diff --git a/packages/spec/src/contracts/data-engine.ts b/packages/spec/src/contracts/data-engine.ts index 334d49239e..96f260af3c 100644 --- a/packages/spec/src/contracts/data-engine.ts +++ b/packages/spec/src/contracts/data-engine.ts @@ -164,6 +164,49 @@ export interface WriteObservabilityOptions { strictReadonlyWrites?: boolean; } +/** + * A datasource *definition* as the engine retains it (ADR-0015) — the + * declarative facts a datasource states about itself, never a live driver + * connection (that is `registerDriver`). + * + * Named because two members share it: {@link IDataEngine.registerDatasourceDef} + * writes it and {@link IDataEngine.listDatasourceDefs} reads it back. Two + * inline copies of one shape on one contract is the de-facto-contract drift + * the #11833 sweep exists to retire — and a name here is what lets a consumer + * type its sweep without re-declaring a consumer-local structural element + * type, the same pattern one member over. Naming follows the contract's + * existing `Engine*` family (`EngineQueryOptions`, `EngineUpdateOptions`, …); + * `@objectstack/objectql` exports its own structurally-identical + * `DatasourceDef`, which this contract cannot import (dependency direction), + * so the distinct name keeps the two declarations tellable apart until the + * engine converges on this one. + * + * The keys are a deliberate SUBSET of the spec's authored datasource surface + * (`ExternalDatasourceSettingsSchema` in `data/datasource.zod.ts`) — the + * engine retains only what it has a use for. ⛔ Not a mirror of the authored + * block and not a place to grow one: `validation` and `queryTimeoutMs` are + * absent because nothing in the engine reads them ([#12805] mirrors engine + * reality, it does not invent surface). + */ +export interface EngineDatasourceDef { + name: string; + schemaMode?: string; + external?: { + /** Datasource-wide write gate — ADR-0015 §5.3 Gate 3. */ + allowWrites?: boolean; + /** + * Reference into the secrets store, never an inline credential + * (`credentialsRef` on `ExternalDatasourceSettingsSchema`). Valid in + * EVERY `schemaMode` — it is the one `external` key a managed datasource + * may carry (#8153) — so a reader sweeping for handles must not filter + * by schema mode. [#12805] The engine has accepted and retained this key + * since #12758; this contract is the catch-up, not a widening beyond + * engine reality. + */ + credentialsRef?: string; + }; +} + /** * IDataEngine - Standard Data Engine Interface * @@ -351,16 +394,41 @@ export interface IDataEngine { * the third such type the #11833 sweep measured (#12010), adjudicated onto * the contract by the 2026-08-25 ruling's item 4. * - * Register a datasource *definition* (ADR-0015) — declarative - * `schemaMode` + `external.allowWrites`, so the engine's write gate can - * enforce external-datasource ownership. Distinct from registering a live + * Register a datasource *definition* (ADR-0015) — the declarative + * {@link EngineDatasourceDef}: `schemaMode` + `external.allowWrites` so the + * engine's write gate can enforce external-datasource ownership, and + * `external.credentialsRef` so {@link listDatasourceDefs} can hand a + * credentials sweep the handle a code-declared datasource holds ([#12805] + * — the engine accepts and retains the reference since #12758; before + * this catch-up a caller typed against THIS contract was refused with + * TS2353 for a value the runtime keeps). Distinct from registering a live * driver connection. Safe to call repeatedly; last write wins. */ - registerDatasourceDef?(def: { - name: string; - schemaMode?: string; - external?: { allowWrites?: boolean }; - }): void; + registerDatasourceDef?(def: EngineDatasourceDef): void; + + /** + * Every datasource DEFINITION this engine holds — from BOTH entry routes, + * {@link registerDatasourceDef} and the engine's package-manifest install + * path. The read-back of the same registry the member above writes, so it + * shares that member's population ("only engines that own a datasource + * registry answer") and its optionality — the fourth member of the + * lifecycle family the 2026-08-25 ruling's item 4 adjudicated onto this + * contract, declared under the same [#4251]/[#11493] evidence bar + * ([#12805]: implemented on `ObjectQL` since #12758, and the #12804 + * `sys_secret` reference sweep is the consumer that otherwise must name + * the engine class concretely or re-declare the member structurally). + * + * Exists because a datasource declared IN CODE never reaches the metadata + * store, so a `sys_secret` reference sweep reading `sys_metadata` alone + * cannot see the handle such a datasource holds at + * `external.credentialsRef` and must be handed the list by the engine. + * Implementations answer UNFILTERED (every definition, whatever its + * `schemaMode`, with or without a reference — `credentialsRef` is valid on + * a managed datasource too, #8153) and answer FRESH copies: a reader must + * not be able to reach through this accessor and mutate the write gate's + * own input. + */ + listDatasourceDefs?(): EngineDatasourceDef[]; /** * Record that a **declared** datasource has no live driver, and why From 99d74bb48b22771fbb84d29619becf25d8db6f38 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 09:54:20 +0000 Subject: [PATCH 2/2] chore(spec): regenerate api-surface and export-origins for EngineDatasourceDef; add changeset Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LpRNHxWZgSUgVnFT9mQQo4 --- .../data-engine-datasource-def-catchup.md | 21 +++++++++++++++++++ packages/spec/api-surface/contracts.json | 1 + packages/spec/export-origins/contracts.json | 1 + 3 files changed, 23 insertions(+) create mode 100644 .changeset/data-engine-datasource-def-catchup.md diff --git a/.changeset/data-engine-datasource-def-catchup.md b/.changeset/data-engine-datasource-def-catchup.md new file mode 100644 index 0000000000..3e512c9b4a --- /dev/null +++ b/.changeset/data-engine-datasource-def-catchup.md @@ -0,0 +1,21 @@ +--- +"@objectstack/spec": minor +--- + +feat(spec): `IDataEngine`'s datasource-def contract catches up to the engine — `registerDatasourceDef` accepts `external.credentialsRef`, and `listDatasourceDefs` is declared (#12805) + +Additive contract catch-up to declared-and-enforced engine reality (#12758), +the fourth datasource-lifecycle member under the 2026-08-25 #11833 ruling's +item-4 precedent. `registerDatasourceDef`'s parameter now admits the +`external.credentialsRef` secrets-store handle the engine has accepted and +retained since #12758 — before this, a caller typed against the published +contract was refused with TS2353 at the consumer seam for a value the runtime +keeps (the engine's own typecheck was green either way, parameter bivariance). +The new optional `listDatasourceDefs?(): EngineDatasourceDef[]` member is the +read-back of the same registry, declared so a `sys_secret` reference sweep +(#12804) can ask the engine "which code-declared datasources hold a handle" +through the `'data'` slot contract instead of naming the engine class or +re-declaring a consumer-local structural type (the #11833 pattern). Both +members share the newly exported `EngineDatasourceDef` shape and stay +optional — only engines that own a datasource registry answer. No runtime +change; existing `IDataEngine` implementers and callers are unaffected. diff --git a/packages/spec/api-surface/contracts.json b/packages/spec/api-surface/contracts.json index 1189c79d8a..ec7d051b15 100644 --- a/packages/spec/api-surface/contracts.json +++ b/packages/spec/api-surface/contracts.json @@ -75,6 +75,7 @@ "EmailAddress (type)", "EmailAttachment (interface)", "EmailDeliveryStatus (type)", + "EngineDatasourceDef (interface)", "EngineSchemaRegistryView (interface)", "EngineTransactionInfo (interface)", "EngineTransactionOptions (interface)", diff --git a/packages/spec/export-origins/contracts.json b/packages/spec/export-origins/contracts.json index a780c8f450..d3fce9f12b 100644 --- a/packages/spec/export-origins/contracts.json +++ b/packages/spec/export-origins/contracts.json @@ -75,6 +75,7 @@ "EmailAddress": "src/contracts/email-service.ts#EmailAddress (type)", "EmailAttachment": "src/contracts/email-service.ts#EmailAttachment (interface)", "EmailDeliveryStatus": "src/contracts/email-service.ts#EmailDeliveryStatus (type)", + "EngineDatasourceDef": "src/contracts/data-engine.ts#EngineDatasourceDef (interface)", "EngineSchemaRegistryView": "src/contracts/objectql-engine.ts#EngineSchemaRegistryView (interface)", "EngineTransactionInfo": "src/contracts/objectql-engine.ts#EngineTransactionInfo (interface)", "EngineTransactionOptions": "src/contracts/objectql-engine.ts#EngineTransactionOptions (interface)",