Skip to content

refactor(service-datasource): derive the admin plugin's engine view from the IDataEngine contract - #12011

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-11833-consumer-local-dataenginelike
Aug 25, 2026
Merged

refactor(service-datasource): derive the admin plugin's engine view from the IDataEngine contract#12011
os-sam merged 1 commit into
mainfrom
claude/issue-11833-consumer-local-dataenginelike

Conversation

@claude

@claudeclaudeBot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Part of #11833

Replaces the consumer-local structural DataEngineLike in service-datasource/src/datasource-admin-plugin.ts with the declared IDataEngine contract members — the #4251 B3 sweep pattern, extending #11493's ruling one file over.

This lands one of the card's two sites. The service-analytics half is deliberately NOT in this PR: measuring it turned up two forks and two signature mismatches that cannot be resolved without widening a contract, which the card rules is not a consumer-side call. That is why this says Part of and not a closing keyword — #11833 stays open for the analytics half. Detail below.

The card's real deliverable: the compatibility measurement

#11833 marked itself "a pattern observation, not a measured defect" and flagged the unverified half: whether the loosened local signatures are still assignable-compatible with the contract members they shadow. Nothing compiled them, so I compiled them — a throwaway probe asserting the real IDataEngine / IObjectQLEngine against each local declaration, whole-object and member-by-member, under this repo's strict: true.

datasource-admin-plugin.ts — the site this PR changes

memberdeclared equivalentverdict
findOne?IDataEngine.findOnecompatible — derived
find?IDataEngine.findcompatible — derived
insert?IDataEngine.insertcompatible — derived
update?IDataEngine.updatecompatible — derived
delete?IDataEngine.deletecompatible — derived
registerDriver?IObjectQLEngine.registerDrivermismatch + dead here — dropped
registerDatasourceDef?nonefork + dead here — dropped
getDriverByName?IDataEngine.getDriverByName?compatible but dead here — dropped

Two things the card did not expect:

1. Three members were dead. The card states "no dead member was found here." Three were: registerDriver, registerDatasourceDef and getDriverByName have zero call sites in datasource-admin-plugin.ts, and the type is file-private. That is the same never-matched-probe shape #11493 deleted. They are dropped rather than derived — hot pool (de)registration really runs through ConnectionEngineLike, which declares them and is the type the connection service is handed, so dropping leaves one declaration instead of two.

2. registerDriver? was actively wrong about the engine. It declared (driver: unknown, …); the contract is (driver: IDataDriver, …). Under strictFunctionTypes the real engine is therefore not assignable to the local view:

Type 'IObjectQLEngine' is not assignable to type 'DataEngineLike'.
Types of property 'registerDriver' are incompatible.
Types of parameters 'driver' and 'driver' are incompatible.
Type 'unknown' is not assignable to type 'IDataDriver'.

The local type promised the engine accepts any value as a driver. It does not.

service-analytics/plugin.ts — measured, deliberately NOT changed

The card describes this type as aggregate + execute. It actually declares five members, and only one is cleanly replaceable:

memberdeclared equivalentverdict
execute?IDataEngine.execute?compatible
aggregateIDataEngine.aggregatemismatch — needs a ruling
getObject?IObjectQLEngine.getObjectmismatch — contract returns unknown
resolveEffectiveDatasource?nonefork (real on ObjectQL :6530)
getDriverForObject?nonefork (real on ObjectQL :12361)
  • aggregate — the local type declares aggregations[].function: string; the contract declares the six-value enum 'count'|'sum'|'avg'|'min'|'max'|'count_distinct'. The widening is not vacuous: the authorable AggregationMetricType (packages/spec/src/data/analytics.zod.ts) has nine values — those six plus number/string/boolean custom-SQL expressions. So the auto-bridge can and does carry a method the engine contract does not admit. Substituting the contract member here would surface that as a build error, which is the correct signal — but resolving it is a design decision about how custom-SQL measures route, not something to settle with a cast.
  • getObject? — the contract member returns unknown. The local type claims a structured { fields?, external? } and ~10 call sites read ?.fields?.[…] off it. Substituting would replace real typing with casts at every one — a net loss, and contract-first says fix the contract's return type, not the consumer.
  • resolveEffectiveDatasource? / getDriverForObject? — real on the ObjectQL class, declared by no contract. Per the card these are forks: reported, not widened from the consumer side.

Which members stayed optional, and why

Partial<Pick<IDataEngine, …>>every member stays optional, exactly as the hand-written type had it. This is a deliberate graceful-degradation seam: a lightweight kernel can register a 'data' service with no durable CRUD, in which case all five are absent and datasource persistence degrades to in-memory (pre-existing behavior). The engine?.insert / engine?.find runtime probes are the other half of that same contract. Making any member required would change what a degraded boot does, which the card puts out of scope. Partial<> here is load-bearing, not shorthand.

Verification

Commit de81acf547. Type-only change — the emitted JS is unchanged (a type alias erases).

  • pnpm --filter @objectstack/service-datasource typecheckEXIT=0
  • pnpm --filter @objectstack/service-datasource testTest Files 27 passed (27) · Tests 585 passed (585), including the degraded-path cases (does not block boot when nothing is persisted (dev: in-memory store), the sys_metadata persist/restore-across-restart pair, and the durable-row delete)
  • pnpm --filter @objectstack/service-datasource buildDTS ⚡️ Build success
  • pnpm lint (full repo, eslint . --no-inline-config) → VERDICT command-exit 0
  • Derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, all green, each quoting its own verdict: check:published-files, check:slot-lookup ("ratchet holds: 107 unswept site(s) … none new"), check:test-source-alias, check:type-source-resolution, check:nul-bytes, check:ratchet-remedy-authority, check:pm-skill-ratchet, check:query-options-erasure ("ratchet holds: 67 unswept non-test site(s) … none new"), check-plugin-teardown-shape, check-affected-docs, check-drift-comment

