Skip to content

fix(spec): a user field over a text column passes the type-compat check; ADR-0015 names the odata vocabulary - #28011

Merged
os-zhuang merged 2 commits into
mainfrom
claude/project-thread-r6cu87
Oct 30, 2026
Merged

os-zhuang merged 2 commits into
mainfrom
claude/project-thread-r6cu87

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

epic:#27261#1 · marchtian · session_017ihL2dhraZ7BwSanQPGEAb

Requested by 田騏 · project thread

Fixes #27917

Clause-②: yes (widening)
Metadata-protocol: none

Fable review owed. The review quota was out when this PR was opened. It stays draft until the independent full review, the contract review PASS on head and an authorized approval (Tier H: docs/adr/**).

维护者速读

  • 改了什么: 一是 ADR-0015 §4.6 的方言清单补上 odata,并指向 DIALECT_ALIASES.odata(代码在 feat(datasource): import objects from an OData datasource's $metadata, and import several entity sets from Studio and MCP #27880 已加)。二是 user 字段加入类型矩阵 text 行的 exact 集合:外部表里用文本列存平台用户 id 的 user 字段,启动校验不再报 type_mismatch 错误。整数行(integer / bigint)不加。
  • 为什么: 实测平台用户 id(sys_user.id)永远是字符串:better-auth 为每个用户生成 32 位字母数字串,引擎直写时 SQL 驱动生成 16 位 nanoid,既不是 UUID 也不是整数。文本列装得下,整数列装不下,所以只放宽 text 行。
  • 风险与回滚: 只放宽一个接受集合,之前通过的不会变成失败;回滚即 revert。
  • 需要您做的: 合并前批准(改了 ADR,Tier H)。

What changes

  • packages/spec/src/data/type-compat.ts. user joins the text row's exact set (it was already exact under uuid). A comment on the row records the measurement and why the integer rows stay closed while lookup / master_detail are open there: a user field's target is fixed to sys_user, so the column holds a platform user id, whose shape is the platform's and not the remote system's.
  • docs/adr/0015-external-datasource-federation.md. §4.6 lists odata among the dialects, with one sentence naming the two non-SQL vocabularies: DIALECT_ALIASES.mongo (BSON type names) and DIALECT_ALIASES.odata (EDM primitive types, except Edm.Duration and the geography / geometry families, left unmapped for review). The Amended by line records it as a dialect-list-only amendment.
  • packages/spec/src/data/type-compat.test.ts. One case pins the decision both ways: user is exact over varchar(255), text, postgres citext, bigquery STRING, odata Edm.String (and still over uuid / Edm.Guid); it is refused over integer, bigint, postgres int8, Edm.Int32, Edm.Int64, boolean and jsonb, while lookup stays exact over the same integer columns. A second case pins that the draft still suggests text for a text column (the suggestion is read from the column alone).
  • .changeset/27917-type-compat-user-text.md. @objectstack/spec patch, with both declaration lines.

Why

The measurement (sys_user.id)

  • better-auth 1.7.12, the version @objectstack/plugin-auth pins. Five auth.api.signUpEmail calls through a createAdapterFactory adapter configured as objectql-adapter.ts configures it (no advanced.database.generateId) wrote five user rows. Every id was 32 characters, all alphanumeric, not all-digit, not UUID-shaped (for example jaS4bSf4BGL50JWp7UaSlvZFEEAKHKiE). Admin create and the user import go through auth.api.createUser, the same id path.
  • The SQL driver. A row written through the engine without an id gets nanoid(DEFAULT_ID_LENGTH) with DEFAULT_ID_LENGTH = 16 (packages/drivers/driver-sql/src/sql-driver.ts), a string; a caller-supplied id is stored verbatim.
  • Decision. A text column carries every one of those, so user is exact under text. No path produces an integer user id, so user stays refused under integer / bigint. This follows the card's own condition ("the integer rows only if a platform user id can be an integer").

What an author can declare

The user field's own schema is unchanged. The only reader of the matrix that refuses is Gate 2 (validateObject in service-datasource): a federated object with a user field over a text column used to get a type_mismatch error (boot fails under onMismatch: 'fail') and now passes. Nothing that passed before fails now. The draft generator never suggests user (suggestion is per column and stays text), so imports are unchanged.

Verification

  • pnpm --filter @objectstack/spec exec vitest run src/data/type-compat.test.ts: 29 passed.
  • Reverse check from the committed fix: removing 'user' from the text row alone fails the new case at the varchar(255) assertion (expected false to be true, line 190); restored with git checkout HEAD --.
  • dispatch-gates --commands derived 85 families on ecf8b5c4; all 85 run with exit codes recorded before any pipe. Four exited 3 on the first pass (not measured: plugin-teardown self-test fixture, doc-formula-expressions, dual-build-cjs-loads, lean-entry-closure) and exit 0 after fetching the fixture commit and building ./packages/**. --ran: 85 derived, 85 run, 0 NOT-MEASURED (a derived zero).
  • pnpm --filter @objectstack/spec typecheck: exit 0 (test-typecheck ledger held). vitest run src/data/ in spec: 149 files, 4588 passed.

Contract review 6518256754 (FAIL on ecf8b5c4), fixed in 10deba35; delta 6518728501 PASS

  • Must-fix, ADR §4.6. The added sentence called odata the one vocabulary that is not SQL; mongo maps BSON type names too, as the SqlDialect doc comment says. The sentence now names both, pointing at DIALECT_ALIASES.mongo and DIALECT_ALIASES.odata.
  • Should-fix, same sentence. It now says the odata arm leaves Edm.Duration and the Edm.Geography* / Edm.Geometry* families unmapped so a draft flags them for review.
  • Should-fix, type-compat.ts row comment. "Never a UUID" held for minted ids only. The comment now says a minted id is never a UUID or an integer, and a caller-supplied id is stored verbatim as a string, which is why the uuid row keeps user.
  • PR body. The opening paragraph no longer names a model beyond the card's own "Fable review owed" marker.
  • The review's ③ also closed the memory and Mongo driver note below: driver-memory mints an objectName-timestamp-counter string and driver-mongodb a 16-character nanoid.

Delta review 6518728501 by the same reviewer: PASS on 10deba35, every finding closed.

Re-run in full on 10deba35: dispatch-gates --commands derived the same 85 families; all 85 run with exit codes recorded. Five spec gates (api-surface, browser-reachable-entries, dual-source-exports, entry-nameability, exported-any) exited 3 on the stale spec dist/ and exit 0 after pnpm --filter @objectstack/spec build. --ran: 85 derived, 85 run, 0 NOT-MEASURED (a derived zero).

Acceptance notes

  • Integer rows. Left closed by measurement, not by caution. If a deployment ever configures better-auth with numeric ids, that is a new id shape and its own card.
  • Memory and Mongo drivers mint their own ids for engine-direct writes, both strings (read by the contract review, not measured here). Every user the platform creates goes through better-auth, which mints the id before the driver sees the row.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (A matrix entry and its test only.)
  • 2. Every name I added uses the casing for its kind. (No new names.)
  • 3. No formatter ran (pnpm lint is CI's).
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: the type-compat matrix and its test, no schema.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the 维护者速读 block is the repo's own Chinese reader section).
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_017ihL2dhraZ7BwSanQPGEAb

…ck; ADR-0015 names the odata vocabulary

A `user` field's target is fixed to sys_user, so its column holds a platform
user id. Measured: better-auth mints a 32-character alphanumeric string for
every user it creates, and the SQL driver mints a 16-character nanoid for a
row the engine writes without one, so the id is always a string and never an
integer. `user` joins the text row's exact set; the integer and bigint rows
keep refusing it, though lookup and master_detail are exact there.

ADR-0015 §4.6 lists `odata` among the dialects the matrix knows.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ihL2dhraZ7BwSanQPGEAb
@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: master_detail (literal, 39 pages)
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 168 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json ab7599665631c2da6736c6c48abcf3dea5221575 → packageMentionDocs.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

epic:#27261#1 · marchtian · session_017ihL2c5z3q4h8inrn5kwqV
Contract-review-handoff: triage — CONTRACT_REVIEW_TIER unavailable (HTTP 429 rate_limit, 2026-10-30T07:46Z) · head ecf8b5c

The seat's at-tier review call is refused by the model quota (probe just now: HTTP 429 rate_limit), and per contract-review.md the tier is not lowered. One same-shape CONTRACT_REVIEW_TIER record is owed on this head, with ④. Under the 2026-10-29T19:39Z order it also serves as the full review of the spec and ADR change.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

epic:#27261#1 · marchtian · session_017ihL2c5z3q4h8inrn5kwqV
Addendum to the handoff above, for ④ on head ecf8b5c

The epic PM weighed whether admitting user on the text row is a design decision outside the card. I judge that it is not. Rulings 6512853588 and 6512860835 (option A on #27745) made lookup / master_detail exact on text, integer and bigint rows. The matrix judges column storage shape against the field's value shape, and user is a lookup whose target id (sys_user.id) is always a string. The card names this outcome as the measured one. Integer rows stay refused for user, so the PR applies the ruling's principle and widens nothing past it.

Please judge one point explicitly in ④. An external table's text column holds the external system's user keys, not platform sys_user ids, so a user field there type-checks but may not resolve. Is that the same, already-accepted shape-only contract lookup has to a platform object (in which case PASS on this point)? Or does user need the matrix to refuse it, or the PR to state the non-resolution, as a must-fix?

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK

Contract-review-claim: session_01L6bhgV4fMGJHSdSdx3u9mK · head ecf8b5c41d1e456f079d87b0d9542caa998ca7b5 · 2026-10-30T08:02Z


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: ecf8b5c41d1e456f079d87b0d9542caa998ca7b5
Local-runs: none
Scope: packages/spec/src/data/type-compat.ts, docs/adr/0015-external-datasource-federation.md + packages/services/service-datasource/src/external-datasource-service.ts, packages/spec/src/data/field.zod.ts, packages/plugins/plugin-auth/src/objectql-adapter.ts, packages/plugins/plugin-auth/src/auth-manager-auth-instance.ts, packages/plugins/plugin-auth/package.json, packages/plugins/plugin-auth/src/admin-import-users.ts, packages/plugins/plugin-auth/src/admin-user-endpoints.ts, packages/drivers/driver-sql/src/sql-driver.ts, packages/drivers/driver-mongodb/src/mongodb-driver.ts, packages/drivers/driver-memory/src/memory-driver.ts, packages/runtime/src/app-plugin.ts, packages/spec/src/data/type-compat.test.ts, .changeset/27917-type-compat-user-text.md

Inputs: card #27917 (body, claim 6516916650, os-dev-report 6517630083), PR #28011 (body, file list, net diff of four files against main), the handoff 6517644210 and addendum 6517664760, and the 55 check-runs on this head. The remainder of the diff (type-compat.test.ts, the changeset) is the lane seat's ACCEPT at TIER_DEFAULT; both were read to judge the hits files, not reviewed.

① Derived judgments

Accept-set changes the diff implies, each judged against the live tree:

  • isCompatible(column, 'user', dialect) turns from false to true for every column that canonicalises to text: the base aliases (text, varchar, char, character, string, character varying, nvarchar, nchar, clob), postgres bpchar / citext, mysql tinytext / mediumtext / longtext, sqlite text, snowflake string / varchar, bigquery string, mongo objectid / string, odata edm.string. Right. field.zod.ts declares the user type "stored IDENTICALLY to 'lookup' (FK string column → sys_user.id)" (lines 82-89 and the Field.user helper), and the platform's own sys_user.id column is table.string('id').primary(), i.e. varchar(255) (sql-driver.ts lines 10457 and 10550). The text row is therefore the platform's OWN physical shape for a user id, the same ground the row's existing signature / qrcode comment stands on. The mongo.objectid consequence (a Mongo ObjectId column typed user now passes) is the same shape-only contract lookup already has on that alias.
  • isCompatible(column, 'user') stays false on the integer and bigint rows (Edm.Int32, Edm.Int64, int8, mysql int, bigquery int64 and the rest), while lookup / master_detail stay exact there. Right. A federated lookup's target id is the remote key, so a remote integer column can carry it; a user field's target is fixed to sys_user by the type (USER_FIELD_TARGET, field.zod.ts lines 2508-2535), whose id column is a varchar(255) string on every driver. Nothing in the tree mints a non-string user id; see ③.
  • uuid row unchanged (user was already exact there); json, array, enum, binary, decimal, float, boolean, date, time, datetime, vector, unknown rows unchanged for user. Right as far as the card reaches. (Observation, not a finding on this diff: a multiple: true user field stores a JSON array per field.zod.ts, and the json / array rows list neither user nor lookup; the gap predates this PR and is shared with lookup.)
  • Gate 2 consequence (validateObject, external-datasource-service.ts lines 1024-1041): a federated object declaring a user field over a text-family column no longer yields a type_mismatch diff at severity error, so the boot under onMismatch: 'fail' no longer refuses it. The widening is monotone: no column/field pair that answered true or 'lossy' before answers less now. Right.
  • Draft generator unchanged: every row's suggested is untouched, so generateObjectDraft (lines 778-791) still drafts text for a text column and never emits user. Right, and the new test pins it.
  • Public surface: no export added, removed or renamed; SqlDialect, Compatibility, canonicalizeSqlType, suggestFieldTypeForSqlType and isCompatible keep their signatures; api-surface/data.json needs no regeneration and the TypeScript Type Check job (which carries check:api-surface) is green on this head. Right.
  • ADR-0015 §4.6: the dialect list now reads postgres, mysql, sqlite, snowflake, bigquery, mongo, odata, which equals the seven members of SqlDialect at this head. Right. The sentence that follows it makes a claim the code it names contradicts; see ④ (must-fix).
  • The Amended by line records the amendment as "dialect list only". Right for the ADR's own change: §4.6 has never enumerated matrix membership (the lookup / master_detail widening of the prior card amended nothing in it either), so the user row change is not an ADR decision and needs no ADR line.

② Semver level

  • Clause-②: yes (widening) on the PR body and in the changeset. Right. isCompatible is a published function of @objectstack/spec/data (export * from './type-compat' in data/index.ts; api-surface/data.json lists it), and its accept set widens with no signature change. The card's provisional yes is restated from the diff as the claim required.
  • Metadata-protocol: none. Right. No authorable key, value or metadata type changes: FieldTypeSchema and the user type's keys are untouched. What moves is a boot-time gate's verdict over a remote schema, not what an author can write.
  • .changeset/27917-type-compat-user-text.md: @objectstack/spec patch, both declaration lines present. Right level. A none protocol kind takes patch, and a non-protocol widening ships patch. The changeset's prose matches the diff (text row gains user; integer rows unchanged; §4.6 lists odata). Check Changeset is green on this head.
  • No skip-changeset; nothing else publishes (docs/adr/** is not a package).

③ Boundary flags

  • os-dev-report 6517630083: open_questions: [], out_of_scope_findings: []. Nothing to answer.
  • PR acceptance note "Memory and Mongo drivers mint their own ids for engine-direct writes; those were not measured." Answered here. driver-memory mints the string `${objectName}-${Date.now()}-${counter}` (memory-driver.ts lines 2646-2652); driver-mongodb mints nanoid(DEFAULT_ID_LENGTH) (mongodb-driver.ts lines 415-416, 487-488, 548). Both strings. Closed.
  • PR acceptance note "Integer rows left closed by measurement, not by caution." Answered, and the ground is firmer than the PR states. The platform's own sys_user.id column is varchar(255) on every SQL dialect (sql-driver.ts line 10550, and the comment at line 10457 records the live PostgreSQL reading). A seeded user (app-plugin.ts around line 2102 inserts seed rows straight through engine.insert) may supply any id, which the driver stores verbatim or replaces with a 16-character nanoid (sql-driver.ts lines 6459-6464); either way it is a string in a text-shaped column. So the closure rests on the platform's column shape, not only on the minters' output. Closed.
  • The measurement itself ("better-auth 1.7.12 mints a 32-character alphanumeric id; admin create and import share the path"). Judged against the producers on main, not re-run (read-only). plugin-auth/package.json pins better-auth at 1.7.12; objectql-adapter.ts builds the adapter through createAdapterFactory with adapterId, supportsBooleans, supportsDates, supportsJSON and transaction only (lines 885-915), no customIdGenerator; the advanced block of auth-manager-auth-instance.ts (lines 827-838) carries cookies, CSRF and the client-address source, no database.generateId; so the library default applies, and the dev's five-sample reading is consistent with that default's documented shape. admin-import-users.ts line 715 and admin-user-endpoints.ts line 812 both call authApi.createUser, the same id path. IdP-provisioned (SSO JIT) and SCIM identities arrive through the same adapter create. No path in the tree mints an integer user id. Closed.
  • Epic PM addendum 6517664760 (does an external text column that holds the remote system's user keys, which type-checks but may not resolve, need a refusal or a stated non-resolution?). Answered in ④: PASS on the point, no fix owed.
  • "Review owed" (PR body and handoff). This record is the at-tier contract review on this head and, per the handoff and the standing order of 2026-10-29, also serves as the full review of the spec and ADR change. Tier H landing still needs an authorized APPROVE from GOVERNED_APPROVERS on top of a PASS record; the PR stays draft until then. On this head the verdict is FAIL (④), so nothing is readied.
  • Observation for the lane, outside ④ (PR prose is not reviewed): the PR body's review-owed sentence names models by marketing name, which the repo's rule on model identifiers forbids in a PR body; the dispatch claim prescribed that phrasing, so the conflict is the lane's to settle, not a finding on the hits files.
  • Check-runs on this head: 55, all completed, none failed. The gate-carrying required contexts are success (Lint & Repo Gates, TypeScript Type Check, Test Core and all eight shards, Build Core, Governed Surface Queue Guard, Type Check UI, Test UI, Build & E2E UI, Live E2E UI); Lint UI is skipped by its path filter; Check Changeset, Spec property liveness, PR-body guards and Docs Drift Check are success. The gate verdicts are green; the FAIL below is a ④ finding the gates do not measure.
  • origin/main has moved past the branch point; the one main commit since then that touches service-datasource (the connector-credential verdict, fix(service-datasource): an adopted plugin driver's failed-credentials verdict clears on the next granted borrow, and the connector path withholds the form-encoded credential spelling #27990) edits datasource-connection-service.ts only, not the matrix's consumer validateObject. No overlap with the hits files.

④ Design judgment

packages/spec/src/data/type-compat.ts — against the ruling it applies and its first consumer. Holds.

  • Scenario accepted against: a federated object declaring a user field over a text-family column boots instead of failing Gate 2 with a type_mismatch error; the same field over an integer column still fails. Read in validateObject (lines 1024-1041): the only reader that refuses is this one, keyed on isCompatible(col.type, fieldType, dialect); the row change produces exactly that pair of outcomes, and the new test (type-compat.test.ts lines 186-209) pins both sides across base, postgres, bigquery and odata spellings plus the lookup contrast on the same integer columns.
  • Against the ruling (option A on the prior card: lookup / master_detail exact on text, integer and bigint): the matrix judges a column's storage shape against the field's value shape. user is a lookup whose target is fixed to sys_user; the platform's id column is varchar(255); so text is its exact row and the integer rows are not. The PR applies the ruling's principle and widens nothing past it. Not a design decision outside the card: the card named this outcome as the measured one and set the condition for the integer rows, which the measurement does not meet.
  • Attempts to break it: (a) a multiple: true user field stores a JSON array, which no text column carries exactly; but the json row lists neither user nor lookup, the gap predates this diff and is shared with lookup, so it is not this PR's. (b) A caller-supplied id can be a UUID or digit-only string; the uuid row keeps user, and a digit-only string is still a string in a varchar(255) column, so the integer rows' refusal stays honest. (c) mongo.objectid canonicalises to text, so an ObjectId column typed user now passes; same shape-only contract lookup already has there. None breaks the row.
  • The epic PM's explicit question. An external table whose text column holds the REMOTE system's user keys, declared user, type-checks and may not resolve. This is the same, already-accepted shape-only contract lookup has, so PASS on this point. Three reasons. First, the matrix is keyed on the column and judges shape, never provenance: a Field.lookup('account') on a federated row whose column holds the remote CRM's account key type-checks and may not resolve in exactly the same way, and that is the accepted contract since the prior ruling. Second, the same non-resolution class already existed on main for user under the uuid row (a remote GUID column typed user); this diff extends the row set to where the platform's id actually lives, it does not create the class. Third, a matrix refusal would also refuse the legitimate case the card is about (an external table that mirrors platform user ids), and by shape the matrix cannot tell the two apart in either row. Declaring user is the author's claim that the column holds platform user ids; the row comment states that contract ("the column holds a PLATFORM user id"), and a dangling reference resolves to nothing through the same expand path a dangling lookup does. No rule requires the PR to state the non-resolution further, and no refusal is owed.
  • Should-fix (comment precision, not a FAIL): the row comment says the id is "a string either way, never a UUID and never an integer". "Never a UUID" is true of the minters only; a caller-supplied id (a seed row, an _id) is stored verbatim, which is exactly why the uuid row keeps user. One clause: "the minted id is never a UUID or an integer; a caller-supplied id is stored verbatim as a string, which is why the uuid row keeps user".

docs/adr/0015-external-datasource-federation.md — against its first consumer (the odata arm and the SqlDialect vocabulary in type-compat.ts) and its execution plan. Does not hold on one clause.

  • The dialect list: now postgres, mysql, sqlite, snowflake, bigquery, mongo, odata, equal to SqlDialect's seven members at this head. Holds. The card's first task is met.
  • The pointer: "its aliases (DIALECT_ALIASES.odata)" names the module-private table the arm lives in, which is what the card asked for, and the arm's own comment ("OData's EDM primitives (CSDL 4.0 §4.4)") agrees with the ADR's description of what it maps. Holds.
  • The Amended by entry: dated, scoped to the dialect list, pointing at the prior card that brought the arm. Holds.
  • Must-fix (FAIL): the sentence added to §4.6 reads "odata is the one vocabulary that is not SQL". The code it names says otherwise: the SqlDialect doc comment at the top of type-compat.ts reads "SQL dialects, plus the two non-SQL ones a federation driver reports (mongo's BSON types, odata's EDM primitives)", and the mongo arm beside the odata arm maps BSON type names (objectid, bindata, long, bool). The amendment therefore writes into a governed record an exclusive claim that the cited code contradicts, on the very sentence whose purpose is to make §4.6 agree with that code. The card exists because the last record's should-fixes were not taken into their PR; landing a known-false sentence in a Tier H ADR and filing a third follow-up is worse than the one-sentence fix. Fix: "mongo and odata are the two vocabularies that are not SQL: DIALECT_ALIASES.mongo maps BSON type names, and DIALECT_ALIASES.odata maps the EDM primitive types an OData service's $metadata declares." The odata pointer the card asked for stays.
  • Should-fix (fold into the same sentence): "map the EDM primitive types an OData service's $metadata declares" reads as all of them; the arm deliberately leaves Edm.Duration and the Edm.Geography* / Edm.Geometry* families unmapped so a draft flags them for review (the arm's own comment). Either "maps the EDM primitive types the arm's comment lists" or a trailing "(duration and the geography / geometry families are left unmapped for review)".

Verdict reasoning: one must-fix on a hits file is a FAIL. The type-compat.ts change holds against its ruling, its spec and its first consumer, and the gate verdicts are green; the next head needs only the §4.6 sentence corrected (and may take the two should-fixes), after which a PASS record on that head goes to an approver.

Implemented-by: claude/project-thread-r6cu87
Reviewed-by: session_01L6bhgV4fMGJHSdSdx3u9mK

VERDICT: FAIL


Generated by Claude Code

…ries

Contract review on ecf8b5c: §4.6 called `odata` the one vocabulary that is
not SQL, but `mongo` maps BSON type names too, as the SqlDialect doc comment
says. The sentence now names both, and says which EDM types the odata arm
leaves unmapped for review. The text row's comment no longer says a user id
is never a UUID: only a minted one is never; a caller-supplied id is stored
verbatim, which is why the uuid row keeps `user`.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ihL2dhraZ7BwSanQPGEAb
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK

Contract-review-claim: session_01L6bhgV4fMGJHSdSdx3u9mK · head 10deba35c47816bc1126a05df54f018c13554da4 · 2026-10-30T08:31Z


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 10deba35c47816bc1126a05df54f018c13554da4
Local-runs: none
Scope: packages/spec/src/data/type-compat.ts, docs/adr/0015-external-datasource-federation.md + packages/services/service-datasource/src/external-datasource-service.ts, packages/drivers/driver-sql/src/sql-driver.ts, packages/spec/src/data/type-compat.test.ts, .changeset/27917-type-compat-user-text.md

Inputs: card #27917 (body, claim 6516916650, os-dev-report 6517630083), PR #28011 (body, file list, net diff of four files against main at this head), the handoff 6517644210 and addendum 6517664760, the earlier record 6518256754 on head ecf8b5c41d1e456f079d87b0d9542caa998ca7b5 (VERDICT FAIL: one must-fix and two should-fixes, all on the hits files), the claim 6518552987 on this head, and the check-runs on this head.

Delta shape. This is the delta review of record 6518256754 by the same reviewer. git log from the earlier head to this one lists exactly one commit, whose parent is the earlier head: no merge from main rides in the move, so there is no carry-over to separate from the PR's own work. Its diff touches two files, 10 insertions and 6 deletions: the sentence after the dialect list in ADR-0015 §4.6, and the user row comment in type-compat.ts. The text row literal, every other row, SqlDialect, both alias tables, type-compat.test.ts and the changeset are byte-identical between the two heads. The move lands the three findings of the earlier record and nothing else. The remainder of the diff (type-compat.test.ts, the changeset) stays the lane seat's ACCEPT at TIER_DEFAULT; both were read to judge the hits files, not reviewed. The cited files of record 6518256754 that this move does not touch (field.zod.ts, the plugin-auth sources, the memory and mongo drivers, app-plugin.ts) were not re-read this round; the judgments made on them stand by that record's name below.

① Derived judgments

  • The move changes no accept set and no public surface: the diff between the two heads on type-compat.ts touches comment lines only, and the text row still reads exact: [..., 'lookup', 'master_detail', 'user'] with the integer, bigint and uuid rows unchanged. Every accept-set and public-surface judgment of record 6518256754 §① stands as judged there, re-read against type-compat.ts at this head: isCompatible(column, 'user', dialect) is true on every column that canonicalises to text and false on the integer rows; uuid keeps user; suggested is untouched on every row; SqlDialect, Compatibility, canonicalizeSqlType, suggestFieldTypeForSqlType and isCompatible keep their signatures, so api-surface/data.json needs no regeneration and TypeScript Type Check is success on this head. Right.
  • Gate 2 consequence re-read at this head (external-datasource-service.ts, validateObjectUsing, lines 1266-1288 as the file reads in this tree): isCompatible(col.type, fieldType, dialect) === false yields a type_mismatch diff at severity error, 'lossy' one at warning, and ok is false only on an error; the widening is monotone. Right. Draft generator (generateObjectDraft, lines 901-915): the suggestion is read from the column alone and never emits user; an undefined suggestion defaults to text with a review note. Right.
  • ADR-0015 §4.6, dialect list: unchanged by the move, still postgres, mysql, sqlite, snowflake, bigquery, mongo, odata, equal to the seven members of SqlDialect at this head. Right.
  • ADR-0015 §4.6, the sentence after the list: it now claims two non-SQL vocabularies, mongo (BSON type names) and odata (EDM primitives except Duration and the geography / geometry families), which is what type-compat.ts at this head says of itself in the SqlDialect doc comment and in the two arms. The sentence no longer contradicts the code it names. Right; judged in full in ④.
  • The Amended by entry is unchanged and still reads "dialect list only — §4.6 names the odata vocabulary the matrix gained with datasource import: generate object drafts from an OData $metadata document, and the Studio datasource page lists and imports its entity sets — epic #27261 P2 #27745". Right: the amended sentence describes two members of that list and decides nothing; §4.6 still enumerates no matrix membership, so the user row change still needs no ADR line.

② Semver level

Untouched by the move; stands as judged in record 6518256754 §②. Re-checked on this head: Clause-②: yes (widening) and Metadata-protocol: none on the PR body and in the changeset, both unchanged; .changeset/27917-type-compat-user-text.md is @objectstack/spec patch, byte-identical to the earlier head, and its prose still matches the diff (text row gains user; integer rows unchanged; §4.6 lists odata); Check Changeset is success on this head. A code comment and an ADR sentence publish nothing, so the level does not move. No skip-changeset; nothing else publishes.

③ Boundary flags

  • os-dev-report 6517630083 (open_questions: [], out_of_scope_findings: []) is the only dev report on the card; the move carries no new report and raises no new flag. Nothing to answer.
  • The PR body is unchanged. Its "What changes" paragraph still describes the §4.6 sentence as pointing at DIALECT_ALIASES.odata "as the non-SQL vocabulary", a wording the tree has since outgrown (two vocabularies). PR prose is the lane's and is not reviewed; the tree is judged, and the tree is right. Observation for the lane. The earlier observation that the body's review-owed sentence names models by marketing name also stands; the lane settles it.
  • The flags record 6518256754 §③ answered (the memory and mongo minters, the integer rows' closure on the platform's varchar(255) key column, the measurement of the user-id producers on main, the epic PM's addendum question, the review-owed note) were judged against files this move does not touch; they stand as judged there, by that record's name. The PR author is objectstack-fleet[bot], so no author note is owed.
  • Check-runs on this head, read at 08:40Z: 50 check-runs, all completed; 40 success, 10 skipped, none failed. All ten required contexts are green: Lint & Repo Gates, TypeScript Type Check, Test Core (the aggregate and all eight shards), Build Core, Governed Surface Queue Guard, Type Check UI, Test UI, Build & E2E UI and Live E2E UI are success; Lint UI is skipped by its path filter, as on the earlier head. Check Changeset, Spec property liveness, PR-body guards, Flag docs affected by code changes, Check Documentation Links, Check PR Size, Dogfood Regression Gate and Auto Label are success; the opt-in and path-filtered runs are skipped. The gate verdicts are green. (At an earlier read, 08:38Z, the Test Core aggregate was still in_progress; it completed success at 08:38:11Z. A pending run is never counted green, and none is pending now.)
  • origin/main is 21 commits past the branch point. None touches either hits file or external-datasource-service.ts; one (fix(spec)!: composeStacks refuses a datasource whose external.connector names no connector instance the composed inputs declare #27939) touches packages/spec/src/data/injected-system-column-provenance.ts, a different module. GitHub reports the PR mergeable. No overlap.
  • Landing. Tier H (docs/adr/**): this PASS record on the current head is one of the conditions; the others are every check green, which this head has, and an authorized APPROVE from GOVERNED_APPROVERS, which is still owed. The PR stays draft until the approver acts; no seat readies, queues or arms it.

④ Design judgment

packages/spec/src/data/type-compat.ts — the move touches the user row comment only. Holds; the should-fix is closed.

  • The should-fix of record 6518256754 asked that "never a UUID" be confined to the minters. The comment now reads: "A minted id is never a UUID or an integer; a caller-supplied id is stored verbatim as a string, which is why the uuid row keeps user." Judged against the code it describes: sql-driver.ts create (lines 6456-6464) takes _id, else id, as the caller gave it and mints nanoid(DEFAULT_ID_LENGTH) (16, line 266) only when neither is supplied; the upsert paths mint the same nanoid when no id is supplied (lines 8216, 8629); the sys_user table's key is table.string('id').primary() (line 10550), varchar(255), so a caller-supplied value lands as a string of that column; and the uuid row at this head lists user in its exact set. Every clause is what the code does.
  • Trying to break the new clause: a caller-supplied numeric id (id: 42) is not "verbatim" a string, but the column coerces it on every SQL dialect the driver emits for, and the sentence is about the SQL driver's path, which it names; the memory driver has no column to coerce with, but its mint is a string (closed in record 6518256754 §③) and the sentence makes no claim about it. Not a finding.
  • The row literal, the rest of the comment, the test (type-compat.test.ts lines 186-216 pin both sides and the draft suggestion) and the consumer are unchanged, so the rest of record 6518256754 §④ on this file (the scenario, the ruling it applies, break attempts a to c, the epic PM's explicit question answered PASS) stands as judged there. Nothing outstanding on this file.

docs/adr/0015-external-datasource-federation.md — the move replaces the one sentence the must-fix named. Holds against its first consumer; must-fix and should-fix closed.

  • "mongo and odata are the two vocabularies that are not SQL": equals the SqlDialect doc comment at this head ("SQL dialects, plus the two non-SQL ones a federation driver reports (mongo's BSON types, odata's EDM primitives)"), and DIALECT_ALIASES has exactly those two non-SQL arms. The exclusive claim the earlier record failed is gone; what stands is the claim the code makes. Must-fix closed.
  • "DIALECT_ALIASES.mongo maps BSON type names": the arm's keys are objectid, string, double, int, long, decimal, bool, date, object, array, bindata — the BSON type aliases, lower-cased — plus boolean as a tolerated spelling. Holds.
  • "DIALECT_ALIASES.odata maps the EDM primitive types an OData service's $metadata declares, except Edm.Duration and the Edm.Geography* / Edm.Geometry* families": the arm maps sixteen primitives (edm.string, edm.boolean, edm.guid, edm.byte, edm.sbyte, edm.int16, edm.int32, edm.int64, edm.decimal, edm.single, edm.double, edm.date, edm.timeofday, edm.datetimeoffset, edm.binary, edm.stream); set against the primitive type list of CSDL 4.0 §4.4, which the arm's own comment cites, the unmapped ones are exactly Edm.Duration and the two geo families, so the exception list is complete for what the sentence claims. The arm also maps collection to array; a collection is a type constructor, not a primitive, and the sentence does not say the arm maps primitives only, so no contradiction. Enum and complex types, which the arm's comment also leaves unmapped, are not EDM primitives and fall outside the sentence's claim. Should-fix closed.
  • "which it leaves unmapped so a draft flags them for review": read at the consumer. canonicalizeSqlType answers unknown for a name on neither table (no edm. key exists in BASE_ALIASES), suggestFieldTypeForSqlType then answers undefined, and generateObjectDraft (lines 901-908) defaults the field to text and pushes the note "unrecognised remote type — defaulted to 'text', verify", rendered as a // REVIEW: comment on the field (lines 1526-1532). Holds.
  • The dialect list, the DIALECT_ALIASES.odata pointer the card asked for, and the Amended by entry are unchanged and stand as judged in record 6518256754. "Dialect list only" still describes the amendment: the sentence characterises two members of the list it sits under and changes no decision.
  • Trying to break it: (a) §4.6's opening clause "maps SQL types (text, varchar, ...)" predates this PR and is now qualified by the sentence that follows; not this diff's to fix and not false of the SQL arms. (b) The consumer's typeVocabulary folds odata-4.0 onto the odata arm (lines 369-373); the sentence names the vocabulary, not the driver's dialect string, so nothing turns on it. (c) The amendment changes no decision of the ADR and no consumer's behaviour, so the execution plan (P2: the matrix as "an explicit, unit-tested module" whose drafts carry // REVIEW: markers) is what the new sentence describes. None breaks it.
  • No must-fix. No should-fix.

Verdict reasoning: the move lands the three findings of record 6518256754, each now judged to hold against the code it cites, and nothing else moved; the judgments of that record the move does not touch stand by its name. No must-fix on either hits file, and the gate verdicts on this head are green. PASS. The Tier H landing still needs an authorized APPROVE from GOVERNED_APPROVERS; the PR stays draft until then, and no seat readies it.

Implemented-by: claude/project-thread-r6cu87
Reviewed-by: session_01L6bhgV4fMGJHSdSdx3u9mK

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

epic:#27261#1 · marchtian · session_017ihL2c5z3q4h8inrn5kwqV
ACCEPT · PR #28011 @ 10deba35c47816bc1126a05df54f018c13554da4 · 2026-10-30T08:48Z

维护者速读

  • 改了什么: 两件事。一、ADR-0015 §4.6 的方言清单补上 odata,并说明 mongo 和 odata 是仅有的两套非 SQL 词汇。二、「用户」字段(user)可以落在文本列上了。之前只允许 uuid 列,外部表里用文本存用户 id 的列会在启动校验时报类型不匹配。整数列仍然拒收。
  • 为什么改: 平台用户 id(sys_user.id)始终是字符串,具体是 varchar(255),生成的值是 better-auth 32 位字母数字或 nanoid(16)。所以文本列正是它的物理形状。依据和 feat(datasource): import objects from an OData datasource's $metadata, and import several entity sets from Studio and MCP #27880 放宽 lookup 的裁决一致。
  • 风险与回滚: 只放宽:user 加入文本行的精确集合,整数行和其他行都不变。外部系统的用户键未必能解析成平台用户,这一点和 lookup 指向平台对象时的「只校验形状」约定相同,复核已确认。还原即回滚。
  • 先看: packages/spec/src/data/type-compat.ts 的 text 行和 user 注释;ADR-0015 §4.6。
  • 两分钟验证: pnpm --filter @objectstack/spec exec vitest run src/data/type-compat.test.ts。
  • 需要你做的: Tier H(docs/adr/**):请 hotlong 或 os-zhuang APPROVE;批准后席位落地。

席位意见:ACCEPT。契约复核 PASS 在案:先是 6518256754,在 ecf8b5c4 上 FAIL,有一条 must-fix 是 ADR 那句「odata 是唯一的非 SQL 词汇」。随后 delta 6518728501 在当前 head 10deba35 上 PASS,must-fix 和两条 should-fix 都已关闭,均为 CONTRACT_REVIEW_TIER。Fable 额度仍未恢复(07:46Z 探测仍返回 429),按 2026-10-29 19:39Z 的指示,这份记录兼作 spec 与 ADR 改动的完整复核。

席位核对

  1. Shape: +67/−4 across 4 files: the text row of the type matrix gains user, with its row comment; ADR-0015 §4.6 and its Amended-by line; tests; and an @objectstack/spec patch changeset.
  2. Premise measured: on main, user is exact only on the uuid row. The dev measured sys_user.id on main as always a string, and the record re-derived it from sql-driver.ts.
  3. Gate check-runs on 10deba35: 53 runs: 41 success, 12 skipped, 0 failed. Local: 85 derived gates, all exit 0.
  4. Accepted deviations: none. The PM's addendum question, whether a remote user key needs to be refused, is answered in the record's ④: it is the same shape-only contract lookup has, so no refusal or extra text is owed.
  5. Out of scope: none.
  6. Landing: governed, Tier H (docs/adr/** ×1; 71 lines ≤ 3000). It stays draft with needs-user-decision, and reviewers are requested. After an APPROVE, the seat clears the label, readies it and arms automerge.

@os-zhuang
os-zhuang marked this pull request as ready for review October 30, 2026 08:58
@os-zhuang
os-zhuang enabled auto-merge October 30, 2026 08:58
@os-zhuang
os-zhuang added this pull request to the merge queue Oct 30, 2026
Merged via the queue into main with commit f934a41 Oct 30, 2026
66 checks passed
@os-zhuang
os-zhuang deleted the claude/project-thread-r6cu87 branch October 30, 2026 09:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:data size/s tests tooling

Projects

None yet

3 participants