You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Tighten the external-datasource driver return contract so a mis-shaped custom driver fails at compile time (primaryKeyReader et al.) #11381
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.
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).
Supplemental surface: packages/services/service-datasource/src/external-datasource-service.ts — docblock lines only. The primaryKeyReader shim docblock states verbatim that the handle "types the handle as introspectSchema?(): Promise<unknown>"; this card's diff falsifies that line, so the shim's documented expiry-condition record gets a coupled truth update. The shim's code is untouched.
Plus the new type-level test file under src/__tests__/ and one changeset (both inside the claim's declared surface).
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.introspectSchemaPromise<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.
{
"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"
]
}
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.
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.
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.
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.
Activity
os-steve commentedon Aug 23, 2026
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-contractWorktree:
../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/specand/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-reviewhung 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
majorchangeset is required but gated bycheck-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
os-steve commentedon Aug 23, 2026
File-surface supplement to the PM claim (dev agent on
claude/issue-11381-driver-return-contract, same dispatch):b6d9432d1a: the contract isDatasourceDriverHandle.introspectSchema?(): Promise<unknown>inpackages/services/service-datasource/src/contracts/datasource-driver-factory.ts(exactly where service-datasource: theprimaryKeyReaderdocblock now describes a producer that no longer exists, and the union read'sisPrimaryarm has no in-tree producer left #11123's option C names it — NOT inpackages/spec; the spec side,IntrospectedSchemaincontracts/schema-diff-service.ts, is already the truthful contract after spec: withdraw the never-honored introspectionindexespromise; widendefaultValue/maxLengthto the measured emitted types #11270 and is not edited).packages/services/service-datasource/src/external-datasource-service.ts— docblock lines only. TheprimaryKeyReadershim docblock states verbatim that the handle "types the handle asintrospectSchema?(): Promise<unknown>"; this card's diff falsifies that line, so the shim's documented expiry-condition record gets a coupled truth update. The shim's code is untouched.src/__tests__/and one changeset (both inside the claim's declared surface).Generated by Claude Code
feat(service-datasource): DatasourceDriverHandle.introspectSchema dec…
claude commentedon Aug 23, 2026
Contract review — PASS (fable seat, session
5213b871-5164-5bc3-8874-28b336bbcd40, hourly sub-round; fuse readingget_session→external_metadata.last_served_model=claude-fable-5, matchingCONTRACT_REVIEW_TIERread from origin/main; authorization: maintainer 2026-08-23 「要不还是你挂个定时处理审核吧」). Independence: not this seat's dispatch — independent review.Reviewed PR #11492 @
b6583cd03eagainst the actual diff — option C of the #11123 ruling (2026-08-23 batch 「其他接受你的建议」).DatasourceDriverHandle.introspectSchemaPromise<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.Verdict: PASS. Clearing
needs:contract-reviewon both carriers. Enqueue/flip belongs to the dispatching seat's landing window.Generated by Claude Code
os-steve commentedon Aug 23, 2026
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
os-steve commentedon Aug 23, 2026
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 headb6583cd03e.primaryKeyReaderdocblock now describes a producer that no longer exists, and the union read'sisPrimaryarm 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'sfeat(spec)guess corrected to the owning package) tightensPromise<unknown>→ the one spec introspection contract. Compile-fail quality demonstrated: the identity pin reds directly on revert; the six@ts-expect-errorpins name the field for every shipped defect shape (isPrimary, tables-only driver-sql:introspectSchema()returns only{ tables }— the specIntrospectedSchemacontract'sdialectand (required)introspectedAtare never emitted #10998, bare-contractprimaryKeys,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.check-changeset-no-major's own header states the launch-window convention verbatim ("we ship breaking changes asminor", pre-1.0 lockstep semantics), sominor+ explicit BREAKING body + the mechanicaladr-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.IDataDriverdeclares nointrospectSchema, andIDataEngine.introspectDatasource()returnsPromise<unknown>#11493 (the engine-registration pathIDataDriver/IDataEngine.introspectDatasourcestill typed tounknown— wider blast radius) correctly filed as its own card rather than folded.needs:contract-reviewcleared from both carriers (card + PR) in this same stroke. Landing: flip ready → every check green → merge queue.Fixes #11381closes this card at merge; hygiene at the landing stroke.Generated by Claude Code