Ablation

Predicted in writing before running. A runtime ablation of a type-only change is vacuous by construction, so the real ablation is at the compiler, and it has a control leg:

  • Predicted: adding the fork member registerDatasourceDef back into the Pick<> list fails with TS2344 "does not satisfy the constraint keyof IDataEngine".
  • Observed: exactly that — ABLATED_EXIT=2, error TS2344: Type '"find" | "registerDatasourceDef" | …' does not satisfy the constraint 'keyof IDataEngine'. (First attempt was a malformed 3-arg Pick and failed as TS2314 arity instead; re-run with correct union syntax gave the predicted diagnostic.)
  • Control leg — the same fork member under the original structural type on origin/main: CONTROL_EXIT=0, green. A member no contract declares typechecked clean. That is the drift The service-lookup any rule misses getService<any>(...) — 80 sites erase the slot contract, 3 of them inside the rule's own scope #4251 exists to prevent, demonstrated live, and it is what this PR closes at this seam.

Mutation was confirmed on disk each leg by grepping the injected marker and the displaced anchor (never a bare git diff --stat), each leg carried a trap … EXIT INT TERM restore, and after every leg disk == index == HEAD was verified with git diff --quiet / git diff --cached --quiet plus an empty git status --porcelain.

No changeset

Type-only, no runtime behaviour change, no public surface moves — the replaced type is file-private and the members dropped had no call sites. Labelled skip-changeset.

Out-of-scope finding

Filed as #12010 (unassigned): a third consumer-local structural engine type, ConnectionEngineLike — exported, and the one datasource-admin-plugin.ts actually casts to — carries the same registerDriver? mismatch plus three members no contract declares (registerDatasourceDef, markDatasourceUnavailable, clearDatasourceUnavailable). Not touched here: closing it requires widening a contract, which is a maintainer call.


Generated by Claude Code

…rom IDataEngine
Replace the consumer-local structural `DataEngineLike` in
`datasource-admin-plugin.ts` with a type derived from the declared
`IDataEngine` contract — the #4251 B3 sweep pattern, extending #11493's
ruling one file over. A private structural re-declaration meets no compiler
on the producer side, so engine-surface drift lands silently in the consumer.
The five members this seam actually uses (`findOne`/`find`/`insert`/`update`/
`delete`) all have compatible declared equivalents on `IDataEngine`, verified
member-by-member by substitution before the swap.
`registerDriver?`, `registerDatasourceDef?` and `getDriverByName?` are dropped
rather than derived: all three had ZERO call sites in this file and the type
is file-private — the same never-matched-probe shape #11493 deleted. Hot pool
(de)registration is really driven through `ConnectionEngineLike`, which
declares them and is the type the connection service is handed.
Optionality is preserved exactly (`Partial<…>`): this is a deliberate
graceful-degradation seam and making any member required would change what a
degraded boot does.
Type-only change — no runtime behaviour changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APWX2AwT3a4xDcjPCe8bk4
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 25, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/service-datasource, touching 6 documentable anchor(s).

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

  • content/docs/api/data-flow.mdx(via findOne (symbol), findOne (literal))
  • content/docs/automation/hook-bodies.mdx(via findOne (symbol), findOne (literal))
  • content/docs/automation/webhooks.mdx(via findOne (symbol), findOne (literal))
  • content/docs/kernel/contracts/data-engine.mdx(via findOne (symbol), getDriverByName (symbol), findOne (literal))
  • content/docs/kernel/contracts/index.mdx(via findOne (symbol), findOne (literal))
  • content/docs/kernel/events.mdx(via findOne (symbol), findOne (literal))
  • content/docs/permissions/attachments-access.mdx(via findOne (symbol), findOne (literal))
  • content/docs/permissions/field-level-security.mdx(via findOne (symbol), findOne (literal))
  • content/docs/permissions/record-view-auditing.mdx(via findOne (symbol), findOne (literal))
  • content/docs/permissions/rls.mdx(via findOne (symbol), findOne (literal))
  • content/docs/protocol/objectql/schema.mdx(via findOne (symbol), findOne (literal))
  • content/docs/ui/react-pages.mdx(via findOne (symbol), findOne (literal))

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

  • content/docs/releases/v15.mdx(via findOne (symbol), findOne (literal))
  • content/docs/releases/v16.mdx(via findOne (symbol), findOne (literal))
  • content/docs/releases/v17.mdx(via findOne (symbol), findOne (literal))

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
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • 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
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 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 2cc71222459e91964e883419611a820c28302429packageMentionDocs.

Which tree this was computed on

This run read content/docs from dc41fe3e0ac53b43042120f5fb2d7f46c14e53ad — the merge of head de81acf547a4b7122453f94d9e58df9da051d05f into base 2cc71222459e91964e883419611a820c28302429, 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 dc41fe3e0ac53b43042120f5fb2d7f46c14e53ad && git checkout dc41fe3e0ac53b43042120f5fb2d7f46c14e53ad
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2cc71222459e91964e883419611a820c28302429 de81acf547a4b7122453f94d9e58df9da051d05f && git checkout -B drift-repro 2cc71222459e91964e883419611a820c28302429 && git merge --no-ff de81acf547a4b7122453f94d9e58df9da051d05f
node scripts/docs-audit/affected-docs.mjs --json 2cc71222459e91964e883419611a820c28302429

⚠️ 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 2cc71222459e91964e883419611a820c28302429 → 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

size/sskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-sam@claude