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

os-sam merged 1 commit into
mainfrom
claude/issue-11833-consumer-local-dataenginelike

Conversation

@claude

@claude claude Bot 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

member declared equivalent verdict
findOne? IDataEngine.findOne compatible — derived
find? IDataEngine.find compatible — derived
insert? IDataEngine.insert compatible — derived
update? IDataEngine.update compatible — derived
delete? IDataEngine.delete compatible — derived
registerDriver? IObjectQLEngine.registerDriver mismatch + dead here — dropped
registerDatasourceDef? none fork + 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:

member declared equivalent verdict
execute? IDataEngine.execute? compatible
aggregate IDataEngine.aggregate mismatch — needs a ruling
getObject? IObjectQLEngine.getObject mismatch — contract returns unknown
resolveEffectiveDatasource? none fork (real on ObjectQL :6530)
getDriverForObject? none fork (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 typecheck → EXIT=0
  • pnpm --filter @objectstack/service-datasource test → Test 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 build → DTS ⚡️ 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
@claude claude Bot 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 2cc71222459e91964e883419611a820c28302429 → packageMentionDocs.

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.

Merged via the queue into main with commit 35b4c6e Aug 25, 2026
35 checks passed
os-litant pushed a commit that referenced this pull request Aug 27, 2026
…the declared contracts

Replaces the consumer-local structural `DataEngineLike` in
`service-analytics/src/plugin.ts` with the declared `IDataEngine` /
`IObjectQLEngine` members - the #4251 B3 sweep pattern, and the second
of the two sites named by #11833 (the first landed as PR #12011).

The `aggregate` narrowing surfaced the mismatch the structural type hid:
the local declaration typed `aggregations[].function` as `string` where
the contract declares the six-value `AggregationFunction`. Closed by
parsing with the spec enum itself at the forwarding site - not by
widening back to `string` (which hid it) and not by a cast.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194kbQJxUvv2yvsGRtuXpP5
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 1, 2026
…m the declared contracts (objectstack-ai#12777)

* refactor(service-analytics): derive the auto-bridge engine view from the declared contracts

Replaces the consumer-local structural `DataEngineLike` in
`service-analytics/src/plugin.ts` with the declared `IDataEngine` /
`IObjectQLEngine` members - the objectstack-ai#4251 B3 sweep pattern, and the second
of the two sites named by objectstack-ai#11833 (the first landed as PR objectstack-ai#12011).

The `aggregate` narrowing surfaced the mismatch the structural type hid:
the local declaration typed `aggregations[].function` as `string` where
the contract declares the six-value `AggregationFunction`. Closed by
parsing with the spec enum itself at the forwarding site - not by
widening back to `string` (which hid it) and not by a cast.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194kbQJxUvv2yvsGRtuXpP5

* test(service-analytics): pin the aggregate bridge's engine vocabulary

Positive control (a declared function reaches the engine as `function`)
plus the refusal (a method outside the engine's six never reaches the
engine, and answers in the bare-Error/undeclared-500 tier rather than a
400 that would blame the caller for host drift).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194kbQJxUvv2yvsGRtuXpP5

* chore: changeset for the analytics auto-bridge contract derivation

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194kbQJxUvv2yvsGRtuXpP5

---------

Co-authored-by: claude[bot] <claude-bot@anthropic.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant