Skip to content

feat(driver-odata): an OData v2 dialect beside v4, selected with version: '2.0' - #28177

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-9mtiyf
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-9mtiyf

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

domain:server#4 · os-elon-musk · session_016WV4cykf9LVyVYxnw1NL95

Fixes #28125

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

维护者速读

  • 改了什么:@objectstack/driver-odata 在 v4 留好的方言接缝上新增 OData v2 方言,插件配置 version: '2.0' 即可接 v2 服务(默认仍是 4.0)。覆盖卡上每一项:d.results / __count / __next 响应封装、$inlinecount、substringof 与 v2 带类型字面量(datetime'…'、guid'…'、7L、1.5M 等)、键谓词、MERGE 写入(POST + X-HTTP-Method)、EDMX 1.0 的 Association / AssociationSet。spec 的类型矩阵补 Edm.DateTime → datetime、Edm.Time → time。
  • 为什么改:v2 仍是 SAP Gateway 大部分标准服务、旧版 Dynamics CRM 和 SharePoint 2013 REST 的协议;main 上 version: '2.0' 直接拒绝启动。SAP 专有机制(CSRF 握手、sap-client、sap:* 注解)按 10-29 裁定没有放进来。
  • 风险与回滚:v4 行为不变:v4 原有 732 个测试在 main 源码和本 PR 上都是绿的。方言接口只新增 4 个可选成员,仓库外的方言照常编译。spec 两个别名只放宽(以前是 unknown)。回滚即 revert。
  • 先看:packages/drivers/driver-odata/src/dialect-v2.ts,以及 odata-driver.ts 里 4 个可选钩子的接入点。
  • 两分钟验证:pnpm --filter @objectstack/driver-odata exec vitest run,预期 1383 passed(其中 v2 651 条:一致性用例集 336、插件套件 276、录制驱动 15、方言 24)。
  • 需要你做的:无。欠一次契约复审(packages/spec/src/**),由席位安排。

What changes

  • packages/drivers/driver-odata/src/dialect-v2.ts (new): odataV2Dialect and parseCsdlV2, implementing the existing ODataDialect.
    • Requests carry DataServiceVersion: 2.0 / MaxDataServiceVersion: 2.0 and Accept: application/json (verbose JSON).
    • Collections are read out of d.results (or a 1.0 service's bare d array), with __next as the next link and the string __count as the count. An entity is read out of d, and its ETag comes from __metadata.etag. __metadata and each navigation's { __deferred } stub are dropped from the row.
    • The count is $inlinecount=allpages, and contains is substringof('x',P).
    • Typed literals: datetime'…' (only for a comparand that names its instant, spelled as its UTC wall clock), datetimeoffset'…', time'PT…', guid'…', 7L, 1.5M (no exponent), 1.5d, 1.5f.
    • keyValue reads a composite key's typed literal back, so TrackingId=guid'…',Leg=2L round-trips.
    • Values: /Date(ms)/ → the UTC instant, or the date under a field declared date (v2 has no Edm.Date). Edm.Int64 / Edm.Decimal text → number, except an Edm.Int64 beyond the safe range, which stays text. Edm.Time PT13H20M → 13:20:00. Writes use the inverse forms.
    • A partial update is POST with X-HTTP-Method: MERGE.
    • EDMX 1.0 $metadata: a navigation's target and cardinality come from its Association ends. Its ReferentialConstraint pairs are taken only on the dependent end, as v4 writes them on that navigation. Its entity-set binding comes from the AssociationSets, base types included.
    • ⛔ No vendor mechanics (no CSRF handshake, sap-client or sap:*).
  • dialect.ts: the seam gains four optional members (readValue, writeValue, keyValue, updateRequest). If a dialect leaves one out, the driver keeps v4's behaviour. odataV4Dialect implements none of them.
  • odata-driver.ts:
    • DIALECTS['2.0'] is the v2 dialect.
    • toRow reads each declared property through readValue, and toRemote writes it through writeValue.
    • update takes its verb from updateRequest, and the composite key predicate is re-spelled through keyValue.
    • registerExternalObject records each field's declared type from the object's fields, the way the SQL driver learns field types, so readValue sees it.
  • config.ts: version: z.enum(['4.0', '2.0']), default 4.0.
  • dialect-v4.ts: pure refactor: the XML helpers are exported, and the inheritance flattening is extracted into flattenEntityTypes for the v2 reader. The v1/EDMX refusal message now names version: '2.0' and still matches the v4 suite's pin.
  • packages/spec/src/data/type-compat.ts: DIALECT_ALIASES.odata gains edm.datetime → datetime and edm.time → time, and nothing else.
  • Fixtures and tests:
    • northwind-v2-metadata.xml is Northwind's v2 shape, abridged, plus two fixture-own sets with a Guid key and a composite (Guid, Int64) key. recorded-service-v2.ts holds the verbose JSON recordings. Both are written from the service's shape, because the container has no network.
    • dialect-v2.test.ts has 24 tests and odata-driver-v2.test.ts has 15.
    • The in-process service (odata-service.ts) gains a strict version: '2.0' mode: EDMX 1.0, the d envelope, $inlinecount and MERGE. It refuses v4's $count, contains, bare temporal literals and PATCH.
    • The conformance case-sets (odata-driver.conformance.test.ts) and the plugin-driver kit (…conformance-kit.test.ts) now also run over that v2 service. The v4 describe names are unchanged; the kit's object is registered with its fields, as the engine registers it.
    • service-datasource's external-object-draft-odata-metadata.test.ts gains a v2 describe over its own copy of the v2 $metadata. This test-only path was added to the claim's surface by the seat.
  • Docs: content/docs/data-modeling/odata-datasources.mdx gains an "OData v2 services" section with the version: '2.0' line, a v4/v2 spelling table, and what stays out. README.md and the package description name v2.
  • .changeset/28125-driver-odata-v2-dialect.md: @objectstack/driver-odata minor and @objectstack/spec minor (added).

Why

Before (measured at origin/main 062a2323, production sources reverted, new tests kept)

  • driver-odata: 627 failed, 732 passed. Every v2 test failed: 336 in the case-sets, 276 in the kit and 15 in the recorded-driver suite, each on driver-odata: invalid config — version: Invalid input: expected "4.0". dialect-v2.test.ts could not load, because the module did not exist. The 732 passing tests are the whole v4 suite.
  • service-datasource v2 import: 5 failed (the same config refusal), 13 passed (v4).
  • spec type-compat.test.ts: 1 failed, expected 'unknown' to be 'datetime'.

After

All green: driver-odata 1383 passed, the service-datasource import 18 passed, and spec type-compat 29 passed.

Clause-② and the kind

yes (widening): the driver config accepts a second version value, and the type matrix answers datetime / time where it answered unknown (a draft typed those columns text for review, and Gate 2 refused a datetime / time field over one). Nothing accepted before is refused now. Metadata-protocol: added: version: '2.0' is a new value a datasource author can write, so both moved packages take minor.

One finding met on the way

The plugin-driver kit's "temporal: every row, as stored" check failed on the first v2 run. A date field over v2's Edm.DateTime read back as 1969-12-31T00:00:00.000Z while the remote held 1969-12-31. v2 has no Edm.Date, and a v2 service keeps a date in an Edm.DateTime at midnight. So the driver now reads the date alone under a field the object declares date, and the kit registers its object with its fields, as the engine does.

Verification

  • Pins red first, ablated, restored. Each ablation used scripts/ablation-replace.mjs on the committed WIP head, each restored with git diff HEAD empty. Every one went red:

    • the envelope (d.results)
    • the Association ReferentialConstraint
    • the AssociationSet bindings
    • substringof
    • the numeric suffixes
    • the datetime'…' prefix
    • the guid'…' key literal
    • $inlinecount
    • version accepting 2.0
    • 2.0 selecting the v2 dialect
    • the spec aliases
    • MERGE
    • a date field over Edm.DateTime
    • the dropped __deferred stubs

    One extra run stayed green: the spec-alias ablation against the service-datasource import test. service-datasource resolves @objectstack/spec from its built dist, so a src mutation does not reach it. The alias pin is the spec test, which went red.

  • The v4 suite is unchanged. Its 732 tests passed on main's sources and pass on this head. The shared test files gained a version loop, and the v4 run keeps its exact describe names.

  • Gates. On the committed head 667660ad, after a whole-tree turbo run build (exit 0): dispatch-gates --commands derived 117 families, all run, all exit 0. --ran: 117 derived, 117 run, 0 NOT-MEASURED. Also run: pnpm lint exit 0; typecheck of driver-odata, service-datasource and spec, each exit 0; driver-odata 1383 passed; service-datasource 1074 passed; spec src/data 4591 passed. The head is 15 commits behind origin/main; none of them touches a file this diff changes.

  • Generated spec artifacts. pnpm --filter @objectstack/spec build then check:generated: all 14 up to date (the alias table feeds no artifact).

Acceptance notes

  • The v2 fixtures are written from the services' published shape, not recorded live. The container has no route to services.odata.org. The XML header says what is abridged and what is fixture-own, as the v4 TripPin fixture's header does.
  • Prefer: odata.maxpagesize is still sent on a v2 GET. A v2 service ignores a preference it does not know (RFC 7240 §2), and the page size it uses is its own. The docs and the pageSize describe say so. The transport is untouched.
  • A zone-less date comparand on Edm.DateTime is not pushed down. It is left to the engine, the same rule v4 applies to Edm.DateTimeOffset, so the service is never asked a narrower question.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (Two alias entries in a pure mapping table.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 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: its generated artifacts are checked (check:generated: all up to date, nothing to regenerate).
  • 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 by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_016WV4cykf9LVyVYxnw1NL95

…ion: '2.0'

The v2 dialect plugs into the existing dialect seam: the verbose JSON
envelope (d.results, __count, __next, __metadata), $inlinecount, the typed
v2 literals and key predicates, substringof, MERGE tunnelled through POST,
and EDMX 1.0 $metadata whose Associations and AssociationSets are read into
the navigation shape v4 yields, so an import drafts the same lookups.
Edm.DateTime, Edm.Int64, Edm.Decimal and Edm.Time values are read and written
in their v2 JSON forms; a date field over an Edm.DateTime reads its date.
The spec type matrix's odata arm maps the two v2-only primitives,
Edm.DateTime and Edm.Time. Vendor mechanics (SAP's CSRF handshake,
sap-client) stay out.

The conformance kit and the eleven shared case-sets now also run over an
in-process v2 service that refuses every v4 spelling.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cykf9LVyVYxnw1NL95
@github-actions github-actions Bot added size/xl dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation protocol:data tests tooling labels Oct 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/driver-odata, @objectstack/spec, touching 61 documentable anchor(s). ⚠️ 3 changed file(s) yielded no anchor (packages/drivers/driver-odata/README.md, packages/drivers/driver-odata/package.json, packages/drivers/driver-odata/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/data-api.mdx (via pageSize (symbol, a field of const object ODataDriverConfigSchema))
  • content/docs/api/error-catalog.mdx (via pageSize (symbol, a field of const object ODataDriverConfigSchema))
  • content/docs/data-modeling/odata-datasources.mdx (via pageSize (symbol, a field of const object ODataDriverConfigSchema))
  • content/docs/frontend/data-source.mdx (via pageSize (symbol, a field of const object ODataDriverConfigSchema))
  • content/docs/frontend/plugins/plugin-grid.mdx (via pageSize (symbol, a field of const object ODataDriverConfigSchema))
  • content/docs/frontend/plugins/plugin-view.mdx (via pageSize (symbol, a field of const object ODataDriverConfigSchema))
  • content/docs/protocol/kernel/error-handling.mdx (via readError (symbol, a method of const object odataV2Dialect))

⛔ 4 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via keyValue (symbol, a method of const object odataV2Dialect; a method of interface ODataDialect))
  • content/docs/releases/v17/17-3.mdx (via fieldTypes (symbol, a field of interface ObjectDeclaration))
  • content/docs/releases/v17/17-5.mdx (via pageSize (symbol, a field of const object ODataDriverConfigSchema))
  • content/docs/releases/v17/17-6.mdx (via registerExternalObject (symbol, a method of class ODataDriver))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 3 changed file(s) yielded no anchor (packages/drivers/driver-odata/README.md, packages/drivers/driver-odata/package.json, packages/drivers/driver-odata/src/index.ts) — pages documenting those are invisible to this run
  • 13 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 58 of 210 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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 412908cf9e982d86307504fa6e860111c046926a → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 39262b3e630e444c8259fd37d92f13ed1dd442d7 — the merge of head 667660add518c9a9e904cf24836ea68dfb09b5a3 into base 412908cf9e982d86307504fa6e860111c046926a, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 39262b3e630e444c8259fd37d92f13ed1dd442d7 && git checkout 39262b3e630e444c8259fd37d92f13ed1dd442d7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 412908cf9e982d86307504fa6e860111c046926a 667660add518c9a9e904cf24836ea68dfb09b5a3 && git checkout -B drift-repro 412908cf9e982d86307504fa6e860111c046926a && git merge --no-ff 667660add518c9a9e904cf24836ea68dfb09b5a3

node scripts/docs-audit/affected-docs.mjs --json 412908cf9e982d86307504fa6e860111c046926a

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 412908cf9e982d86307504fa6e860111c046926a → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

Contract-review-claim: session_016WV4d1Np3hU7RQnA13YZcL · head 667660a · 2026-10-30T15:33Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 667660add518c9a9e904cf24836ea68dfb09b5a3
Local-runs: none
Scope: packages/spec/src/data/type-compat.ts + packages/spec/src/data/type-compat.test.ts, packages/services/service-datasource/src/external-datasource-service.ts (typeVocabulary, generateObjectDraft, validateObjectUsing, read on main), packages/services/service-datasource/src/tests/external-object-draft-odata-metadata.test.ts, packages/drivers/driver-odata/src/dialect-v2.ts (parseCsdlV2 property types, readValue / writeValue for Edm.DateTime and Edm.Time), packages/drivers/driver-odata/src/odata-driver.ts (introspectSchema dialect string, readValues), packages/drivers/driver-odata/src/config.ts, docs/adr/0015-external-datasource-federation.md §4.6 (read, not changed)

Inputs read: card #28125 (body and all three comments: triage 6524734159, claim 6524866087, os-dev-report 6526649009), PR #28177 (body, file list, net diff against main at merge-base 062a2323), and the 43 check-runs on this head. Reviewed at 2026-10-30T15:47Z.

① Derived judgments

  1. ODataDriverConfigSchema.version: z.literal('4.0') → z.enum(['4.0', '2.0']), default 4.0 unchanged (config.ts, outside hits, named because it drives the declared arm). A second accepted value of an authored datasource config field: accept-set widening; Metadata-protocol: added is the right kind. Right.
  2. DIALECT_ALIASES.odata gains edm.datetime → datetime and edm.time → time (the hits file). Both answered unknown on main. The consequences at the published canonicalizeSqlType / suggestFieldTypeForSqlType / isCompatible face, each named:
    • suggestFieldTypeForSqlType('Edm.DateTime' | 'Edm.Time', 'odata'): undefined → 'datetime' / 'time'. A draft stops typing those columns text with an "unrecognised remote type" review note. Right.
    • isCompatible(…, 'datetime' | 'time', 'odata'): false → true. Gate 2 stops refusing a datetime field over Edm.DateTime and a time field over Edm.Time as a type_mismatch error. Widening. Right.
    • isCompatible('Edm.DateTime', 'date', 'odata'): false → 'lossy' (the datetime row's lossy set). Gate 2 moves from an error to a warning, which matches the driver's readValue reading the UTC date alone under a field declared date; lossy is the honest word, because an Edm.DateTime column may carry a time of day the field drops. Widening. Right.
    • isCompatible('Edm.DateTime' | 'Edm.Time', 'text' | 'json', 'odata'): 'lossy' → false. The unknown row's permissive text/json fallback no longer applies once the type is recognised. At the function face this is a narrowing. In a deployment it is reachable only through a dialect-odata introspection that names those two types, and on main none can exist: version: '2.0' was refused at boot, and CSDL 4.0 declares neither name. The deployed accept set therefore only widens, and the arm yes (widening) stands; it is the same shape as 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, which introduced the whole odata arm (every EDM name moved out of unknown) under Clause-②: yes (widening). The sentence "Nothing that passed before fails now" in the PR body and changeset is true of deployed objects and overstated at the bare function face; it is prose and is noted here, not judged.
    • Dialect reach: the driver reports dialect: odata-2.0 (odata-driver.ts introspectSchema); the service's typeVocabulary on main folds odata and every odata-* to the matrix's odata arm, so the two aliases reach the first consumer without a service change. Verified in the cited code; the head's service-datasource test pins the dialect string and the drafted datetime.
  3. ODataDialect gains four optional members (readValue, writeValue, keyValue, updateRequest; dialect.ts, outside hits). Additive on an exported interface; odataV4Dialect implements none; the driver falls back to v4 behaviour when one is absent, so an out-of-repo dialect still compiles and behaves as before. Right.
  4. New exports odataV2Dialect, parseCsdlV2 from the driver's index; parseCsdlV4's v1/EDMX refusal text is reworded (the v4 suite's pin still matches, per the diff). Additive. Right.
  5. No governed path in the file list; docs/adr/0015 §4.6 is not touched and does not need to be: its text says the odata arm "maps the EDM primitive types an OData service's $metadata declares, except Edm.Duration and the Edm.Geography* / Edm.Geometry* families", which is version-neutral, and the two additions bring the table closer to that sentence rather than away from it. Right.

② Semver level

③ Boundary flags

  • open_questions: [] in the os-dev-report. Nothing to answer.
  • Out-of-scope finding 1 (not filed): Prefer: odata.maxpagesize is still sent on a v2 GET; a v2 service ignores an unknown preference (RFC 7240 §2), and the pageSize describe and the guide say so. Accepted as a documented no-op; no card owed.
  • Out-of-scope finding 2 (not filed): the v2 fixtures are written from the services' published shape, not recorded live (no route to services.odata.org from the container); the fixture headers say so, as the v4 TripPin fixture's does. Accepted; the lane seat may file a live-recording follow-up when a networked run is available, at its discretion.
  • Author: PR user.login is objectstack-fleet[bot]; no author note owed.
  • Should-fix (lane seat, before ACCEPT; not a contract defect): Metadata-protocol: added owes the PR body a third line, Skills-docs: updated here | follow-up owed — FILES | follow-up #N | none — REASON (pm-dispatch SKILL.md, the Metadata-protocol row). The PR body carries neither it nor a reason; the guide section in content/docs/data-modeling/odata-datasources.mdx is the likely updated here answer, but the line is the dev's to write from the diff.
  • Check-runs on this head at review time: 43 in all; 18 concluded success, 5 concluded skipped (opt-in and matrix placeholders), none failed; 20 had not yet concluded: Build Core, Build UI Deps, Lint · CI release & engine gates, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped (query-options erasure; slot lookup & corpus; tooling self-tests), Live E2E UI, Test Core 1/8 through 8/8, Type Check · consumer gates, Type Check · debt ledger, Type Check · workspace. Their conclusions are the gate verdicts; this record does not stand in for them, and the Tier S landing condition (every check green) waits on them.

④ Design judgment

packages/spec/src/data/type-compat.ts (the one hits file), against the code it cites and the scenario it is accepted against:

  • Key spelling holds end to end. parseCsdlV2 carries each Property/@Type verbatim after alias qualification (Edm.DateTime, Edm.Time); introspectSchema reports p.type as the column type (or Collection(…)); canonicalizeSqlType lower-cases and strips the parameter list, so edm.datetime / edm.time hit the new entries and Collection(Edm.DateTime) still reaches collection → array, as the comment above the arm says.
  • The rows match the driver's value shapes. readValue yields an ISO UTC instant for Edm.DateTime and HH:MM:SS[.f] for Edm.Time; writeValue sends /Date(ms)/ and PT…S back. The datetime row (exact: ['datetime'], lossy: ['date']) and the time row (exact: ['time']) describe exactly what a datetime / time / date field can hold of those values. The date-over-Edm.DateTime case the plugin-driver kit met ("temporal: every row, as stored") lands on lossy, which is the right verdict for a column that can carry midnight or any other time.
  • The comment is accurate. [MC-CSDL] 2.0's primitives are Binary, Boolean, Byte, DateTime, DateTimeOffset, Decimal, Double, Guid, Int16, Int32, Int64, SByte, Single, String, Time; every one but the two added already sits in the arm under its v4 name, so "every other v2 primitive keeps its v4 name" is true, and Edm.Time "a time of day, written as a duration since midnight" is how v2 verbose JSON writes it.
  • Against the ruling it implements. ADR-0015 §4.6's description of the odata arm is version-neutral and lists the deliberate exclusions (Edm.Duration, the Geo families); Edm.DateTime and Edm.Time were not among them, so mapping them closes a gap rather than opening one. No ADR amendment is owed, and none is in the diff, which is what keeps this PR off the Tier H register.
  • First consumer. The head's service-datasource test drafts the v2 Northwind Employees.BirthDate (Edm.DateTime) as datetime with an empty review list apart from navigations, and Gate 2 passes over the same $metadata for five imported sets. Edm.Time has no consumer-level pin (Northwind declares none); it is pinned in type-compat.test.ts on both canonicalizeSqlType and suggestFieldTypeForSqlType, and exercised through the driver's in-process v2 service in the plugin-driver kit. Sufficient for a two-entry alias change.
  • Tried to break it. canonicalizeSqlType('Edm.DateTime') with no dialect, or under postgres, stays unknown (the vocabulary remains the dialect's own, as the existing Edm.Int32 pins require). The own-property guard on DIALECT_ALIASES[dialect] is unaffected by two more literal keys. isCompatible for every pair not naming the two types answers as on main: the diff adds keys and changes no existing entry or row.

No must-fix. No should-fix on the hits file. The one should-fix in ③ is a PR-body line for the lane seat's ACCEPT and does not touch the contract face.

Implemented-by: claude/project-thread-9mtiyf
Reviewed-by: session_016WV4d1Np3hU7RQnA13YZcL

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4cykf9LVyVYxnw1NL95

PR-body addendum (the relay cannot re-edit the body). This answers the contract review's should-fix in comment 6526958904, the third declaration line that Metadata-protocol: added owes:

Skills-docs: updated here — content/docs/data-modeling/odata-datasources.mdx teaches version: '2.0' in its new "OData v2 services" section (the version option row, a v4/v2 spelling table, and what stays out of the package), and packages/drivers/driver-odata/README.md names it in its config table. No skills/** file teaches the driver's plugin options: skills/objectstack-data/rules/datasources.md names @objectstack/driver-odata only for credential borrowing. So no skill eval exercises the new value; the driver's own suites do (1383 tests, 651 of them over v2).


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

维护者速读

  • 改了什么:@objectstack/driver-odata 在 v4 的方言接缝上新增 OData v2 方言,插件配置 version: '2.0' 即可接 v2 服务(默认仍是 4.0):响应封装、$inlinecount、substringof 与 v2 带类型字面量、键谓词、MERGE 写入、EDMX 1.0 的 Association / AssociationSet。spec 类型矩阵补 Edm.DateTime → datetime、Edm.Time → time 两个别名。
  • 为什么改:v2 仍是 SAP Gateway 标准服务、旧版 Dynamics CRM、SharePoint 2013 REST 的协议;main 上 version: '2.0' 直接拒绝启动。SAP 专有机制按 10-29 裁定不放进来。
  • 风险与回滚:Clause-②: yes (widening)、Metadata-protocol: added;v4 原有 732 个测试在 main 与本 PR 上都绿;方言接口只加 4 个可选成员。回滚即 revert。
  • 先看:packages/drivers/driver-odata/src/dialect-v2.ts;odata-driver.ts 的 4 个可选钩子;packages/spec/src/data/type-compat.ts 两行别名。
  • 两分钟验证:pnpm --filter @objectstack/driver-odata exec vitest run,1383 passed。
  • 需要你做的:无(非受管;契约复审 PASS;席位按本任落地授权落地)。

席位意见:ACCEPT。

席位核对

Head-sha: 667660add518c9a9e904cf24836ea68dfb09b5a3

  • Shape: 22 files, +2291 / −309. Nearly all of it is in packages/drivers/driver-odata: the v2 dialect, the four optional dialect hooks, the config's version arm, fixtures and tests. Outside the driver: one hits file (packages/spec/src/data/type-compat.ts, two aliases), its test, the service-datasource v2 import test, the driver README, the new "OData v2 services" section in content/docs/data-modeling/odata-datasources.mdx, and one changeset (driver-odata minor, spec minor). check-governed-merges --pr 28177: NOT governed (0 of 22 paths), 2600 changed lines, under the human-merge threshold.
  • Premise measured: on main, 627 v2 tests are red and all 732 v4 tests are green. After the change, driver-odata has 1383 passing (651 of them v2). 14 ablations each went red and each was restored. service-datasource has 1074 passing and spec src/data 4591. The three typechecks exit 0. dispatch-gates --ran derived 117 families and ran 117, with 0 NOT-MEASURED. pnpm lint exits 0, and check:generated reports all 14 artifacts up to date.
  • Gate check-runs on the head: 51 runs: 42 success, 9 skipped, 0 failure or pending. mergeable_state is clean.
  • Contract review: owed at CONTRACT_REVIEW_TIER (type-compat.ts), PASS on this head, record 6526958904. No must-fix. Points the seat carries:
    • The one should-fix was the missing third declaration line. It is answered as Skills-docs: updated here: the new section in odata-datasources.mdx and the driver README teach version: '2.0'. No skills/** file teaches the driver's dialects; datasources.md names the driver only for credential borrowing. The dev posted the line as a signed addendum, 6526989624, and no gate reads it from the body.
    • Correction to the PR body: "Nothing that passed before fails now" holds for every deployment, but not at the bare isCompatible face. There, text/json over Edm.DateTime/Edm.Time go from lossy to false. No deployment on main can reach that pair, because v2 refused at boot and CSDL 4.0 declares neither type. The arm stays yes (widening), the same shape as the odata arm's introduction in 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.
    • Accepted deviation: spec's own share is kind none, so minor on @objectstack/spec is one level over what that package needs. It is accepted as the kind-driven reading of AGENTS.md step 3.
  • Accepted deviations (dev's notes):
    • The v2 fixtures are written from the services' published shape, not recorded live, because the container has no route out. The XML header says so.
    • Prefer: odata.maxpagesize is still sent on a v2 GET. A v2 service ignores it (RFC 7240 §2), and the docs say so.
    • A zone-less date comparand on Edm.DateTime is left to the engine, by the rule v4 applies to Edm.DateTimeOffset.
  • Found in scope and fixed: a date field over v2 Edm.DateTime read back as a datetime. The plugin kit caught it, and the driver now reads a date under a field declared date.
  • Landing: under the maintainer's landing grant for this seat term (seat 4 thread, 2026-10-29 22:04Z): pr_ready + automerge_enable through the relay.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 30, 2026 15:57
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 30, 2026 15:58
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 30, 2026
Merged via the queue into main with commit d359857 Oct 30, 2026
52 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-9mtiyf branch October 30, 2026 16:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation protocol:data size/xl tests tooling

Projects

None yet

2 participants