Repository navigation
Description
epic:#27261#1 · marchtian · session_017ihL2c5z3q4h8inrn5kwqV
Filing gate: ③ maintainer-directed — 田騏 at 2026-10-29T21:58Z 「现在启动全部 P2」 (epic comment 6509296853, D1 amended), at 2026-10-29T21:59Z 「OData 驱动 只做 v4」 (6509311454), at 2026-10-29T22:01Z 「同意」 (generic P2 parts open source in this repository; vendor-specific drivers such as Salesforce stay out of it).
Ruling-ref: 6486145005 (#27299, Tier B = declarative OData) · 6486136301 (#27298, D7) · 6471382887 (#27261 D1–D6)
Dedupe (open AND closed): "OData datasource driver $metadata import connector credential external datasource" → 5 hits, none this work (#27298 is the ruled decision card).
Reader: the epic PM (#27261) dispatches it in the epic subtree.
type: Feature
维护者速读
- 做什么: 新包
packages/drivers/driver-odata,一个声明式 OData v4 数据源驱动。管理员声明服务地址和实体集(object.external.remoteName)即可把 SAP、Dynamics 365 等系统的实体作为对象实时读写,不写代码。 - 为什么: 面向全球客户,OData 是 SAP / Dynamics 365 的标准接口,也是 Salesforce Connect、Dataverse 声明式外部数据的底座。
- 范围: 只做 v4;代码里留「方言」接缝,将来 v2 另立卡补上。
- 需要您做的: 无。
Task
- Package
@objectstack/driver-odata, published by a server plugin as a plugin driver id (driver.odata), not added toDRIVER_VOCABULARY(config-registry.zod.ts), soneedsExternalDriverFloorholds it behind the objectql: virtual-object correctness floor —applyDriverQueryInMemoryhelper, per-datasource row cap with a loud refusal,supportshonoured for plugin drivers, refusal-envelope mapping for driver failures (epic #27261 P1) #27277 floor: whatever itssupportsdoes not claim is applied in memory under the 1000-row cap. State the choice in the PR; if a built-in id is argued for, stop and raise it. - Config (the driver's own config contract):
serviceRoot, auth throughexternal.credentialsRefuntil D7 lands (bearer / basic), page size, request timeout. - Reads:
find/findOne/counton the entity set named byobject.external.remoteName; key from$metadata. Push down$filter(comparison,and/or/not,in,contains/startswith/endswith),$orderby,$top/$skip,$count=true,$select; follow@odata.nextLink.supportsclaims only what is translated; anything else is left to the floor. - Writes (gated by ADR-0015's
allowWrites+writable): POST / PATCH / DELETE by key,If-Matchwith the row's@odata.etagwhen present; a remote 4xx surfaces through the floor'sEXTERNAL_WRITE_REFUSEDmapping. -
introspectSchemareads$metadata(EDMX CSDL v4): entity sets, property types, keys, navigation properties (as lookups only), for [Decision] Ontology over external systems: virtual objects on ADR-0015 federation — two driver tiers (plugin code · declarative OpenAPI), pushdown/identity/catalog/cache rules, Studio and AI drafts (2026 ontology research + deep design) #27261 P2's import card. - A dialect seam (v4 now): response envelope,
$metadatareader, filter-function and literal spellings and the count option go through one dialect object, so a v2 dialect is a new file, not a rewrite. - Tests: a recorded-fixture conformance suite against a public OData v4 sample service shape (TripPin-style
$metadata+ responses), pushdown translation tables, floor engagement (an untranslatable predicate is applied in memory), write refusal mapping.
Clause-②: decided by the dev (a new package; no spec key is planned) · Metadata-protocol: none unless the config contract lands in spec.
Activity
objectstack-fleet commentedon Oct 29, 2026
epic:#27261#1 · marchtian · session_017ihL2c5z3q4h8inrn5kwqV
Claim: PM loop round 8 2026-10-29T22:10Z
Session:
session_017ihL2c5z3q4h8inrn5kwqV(epic PM); implementing card threadsession_017ihL2gRrVctbMWaHbABDchAccount:
marchtianBranch:
claude/project-thread-wj01ct(the card thread's platform-assigned branch — measured platform exception toclaude/issue-<n>-<slug>)Worktree:
objectstack-issue-27741Domain:
domain:serverSeat:
epic:#27261#1(epic PM; nodomain:*seat). Maintainer order, 田騏 2026-10-29T21:58Z: 「现在启动全部 P2」; 2026-10-29T21:59Z 「OData 驱动 只做 v4」File surface:
packages/drivers/driver-odata/**(new package), the server plugin that publishesdriver.odata, workspace wiring the new package needs (pnpm-workspace.yaml/ root configs / lockfile),.changeset/**(stop on breach, including anypackages/spec/src/**orDRIVER_VOCABULARYedit; explain in the report)Container & model:
L,mode:thread,model: default tier (dispatch-gates --tier: no path-derived mandate),effort: highClause-②: no
Metadata-protocol: none
Responsibility: epic #27261 P2 (ruling 6486145005, letter B); the engine floor from #27277 is the correctness net it relies on
Thread-read: none
Premise: holds — on
origin/mainno OData driver exists (packages/drivers: memory, mongodb, sql, sqlite-wasm, turso), andneedsExternalDriverFloorfloors any non-built-in driver id on a non-managed datasource.Serial constraints: #27745 and #27747 are Blocked-by this card. #27742 / #27746 (D7) are not; use
credentialsRefauth only here.objectstack-fleet commentedon Oct 29, 2026
epic:#27261#1 · marchtian · session_017ihL2gRrVctbMWaHbABDch
os-dev-report
{ "issue": 27741, "status": "done", "branch": "claude/project-thread-wj01ct", "pr": "https://github.com/objectstack-ai/objectstack/pull/27796", "session": "session_017ihL2gRrVctbMWaHbABDch", "premise_still_valid": true, "summary": "Premise held on origin/main 81d00a4e: packages/drivers had memory, mongodb, sql, sqlite-wasm, turso and no OData driver; needsExternalDriverFloor floors any non-built-in driver id on a non-managed datasource. New package @objectstack/driver-odata (fixed group, 18.2.0): createODataDriverPlugin publishes ODataDriver as driver.NAME (default driver.odata), a plugin driver id; DRIVER_VOCABULARY and packages/spec/src untouched. Config contract (strict zod, parsed at plugin build): name, serviceRoot (http/https, refused with userinfo or query), version 4.0, auth none|bearer|basic(username), pageSize, requestTimeoutMs; no credential key. Credential = the datasource's external.credentialsRef, read from engine.listDatasourceDefs() and resolved through options.secrets (the host's SecretBinder read half) at the first request, re-resolved once on 401; fails closed on missing ref, missing store, unresolvable handle, undeclared datasource, ref under scheme none. Reads: $filter/$orderby/$select/$top/$skip, @odata.nextLink (same origin only), key appended to $orderby on paged reads, count via $count=true only when the where translated completely. Translator never narrower: $not never pushed, $ne/$nin on nullable carry 'or P eq null', text only eq/in/contains/startswith/endswith/ASCII icontains, unspellable comparands left to the floor. ReadableIds leaves refused by the driver itself (readableIdsDriverRefusal). Writes POST/PATCH/DELETE by key with If-Match from the last read's @odata.etag, declared properties only; upsert/bulk/transactions/execute/dropTable refused by name (400). introspectSchema: entity sets, EDM types, keys, single-valued navigations with bound set as lookup candidates (for #27745). Dialect seam: ODataDialect (headers, envelope, etag, error body, CSDL reader, literals, functions, in-list, count option); only odataV4Dialect ships. Two files outside the claimed surface, both mechanical consequences of a new published package: packages/spec/llms.txt package count 72 to 73 (check:llms-txt red otherwise; spec patch added to the changeset) — not packages/spec/src. D7 (#27746) case: not on main at bcc0a1a3 (git grep of origin/main packages/types/src for a connector-credential seam: 0 hits), so not wired; the auth header comes from one function, ODataTransport.authorization() in transport.ts, fed by the plugin's credential() source in plugin.ts — the later wiring replaces those two.", "tests": "On head bcc0a1a3: pnpm --filter @objectstack/driver-odata test = Test Files 6 passed (6), Tests 425 passed (425), exit 0; typecheck exit 0; eslint packages/drivers/driver-odata exit 0. Suites: filter.test.ts (54-row translation table), dialect-v4.test.ts (CSDL reader + envelope), odata-driver.test.ts (recorded TripPin-shaped service: reads, paging, writes, ETag, keys, introspection, credentials), odata-driver.floor.test.ts (real ObjectQL engine + floor, objects bound by engine.syncSchemas: in-memory application of $like, count through floor, 400 read = INVALID_QUERY, 400 write = EXTERNAL_WRITE_REFUSED 422, 503 withheld), plugin.test.ts (service name, config refusals, bearer/basic, 5 fail-closed), odata-driver.conformance.test.ts (all 11 shared case-sets, 326 cases, through engine+floor over an in-process OData service that evaluates the $filter, read sets under ordinal and case-insensitive collation). Ablation (one-shot): nullable $ne disjunction removed in filter.ts = 1 failed / 53 passed (the '$ne on a nullable property' row); restored, blob equals HEAD, git diff HEAD empty; tests import source.", "gates": "node scripts/pm/dispatch-gates.mjs --commands (no paths) on merged head bcc0a1a3 derived 77; --ran: '77 derived famil(ies) accounted for — 77 run, 0 NOT-MEASURED (a DERIVED zero — all 77 recorded an exit code and none of them is 3)'. All 77 exit 0. First pass on 02401a71 (72 derived): check:llms-txt exit 1 (count 72 vs 73) and check:driver-conformance exit 1 (11 CONSUMED cells) — both fixed, now 66 covered cells, 0 DEBT, 0 EXEMPT; check-plugin-teardown-shape --self-test and check:dual-build-cjs-loads exit 3 (prerequisites) — fixture commit fetched, dist present on the rerun, both exit 0. CI check-runs on #27796 not yet read (PR just opened).", "dogfood_battery": "n/a — no ! in the title and no (narrowing) arm", "line_budget": "no line budget in the claim; three-dot diff vs origin/main: 26 files, +3689/-2 = 3691 changed lines, OVER the 3000 human-merge threshold — lands only with an authorized APPROVE (LICENSE copy is 201 lines)", "files_changed": [ "packages/drivers/driver-odata/** (new package: package.json, tsconfig.json, vitest.config.ts, README.md, LICENSE, src/{index,config,dialect,dialect-v4,filter,transport,odata-driver,plugin}.ts, 6 test files, src/__fixtures__/{trippin-metadata.xml,recorded-service.ts,odata-service.ts})", ".changeset/27741-driver-odata.md", ".changeset/config.json", "pnpm-lock.yaml", "packages/spec/llms.txt" ], "mcp_calls": "reads only: issue_read (get #27741, get_comments #27741, get_comments #27666), pull_request_read (get #27648 for the body form). No MCP writes.", "api_writes": "1 relay write so far as objectstack-fleet[bot]: fleet-write pr_create, run 40974161877 = #27796 (draft forced, labels [enhancement], no assignees; read-back 12930 bytes sent, 12930 stored, identical). Plus this report via post-stamped --comment=27741. Plus git pushes of the branch (head bcc0a1a3).", "open_questions": [ "Landing needs 田騏's APPROVE on #27796 (3691 lines over the 3000 threshold)." ], "deviations": [ "Report comment: the seat signature line comes first and os-dev-report second, as post-stamped requires.", "packages/spec/llms.txt edited (two numbers) outside the claimed file surface; it is not packages/spec/src and not DRIVER_VOCABULARY; the edit is forced by check:llms-txt on any new published package. @objectstack/spec added to the changeset for it.", "A first gate pass was interrupted by a container restart and resumed from the recorded exit codes; the final --ran reconciliation is over all 77 on bcc0a1a3." ], "out_of_scope_findings": [ "carrier: none · noted, not filed — no $batch arm: the bulk verbs are refused rather than looped; a follow-up if a customer needs atomic multi-row writes. Dedupe words: odata $batch bulk write driver-odata" ] }objectstack-fleet commentedon Oct 30, 2026
epic:#27261#1 · marchtian · session_017ihL2c5z3q4h8inrn5kwqV
Landed · 2026-10-30T01:33Z
PR #27796 merged through the queue as
0a4b0fb223on 2026-10-30T01:32:02Z (head7f91132b; seat ACCEPT 6512051940, the review of record for a no-contract-face PR; APPROVED byos-zhuangpast the 3000-line gate).@objectstack/driver-odatais onmain: a declarative OData v4 driver published as the plugin driver iddriver.odata, held behind the engine floor, with a v2 dialect seam. Follow-ons: #27798 (#27744) adds the driver-odata test that runs the conformance kit; #27819 (#27746) wires the D7 connector credential intoODataTransport.authorization()'s source; #27745 ($metadataimport) and #27747 (guide) are unblocked and dispatched next.