Skip to content

feat(spec): declare introspectSchema on IDataDriver and type IDataEngine.introspectDatasource — the engine-registration seam meets the compiler - #11834

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-11493-idatadriver-introspect
Aug 25, 2026
Merged

feat(spec): declare introspectSchema on IDataDriver and type IDataEngine.introspectDatasource — the engine-registration seam meets the compiler#11834
os-warren merged 2 commits into
mainfrom
claude/issue-11493-idatadriver-introspect

Conversation

@os-warren

@os-warrenos-warren commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Fixes#11493

What this does

Extends the #11123 ruling to the engine-registration seam, per the maintainer ruling on the card (comment 5394014425), quoted verbatim and untranslated:

「接受你的建议。」

(maintainer, 2026-08-24, live PM chat, on the aligned four-facet analysis — both steps approved as the extension of the #11123 ruling to the engine-registration seam.)

  1. IDataDriver (spec contract) gains an OPTIONAL introspectSchema?() member returning the spec type IntrospectedSchema (packages/spec/src/contracts/data-driver.ts), so drivers without introspection stay conformant. A driver implementing it with a wrong shape now fails compile at the offending field, on the registerDriver() road that feat(service-datasource): external-datasource driver return contract fails compile-time on mis-shaped drivers (#11123 option C) #11492 could not reach.
  2. IDataEngine gains an OPTIONAL introspectDatasource?(datasource: string) member with the same spec return type (packages/spec/src/contracts/data-engine.ts, the same evidenced-binding posture as the [The service-lookup any rule misses getService<any>(...) — 80 sites erase the slot contract, 3 of them inside the rule's own scope #4251] registry pair beside it), and ObjectQL.introspectDatasource() tightens its return from the untyped Promise to the spec type, dropping the as any driver probe (packages/objectql/src/engine.ts).
  3. service-datasource/plugin.ts DELETES its structural DataEngineLike re-declaration — part of the fix per the ruling, not follow-up. The plugin now types the 'data' service with the real IDataEngine contract. Forced by the deletion: the workaround's second member, getDatasourceDriver?, matched no engine in either repository, ever (measured 2026-08-24: zero references outside this file), so its fallback branch could never fire; the probe is respelled to the member the contract actually declares (getDriverByName?, [The service-lookup any rule misses getService<any>(...) — 80 sites erase the slot contract, 3 of them inside the rule's own scope #4251]), making the degradation reachable for the first time. This is the one deliberate runtime-visible edit in the PR (see "Runtime" below).

The interface member on IDataEngine is the mechanism step ② requires: without it, deleting DataEngineLike leaves the plugin nothing typed to compile against — the ruling's own wording (IDataEngine.introspectDatasource()) names the member on the contract.

In-tree driver audit (required by the ruling — readings)

DriverintrospectSchemaDeclared returnVerdict
@objectstack/driver-sql (SqlDriver)yes (sql-driver.ts)local IntrospectedSchema extends SpecIntrospectedSchema (derive-not-redeclare, post-#11124/#11270; extras: per-column isUnique/maxLength, per-table primaryKeys/foreignKeys)aligned — assignable to the new member; package tsc --noEmit green against the new contract
@objectstack/driver-turso (TursoDriver extends SqlDriver)inheritedinherits driver-sql'saligned
@objectstack/driver-sqlite-wasm (SqliteWasmDriver extends SqlDriver)inheritedinherits driver-sql'saligned
@objectstack/driver-memory (InMemoryDriver)absentconformant via optionality
@objectstack/driver-mongodb (MongoDBDriver)absentconformant via optionality
@objectstack/objectql consumer types (util.ts)n/a (consumer)IntrospectedSchema extends SpecIntrospectedSchemaaligned

No drift found ⇒ no driver touch-ups needed. Deliberately NOT widened: the Zod mirror DriverInterfaceSchema (data/driver.zod.ts) — its coverage of optional TS members is already partial by existing practice (updateMany/syncSchemasBatch/explain declared; temporalFilterValue/registerExternalObject/getSchemaSyncStats/reclaimSpace not), and the ruling names the TS contract; widening the mirror is a separate decision.

Out-of-tree driver risk (the accepted face)

An out-of-tree driver with a same-named, differently-shaped introspectSchema gets a loud compile error on upgrade — carried from the four-facet block as the accepted face of this change. The runtime is untouched: plain-JS drivers and stale builds are reached by no compiler, and the PR #11001 runtime shim keeps absorbing them exactly as before.

Reverse verification (the required inversion, both legs recorded)

One fixture (a driver class whose introspectSchema answers { tables } with the retired isPrimary column spelling, plus an engine whose introspectDatasource answers { tables: {} }), no suppression directives, compiled by tsc --noEmit in packages/spec:

  • Leg A — contracts at BASE e43b18fd9 (pre-fix): exit 0, zero diagnostics. The mis-shapes compile GREEN — the gap the card describes, measured.
  • Leg B — contracts at the fix (same fixture): exit 2
    • TS2416: Property 'introspectSchema' in type 'MisShapenDriver' is not assignable to the same property in base type 'IDataDriver'. … Type '{ tables: … isPrimary … }' is missing the following properties from type 'IntrospectedSchema': dialect, introspectedAt
    • TS2322 … The types returned by 'introspectDatasource(...)' are incompatible … Type '{ tables: {}; }' is missing the following properties from type 'IntrospectedSchema': dialect, introspectedAt

The fixture was never committed; the committed pins carry the same inversion as @ts-expect-error directives (an unused directive is itself an error, so a revert reds pnpm --filter @objectstack/spec typecheck in either direction).

Pins

  • Wrong shape fails compile: @ts-expect-error pins in data-driver.test.ts (retired isPrimary spelling; bare { tables }; a mis-shaped member offered on the driver value) and data-engine.test.ts (non-spec engine answer) — the repo's existing type-test idiom (tsc-resolved, vitest-hosted, zero entries in test-typecheck-debt.json).
  • Optionality: type-level undefined extends pins on both members; value-level, the file's pre-existing minimal literals already omit both members and compile.
  • Exactness: mutual-extends pins that the members' return type IS IntrospectedSchema (a revert to the untyped Promise resolves the pin type to never).
  • Engine-side pins are deliberately type-level — no new IDataEngine literal, so check:engine-double-contract's shrink-only double census is untouched.
  • plugin.ts compiles against the real engine type: @objectstack/service-datasourcetsc --noEmit green with the workaround deleted.

Runtime

Zero runtime change in spec and objectql, verified at the artifact: the spec edits are interface members (type-only imports + declarations — no JS is emitted for them), and the compiled ObjectQL.introspectDatasource body in packages/objectql/dist/index.js after the change is the identical three-step function (lookup → two named throws → delegate); the as any and the return annotation never reached JS. The one deliberate JS change is service-datasource/plugin.ts's fallback probe respelling (getDatasourceDrivergetDriverByName), declared above: the old spelling was dead against every engine that has ever existed in-tree, so no behaviour that ever executed changes; the branch becomes reachable, which is the point.

H17-adjacent note: service-datasource/sqlite-driver-fallback.ts and tsup.config.ts (hold #10062 trigger files) are untouched — the diff in this package is src/plugin.ts only.

skills/** readings (required)

check:skill-refs regenerated skills/objectstack-data/references/_index.md: the new type-only IntrospectedSchema import pulls driver-sql.zod.ts + deploy-bundle.zod.ts into the skill's reference closure (generated artifact; gate red without it).

  • Changed file, whole-file: 63 → 65 lines (+2, both generated index rows).
  • Whole published package (sum of all skills/**/SKILL.md): 10505 → 10505 lines (no SKILL.md touched).

This path also makes the diff touch a governed surface (skills/**, Prime Directive #14): this PR is human-merge only — it stays DRAFT, is never queued, and auto-merge is never armed. Clause-②: yes — public contract member addition; the PR stays draft, the contract-review chain runs before enqueue.

Verification (all readings at head eab6d131b unless noted)

  • @objectstack/spec: pnpm typecheck → exit 0 (tsc --noEmit + scripts + check:test-typecheck OK, debt ledger unchanged at 55 files / 263 errors); full vitest run --maxWorkers=2420 files / 11228 tests passed (verify-lock verdict command-exit 0; suite ran at 0acd55c — the follow-up commit touches only data-engine.test.ts, whose file-scoped rerun at head is below).
  • @objectstack/objectql: pnpm typecheck → exit 0; full vitest run --maxWorkers=2231 files / 4106 tests passed (verify-lock verdict command-exit 0).
  • @objectstack/service-datasource: pnpm typecheck → exit 0; vitest run --maxWorkers=227 files / 585 tests passed (verify-lock verdict command-exit 0).
  • @objectstack/driver-sql: pnpm typecheck → exit 0 (audit leg).
  • Contract test files at head: data-driver.test.ts + data-engine.test.ts2 files / 35 tests passed.
  • Full workspace: turbo run build over ./packages/* + ./packages/*/* → exit 0 (70/70 tasks); turbo run typecheck --concurrency=2exit 0, "Tasks: 129 successful, 129 total" (verify-lock verdict command-exit 0) — the return-type tightening surfaced no latent consumer.
  • pnpm --filter @objectstack/spec check:generated → after regenerating the one proved-stale artifact (gen:skill-refs), exit 0; check:api-surface and check:docs green without regeneration.
  • Derived gate families (node scripts/pm/dispatch-gates.mjs, no paths — change set derived by the script from the merge base; derivation line: "gate list derived from the tree of 'objectstack-ai/objectstack' at commit …"): all 32 path-matched famil(ies) + the 5 convention-triggered test-file famil(ies) + check:nul-bytes run locally, every gate exit 0 at head (per-gate exits captured pipe-free; check:engine-double-contract prints OK — 401 pinned, 133 in the DEBT ledger, 2 exempt; check:type-check-debt --re-measure and check:dev-prereqs run after the full workspace build).

CI is expected to re-run the full farm; per the dispatch contract this PR is reported at draft time with CI possibly still in progress.

Out-of-scope finding filed: #11833 (two more consumer-local structural DataEngineLike declarations, observation-class).


Generated by Claude Code

…ection (#11493)
IDataDriver gains optional introspectSchema?(): Promise<IntrospectedSchema>;
IDataEngine gains optional introspectDatasource?(datasource): Promise<IntrospectedSchema>.
ObjectQL.introspectDatasource() tightens Promise<unknown> 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rxnd8cyFnoU8V5y21PaTsy
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rxnd8cyFnoU8V5y21PaTsy
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/objectql, @objectstack/service-datasource, @objectstack/spec, touching 7 documentable anchor(s).

12 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ai/actions-as-tools.mdx(via IDataEngine (symbol))
  • content/docs/ai/knowledge-rag.mdx(via IDataEngine (symbol))
  • content/docs/data-modeling/index.mdx(via IDataDriver (symbol))
  • content/docs/data-modeling/queries.mdx(via IDataDriver (symbol))
  • content/docs/kernel/contracts/data-engine.mdx(via IDataDriver (symbol), IDataEngine (symbol))
  • content/docs/kernel/contracts/index.mdx(via IDataDriver (symbol), IDataEngine (symbol))
  • content/docs/kernel/index.mdx(via IDataEngine (symbol))
  • content/docs/kernel/runtime-services/data-service.mdx(via IDataEngine (symbol))
  • content/docs/kernel/services-checklist.mdx(via IDataEngine (symbol))
  • content/docs/kernel/services.mdx(via IDataDriver (symbol), IDataEngine (symbol))
  • content/docs/protocol/knowledge.mdx(via IDataEngine (symbol))
  • content/docs/protocol/objectql/query-syntax.mdx(via IDataDriver (symbol), IDataEngine (symbol))

2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx(via IDataEngine (symbol))
  • content/docs/releases/v17.mdx(via IDataDriver (symbol), IDataEngine (symbol))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: ObjectQL (symbol, 64 pages)
  • the SDK route bridge reached 45 of 222 client-bound route-ledger rows — the other 177 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage

Coarse fallback — 127 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json bb418686547de42c8feae103fa2e41dd419aa171packageMentionDocs.

Which tree this was computed on

This run read content/docs from 58fe91b586da847f6cf0cb6aa6318a523d6d799d — the merge of head eab6d131ba4c791fc57c76c8ff4ae487300a3c41 into base bb418686547de42c8feae103fa2e41dd419aa171, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 58fe91b586da847f6cf0cb6aa6318a523d6d799d && git checkout 58fe91b586da847f6cf0cb6aa6318a523d6d799d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin bb418686547de42c8feae103fa2e41dd419aa171 eab6d131ba4c791fc57c76c8ff4ae487300a3c41 && git checkout -B drift-repro bb418686547de42c8feae103fa2e41dd419aa171 && git merge --no-ff eab6d131ba4c791fc57c76c8ff4ae487300a3c41
node scripts/docs-audit/affected-docs.mjs --json bb418686547de42c8feae103fa2e41dd419aa171

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs bb418686547de42c8feae103fa2e41dd419aa171 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-warren@claude