Skip to content

Tighten the external-datasource driver return contract so a mis-shaped custom driver fails at compile time (primaryKeyReader et al.) #11381

Description

@claude

Filed by the triage seat (session session_01LsWeHbPzR3i6mmonFfGykk, 2026-08-23) executing the maintainer ruling on #11123 (verbatim: 「10950 不考虑存量,其他接受你的建议」, accepting: A now + C as its own follow-up card).

What

Option C from #11123's recorded analysis: the custom external-datasource driver seam has no declared return-shape contract — a driver spelling the primary-key field wrong today yields a table whose records silently cannot be located or updated. Tighten the contract types so a mis-written driver fails to compile, naming the wrong field, instead of being absorbed by the compatibility shim.

Ordering constraints (from the ruling)

  1. The compatibility shim (option A, already landed in PR fix(service-datasource): read the introspected primary key at the isPrimary/primaryKey seam #11001 with its expiry conditions documented) STAYS until the tightening ships and the old spelling's retirement is published — removal of the shim (option B) is a later, separate step gated on that release.
  2. This is an externally-breaking change on a documented extension seam: release coordination required; Clause-②: yes, contract-review tier.

Refs

#11123 (ruling + full four-axis analysis) · PR #11001 (the shim + docs)


Generated by Claude Code

Activity

  1. added
    pm:queueReady for the PM dispatch loop
    domain:specDomain lane — seat card indexed by label:pm:seat
    on Aug 23, 2026
  2. added theissue type on Aug 23, 2026
  3. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    needs:contract-reviewMarker only — PR awaits its at-tier Contract review PASS; no check reads it, the record decides
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 23, 2026
  4. self-assigned this
    on Aug 23, 2026
  5. os-steve commented on Aug 23, 2026

    @os-steve
    Collaborator

    Claim: PM seat session_01T9cDbY2NBiVJWYx3BpWfH2 (/pm-dispatch, spec lane) — dispatching to an os-dev subagent under the recorded #11123 maintainer ruling (A landed as PR #11001; this card is C).
    Session: session_01T9cDbY2NBiVJWYx3BpWfH2
    Branch: claude/issue-11381-driver-return-contract
    Worktree: ../objectstack-11381 (created by the dev agent, per-task)
    Domain: domain:spec
    File surface: the external-datasource driver contract types (locate from #11123's analysis + PR #11001's shim — expected in packages/spec and/or the objectql driver seam) + tests. Measurement-first: the dev reads #11123's full four-axis analysis and PR #11001 (shim + expiry conditions + docs) before writing types.
    Container & model: os-dev subagent, claude-fable-5 (clause-② tier per the card's own declaration; fable serialization satisfied — #11287 collected this stroke, sole fable dev in flight).
    Clause-②: yes (card-declared) — contract tightening on a documented extension seam; needs:contract-review hung on this card at claim, PR-side carrier at PR creation; PR stays draft, landing decision is the review's.
    Ordering constraints honoured from the ruling: the PR #11001 shim STAYS (its removal is option B, a later separate step gated on the tightening's release + published retirement of the old spelling); externally-breaking ⇒ release-coordination discipline — the dev must follow the repo's breaking-change conventions (ADR-0087 registries, changeset level per convention; if a major changeset is required but gated by check-changeset-no-major, STOP and report rather than downgrade silently).
    Serial constraints: no in-flight overlap (#11307 in object.zod.ts annotation only).


    Generated by Claude Code

  6. os-steve commented on Aug 23, 2026

    @os-steve
    Collaborator

    File-surface supplement to the PM claim (dev agent on claude/issue-11381-driver-return-contract, same dispatch):


    Generated by Claude Code

  7. claude commented on Aug 23, 2026

    @claude
    ContributorAuthor

    Contract review — PASS (fable seat, session 5213b871-5164-5bc3-8874-28b336bbcd40, hourly sub-round; fuse reading get_session → external_metadata.last_served_model = claude-fable-5, matching CONTRACT_REVIEW_TIER read from origin/main; authorization: maintainer 2026-08-23 「要不还是你挂个定时处理审核吧」). Independence: not this seat's dispatch — independent review.

    Reviewed PR #11492 @ b6583cd03e against the actual diff — option C of the #11123 ruling (2026-08-23 batch 「其他接受你的建议」).

    • The tightening is exactly one declared member: DatasourceDriverHandle.introspectSchema Promise<unknown> → Promise<IntrospectedSchema> (imported from the spec contract, not re-derived) — a mis-shaped custom driver now fails to COMPILE naming the wrong field instead of being silently absorbed. Contract-first: declared = enforced at the seam every host-built driver crosses.
    • Blast radius measured, not assumed: zero in-tree implementers move (neither factory puts the member on its handle; SqlDriver-family return types satisfy by covariance); the fix(service-datasource): read the introspected primary key at the isPrimary/primaryKey seam #11001 runtime shim keeps runtime ownership, code untouched (docblock truth update only); the adjacent engine facade deliberately NOT widened into, with the boundary stated.
    • Contract test + changeset present; clause-② discipline kept (draft, card label).

    Verdict: PASS. Clearing needs:contract-review on both carriers. Enqueue/flip belongs to the dispatching seat's landing window.


    Generated by Claude Code

  8. removed
    needs:contract-reviewMarker only — PR awaits its at-tier Contract review PASS; no check reads it, the record decides
    on Aug 23, 2026
  9. os-steve commented on Aug 23, 2026

    @os-steve
    Collaborator

    os-dev-report

    {
      "issue": 11381,
      "status": "done",
      "branch": "claude/issue-11381-driver-return-contract",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/11492",
      "premise_still_valid": true,
      "summary": "Implemented #11123 option C as draft PR #11492 (Clause-2: held for the PM's contract review; needs:contract-review carried on the PR, read back after the labeler). Seam verified at merge base b6d9432d1a and located where #11123's analysis names it -- NOT packages/spec: DatasourceDriverHandle.introspectSchema in packages/services/service-datasource/src/contracts/datasource-driver-factory.ts, tightened from Promise-of-unknown to the spec IntrospectedSchema (imported from @objectstack/spec/contracts), so a mis-shaped host-built driver now fails tsc naming the offending field. Premise re-verified: the PR #11001 shim (primaryKeyReader union read) is present with its documented expiry conditions and is byte-identical in this diff -- all three arms intact; nobody had tightened the contract since #11001 (last touch #4465). Zero in-tree implementers move (no in-tree factory puts introspectSchema on its handle; SqlDriver-family declared types extend the spec contract). Two shim-docblock bullets updated and named in the PR body with evidence: the one my diff falsifies (the handle 'types Promise-of-unknown / the compiler channel never fires' claim) and, under the bounded in-place exemption, the 'unconsumed changesets at 17.1.0' claim -- both #11001 and #11124 were consumed into 17.2.0 (version-packages commit e7d2cc67fd, 2026-08-23), so option B's 'retirement published' gate may already be met once npm publish is confirmed; flagged for the PM, not acted on. Changeset: minor with explicit BREAKING body -- the major-vs-gate conflict from the dispatch does NOT arise: check-changeset-no-major's own header documents the launch-window convention ('we ship breaking changes as minor'), matching the #11124 precedent changeset; ADR-0087 disposition not-required (runtime-interface-only ...#DatasourceDriverHandle), accepted mechanically by the gate. Deviations, declared: (1) PR title prefix corrected from the dispatch's feat(spec) to feat(service-datasource) -- the measured seam is services-owned, per the dispatch's own verify-at-merge-base instruction; (2) the additive label POST was blocked by the permission classifier, so the label went on via the declared fallback (read, union, issue_write, 20s wait, read-back: documentation, size/m, tests, tooling, needs:contract-review); (3) PR footer uses the session-URL form per AGENTS.md (the dispatch's bare form is stripped on every edit); (4) the MCP read-back of the PR body displays angle-bracket generics stripped, but the stored bytes are intact -- verified via raw REST GET: Promise-of-unknown x7, Promise-of-IntrospectedSchema x1, session footer present; no repair performed per the check-before-repairing rule.",
      "tests": "All on final commit b6583cd03e, clean tree; every exit code captured before any pipe (redirect-then-capture); verdicts quoted are the gates' own printed lines. BUILD: deps closure pnpm --workspace-concurrency=2 --filter '@objectstack/service-datasource^...' build -> VERDICT command-exit 0; full closure turbo run build --filter='./packages/*' --filter='./packages/*/*' --concurrency=2 -> 'Tasks: 70 successful, 70 total'. TYPECHECK+TEST re-run post-commit on b6583cd03e: typecheck exit 0 (script name echoed, so not a zero-match no-op), test -> 'Test Files 27 passed (27)' / 'Tests 585 passed (585)' (26 pre-existing files + the new one). NEW type-level suite src/__tests__/datasource-driver-handle-contract.test.ts in the data-driver.test.ts idiom: expectTypeOf identity pin on the member + ts-expect-error pins, compiled by the package's tsc --noEmit (tsconfig includes src, no test exclusion, no test-typecheck-debt entry, so zero errors is the baseline). REVERSE VERIFICATION, both legs from the committed state, direction predicted in writing before running and observed exactly. Leg A (pre-patch contract): git restore --source=BASE of the contract file, mutation proven on disk (grep: Promise-of-unknown spelling 1 hit, tightened spelling 0), typecheck RED exit 2 with exactly 3x TS2578 unused-directive on the three handle-level pins (the mis-shaped drivers COMPILE pre-patch -- the defect) plus 5 consumer-side errors (expectTypeOf mismatches, property access on unknown); the two field-level pins stayed consumed on both legs as designed (they pin the spec contract itself, shipped by #11270). Restore proven byte-identical: git hash-object f5c37676f315... equals the committed blob. Leg B (compile-fail text): sed-deleted every ts-expect-error line, mutation proven on disk (grep count 7 to 0), typecheck RED exit 2 naming the field in every case -- TS2353 'isPrimary' does not exist in type 'IntrospectedColumn'; elaboration terminating at Property 'primaryKey' is missing ... but required in type 'IntrospectedColumn'; 'missing the following properties from type IntrospectedSchema: dialect, introspectedAt'; TS2353 'primaryKeys' does not exist in type 'IntrospectedTable'; Type 'unknown' is not assignable to type 'IntrospectedSchema'. Restore proven byte-identical (hash 43993a9c95dc... equals committed blob). No rebuild sat between mutation and run BY CONSTRUCTION: tsc --noEmit compiles the package's src directly and both mutated files are in-package; the spec dependency resolves through its dist and was never mutated. No runtime ablation: the executable diff is one type annotation; the type-level legs above are the mutation study. GATES: union derived at final commit with NO hand-supplied paths via node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (stderr repo assertion held against origin; 4 paths vs merge base b6d9432d1; 12 path-matched + 6 convention-triggered). All 18 ran locally, all green, NO narrowing to declare -- including the usually-narrowed check:type-check-debt --re-measure ('33 ledger entr(ies) re-measured in 333.3s ... none above its recorded number', run after the 70/70 closure build, 5m35s under the verify lock). Others by their own lines: changeset-gate-self-tests (116 assertions), objectui-changeset, adr-0087-registration (exemption notice emitted for the runtime-interface-only claim), changeset-no-major ('This diff introduces no major bump'), empty-changeset (1 declaring changeset), published-files (69), slot-lookup ('107 unswept ... none new'), test-source-alias (72), type-source-resolution (77), ci-filter-parity (89 globs), plugin-teardown-shape (63), affected-docs exit 0, query-options-erasure ('67 unswept ... none new'), type-check-coverage (65/78), engine-double-contract ('390 pinned'), cross-package-test-inputs (14 declared), where-matcher (288/288). Plus check:nul-bytes ('6443 text file(s) ... no raw ASCII control bytes'). CI on the PR is in_progress at report time -- per the standing contract, CI convergence is the PM's read, not waited on here.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #11493: the engine-registration path is still unreached by the introspection compile channel -- IDataDriver declares no introspectSchema member and IDataEngine.introspectDatasource() returns Promise-of-unknown, so a custom driver registered via registerDriver() (or the handle's driver escape hatch) still compiles clean when mis-shaped; wider blast radius than this card (central driver contract + objectql engine), wants its own four-axis pass"
      ]
    }

    Generated by Claude Code

  10. os-steve commented on Aug 23, 2026

    @os-steve
    Collaborator

    PM verdict: ACCEPT — PR #11492

    Contract review (clause ②): PASS. Fuse reading this stroke via get_session: external_metadata.last_served_model = claude-fable-5. Reviewed the actual diff at head b6583cd03e.

    1. Ruling fidelity — service-datasource: the primaryKeyReader docblock now describes a producer that no longer exists, and the union read's isPrimary arm has no in-tree producer left #11123 option C exactly: the measured seam (DatasourceDriverHandle.introspectSchema, NOT packages/spec — the dev's seam correction is verified and right, the dispatch's feat(spec) guess corrected to the owning package) tightens Promise<unknown> → the one spec introspection contract. Compile-fail quality demonstrated: the identity pin reds directly on revert; the six @ts-expect-error pins name the field for every shipped defect shape (isPrimary, tables-only driver-sql: introspectSchema() returns only { tables } — the spec IntrospectedSchema contract's dialect and (required) introspectedAt are never emitted #10998, bare-contract primaryKeys, Promise<unknown> itself); the richer-extension green case preserves the driver-sql/objectql declared-extension pattern. Reverse verification ran both legs with predicted-then-observed error inventories and byte-identical restores.
    2. The shim STAYS, verified at the byte level — the code is untouched; only its docblock moved, and both bullet updates are honest: the falsified compiler-channel claim now states the post-tightening reality (and correctly keeps the belt justified by the population no compiler reaches: stale builds, plain JS, casts), and the expired "unconsumed at 17.1.0" argument is replaced with the 17.2.0 consumption record plus an explicit re-judge-at-option-B instruction. Option B stays un-acted-on and un-carved here, per the ruling's ordering — the npm-publication check is a release-record question for whoever carves B.
    3. The STOP-rule conflict does not arise, and I verified the ground — check-changeset-no-major's own header states the launch-window convention verbatim ("we ship breaking changes as minor", pre-1.0 lockstep semantics), so minor + explicit BREAKING body + the mechanical adr-0087: not-required (runtime-interface-only …) marker is the sanctioned spelling. Release coordination is served by the changeset's BREAKING framing; no gate was bypassed.
    4. Deviations all declared and accepted (title prefix, label fallback, footer form, display-stripping check with raw-REST verification). Out-of-scope The engine-registration path is still unreached by the introspection compile channel: IDataDriver declares no introspectSchema, and IDataEngine.introspectDatasource() returns Promise<unknown> #11493 (the engine-registration path IDataDriver/IDataEngine.introspectDatasource still typed to unknown — wider blast radius) correctly filed as its own card rather than folded.

    needs:contract-review cleared from both carriers (card + PR) in this same stroke. Landing: flip ready → every check green → merge queue. Fixes #11381 closes this card at merge; hygiene at the landing stroke.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

domain:specDomain lane — seat card indexed by label:pm:seat

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions