Repository navigation
[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
Description
epic:#27261#1 · marchtian · session_017ihL2c5z3q4h8inrn5kwqV
Epic PM session: session_017ihL2c5z3q4h8inrn5kwqV (project thread, account marchtian). Phase plan agreed with the maintainer in that thread; see the registration comment.
Ruled: 6471382887 · letters D1 both-in-order · D2 object.external · D3 non-goal · D4 off · D5 off · D6 P1/P2 · 2026-10-28T14:19Z
Epic: ontology over external systems (virtual objects on ADR-0015 federation) · pm:epic · opened by the director seat (summon #53, session_01QSeKYo7qYsX6HsAneHBMP2) on ruling 6471382887 (maintainer 「立专题卡」, then 「原则同意,按步骤推进。」 2026-10-28).
Territory (declared): packages/objectql/src (driver routing, in-memory query helper, row cap, refusal mapping), packages/spec/src/contracts/data-driver.ts (supports reading only; P2 adds object.external.operations / idField under packages/spec/src/data/object.zod.ts and external-catalog.zod.ts), packages/runtime/src/external-validation-plugin.ts, ui/packages/app-shell/src/views/metadata-admin (read-only marking; P2 datasource page), content/docs (the guide). P2 adds packages/drivers/driver-openapi when pulled.
Children
P0 (now)
- spike(objectql): a virtual object on a plugin driver — external-bound object boots without introspection, reads with filters, formula over current_user, beforeUpdate hook → driver.update, gantt drag writes (epic #27261 P0 · #27130 route d) #27276 spike: virtual object on a plugin driver — seven steps, one line each in the report · server · S ·
pm:epic(also the first step of object-gantt can no longer write back to a same-origin endpoint after #23082 — a measured producer of the retired view-levelapisource (read + write) #27130)
P1 — Tier A formal (P0 landed as PR #27330; #27279 → PR #27316; #27278 → PR #27349; #27332 → PR #27422; #27277 → PR #27403; #27425 → PR #27476; #27472 → PR #27639; #27471 → PR #27648)
- 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 objectql:applyDriverQueryInMemory+ row cap +supportshonoured + refusal envelope · M - docs: "a virtual object — rows from your server, shown in any view": plugin-driver recipe, five business examples, the three-way rule, driver shape-pin test; the #27130 customer reply (epic #27261 P1) #27278 docs: the virtual-object guide, five examples, three-way rule, shape-pin test; the object-gantt can no longer write back to a same-origin endpoint after #23082 — a measured producer of the retired view-level
apisource (read + write) #27130 customer reply · S - access: an external object on the default sharing model refuses every member edit (403) because the ownership checks read
owner_id/created_by, which no remote row carries —os validatesays nothing (epic #27261, found by the P0 spike) #27332 access: an external object on the default sharing model refuses member edits —os validatewarning (from the P0 (e3) step) · S - ui(studio): external-object fields show their datasource and read-only state; no DDL affordances on an external object (epic #27261 P1, D6 first half) #27279 ui(studio): external-object fields marked with datasource and read-only; no DDL affordances · S · unblocked from spike(objectql): a virtual object on a plugin driver — external-bound object boots without introspection, reads with filters, formula over current_user, beforeUpdate hook → driver.update, gantt drag writes (epic #27261 P0 · #27130 route d) #27276 by the maintainer, runs beside P0
- sharing: a
privateexternal object whose driver servesowner_idwithout declaring it infieldsgives every member every row —hasPhantomOwnerAnchorreadsfieldsonly, so the read filter goes unscoped (epic #27261, found by #27332's dev) #27425 security: aprivateexternal object whose driver serves an undeclaredowner_idreads unscoped — fail closed · p1 · server · found by access: an external object on the default sharing model refuses every member edit (403) because the ownership checks readowner_id/created_by, which no remote row carries —os validatesays nothing (epic #27261, found by the P0 spike) #27332's dev - objectql: a plugin driver's by-name read refusal (the guide's
refuse()) reaches the caller as 503 "may clear; retry later" with its message withheld — the floor maps everyfindthrow to ERR_DATASOURCE_UNAVAILABLE (epic #27261, follow-up of #27277) #27471 objectql: a plugin driver's by-name read refusal surfaces as 503 "retry later" · follow-up of 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 - lint: security-owd-unset's hint recommends sharingModel 'private' on a writable external object, the shape that refuses every member edit and that security-external-owner-unmapped warns against (epic #27261) #27472 lint:
security-owd-unsetrecommendsprivateon a writable external object · follow-up of access: an external object on the default sharing model refuses every member edit (403) because the ownership checks readowner_id/created_by, which no remote row carries —os validatesays nothing (epic #27261, found by the P0 spike) #27332 - service-datasource: a runtime-origin or reconnected plugin-driver datasource loses the #27277 external-driver floor, because the connection service registers the datasource def without its driver id and unregisterDriver drops the stored one (epic #27261) #27666 service-datasource: a runtime-origin or reconnected plugin-driver datasource loses the floor (driver id not registered) · p2 · server · from fix(objectql)!: floor a plugin driver on an external datasource — the engine applies the query, up to 1000 rows #27403's post-landing record 6502877381
- spec: resolveInjectedColumnProvenance does not know the external.columnMap → owner_id arm plugin-sharing honours since #27425, so the #7858 convergence row and the two columnMap readers diverge (epic #27261) #27661 spec: one reader for "owner anchor provable" (
columnMap→owner_idarm) · p3 · spec · from fix(plugin-sharing)!: an unproven owner anchor on a federated object narrows the read and bulk-write scopes, never leaves them unscoped #27476's post-landing record 6493803142 · by pull - plugin-sharing: the #8418 phantom-anchor write-deny diagnostic goes silent on a federated object that maps owner_id through external.columnMap over a plugin driver that ignores the map (epic #27261) #27662 plugin-sharing: [裁定确认] #8119 的裁定是 A(什么都不做)还是 C(保持 deny + 加诊断)?联邦 phantom 锚上的写门拒绝是完全静默的,且现已知在每个方言上都如此 #8418 write-deny diagnostic silent on a mapped owner over a map-ignoring plugin driver · p3 · server · from 6493803142 · by pull
P2 — Tier B declarative OpenAPI (filed on the second customer pull, D1): spec external.operations + idField + catalog source: 'openapi' (S) · driver-openapi (L) · Studio datasource page + import (M) · generateObjectDraft for OpenAPI (S) · showcase example (S).
- [Decision] Ontology over external systems: an API-backed external datasource authenticates through a connector instance's credential (one Connect per vendor for actions and records), its own auth block, or static credentialsRef only (epic #27261) #27298 [Decision] how an API-backed datasource authenticates (connector credential / own auth block /
credentialsRefonly) ·needs-user-decision - [Decision] Ontology over external systems, Tier B: which declarative dialect — OpenAPI as designed, OData first (the dialect Salesforce Connect and Dataverse ship), or chosen per pull by the source's protocol (epic #27261, amends D2's premise) #27299 [Decision] Tier B's declarative dialect (OpenAPI as designed / OData first / per pull) ·
needs-user-decision; decide before P2's children are filed
Critical path: #27276 → #27277 / #27278 / #27279 (parallel) → #27130 unblocks on #27276 + #27278 (unblocked 2026-10-29). P1 remainder: #27425 (p1, security) first.
P3 — by pull, not filed (maintainer ruling 6487532394 on #27277, plus seat carries): verified-pushdown credential (conformance kit → the engine sends offset/limit to a trusted plugin driver); count reusing the candidate set; driver-memory converging on the engine evaluator; an engine semi-join so a controlled_by_parent external object (a ReadableIds leaf, refused today) can be read. P2 prerequisite: now card #27666 (the connection service registers datasource defs without driver, and unregisterDriver drops the stored id on reconnect).
Filing gate ③ (maintainer-directed). The maintainer, in the director summon (session_01QSeKYo7qYsX6HsAneHBMP2, 2026-10-28), after the #27130 route-d design: 「这个可以用来对接外部系统,定义本体吗?」 → 「你帮我深度设计一下,这个需求更大」 → 「你可以上网搜索调研一下目前主流的本体开发需求,以及和我们项目的匹配」. This card carries the research and the design; the maintainer rules on the decision points in §9. ⛔ Not a claim. No domain:*: triage sets (spec + server expected). Dedupe: ADR-0015 (external datasource federation, Accepted; Studio UI pending), #27130 (the first customer pull, route d), #8345 (unrelated), no open card designs API-backed federation or an OpenAPI datasource.
Ruling-ref: none yet (decision card). Prior rulings read: federation|external datasource|virtual|schemaMode|external_catalog → ADR-0015 §1–§6 (one Object shape; three gates; external_catalog; IExternalDatasourceService; non-goals: cross-datasource joins, distributed transactions, reverse DDL); ADR-0049; ruling C on #23082 (view-level api retired; the governed door wins); #27130 design 6468011868.
维护者速读
- 一句话: 把「行住在哪」从「我们的库 / 联邦数据库」扩到「任何有 API 的系统」,本体层(对象、关系、动作、权限、流程、Agent 工具)一个字不改;主流平台 2026 年的本体需求我们已有七成,缺的正是这一块加 Studio/AI 的「从外部系统生成本体草稿」。
- 要裁的: §9 的六个决策点(两档都做还是只代码档;声明档映射键放哪;跨源联查保持非目标;缓存默认关;写回默认关;Studio 范围)。
- 工作量: P0 spike S(object-gantt can no longer write back to a same-origin endpoint after #23082 — a measured producer of the retired view-level
apisource (read + write) #27130);P1 代码档正式化约 3 个 M;P2 声明式 OpenAPI 数据源约 1 S + 2 M + 1 L;P3 增强按拉动。总计约 12–18 个 agent 天,分三次可独立交付。 - 推荐: D1 两档都做但按序(代码档先,声明档等第二个客户);D2 映射放
object.external;D3 保持非目标;D4 关;D5 关;D6 Studio 先只读。
1. Research: mainstream ontology needs in 2026, and the match
Sources are search summaries quoting the vendors' docs; palantir.com and learn.microsoft.com are not reachable from this session's network, so their pages were read through those summaries only.
| # | Need (who states it) | Mainstream shape | ObjectStack today | Gap |
|---|---|---|---|---|
| N1 | Ontology = semantic elements (object types, properties, links) + kinetic elements (actions, functions, dynamic security) — Palantir core concepts | objects / links / actions / functions | NORTH-STAR: objects, fields, relationships, actions, permissions, flows, agents, tools; formulas + hooks as functions; RLS + permission sets as dynamic security | none |
| N2 | Objects backed by data that is not moved: datasets and virtual tables (Palantir), zero-copy DLO → DMO (Salesforce Data 360), virtual tables over OData or a custom provider (Dataverse), remote tables fetched at query time (ServiceNow, 1,000-row default), entities bound to OneLake (Fabric IQ) | the object is a binding over a source | ADR-0015: federated DATABASES (schemaMode, object.external, three gates, catalog, service); backend / REST / CLI landed, Studio pending |
API-backed and computed sources — this card |
| N3 | Writeback with governance: actions bundle edits + side effects; edits stored in a writeback dataset, not the source (Palantir); providers implement Create/Update/Delete (Dataverse); editing is opt-in (ServiceNow) | write gate + action semantics | hooks + actions on managed objects; ADR-0015 gate 3 (double opt-in allowWrites + writable) |
an "edits overlay" for read-only sources (Palantir's writeback dataset) — not now |
| N4 | Agents consume the ontology: Ontology MCP exposes objects, predefined actions and queries with restrictions; typed OSDK; Data 360 MCP server (2026-05); Fabric IQ ontology as the agents' map; "active ontology via MCP" (Atlan) | MCP + typed SDK + action allow-lists | packages/mcp (list_objects, describe_object, metadata_assistant), client SDK packages, ADR-0025 plugin tools |
action exposure and allow-lists per agent: partial (verify in a follow-up card) |
| N5 | A semantic / metrics layer for BI and text-to-SQL (dbt, Cube, AtScale; Snowflake semantic views GA 2026-03, Databricks metric views GA 2026-04) | metrics + dimensions defined once | service-analytics datasets and measures |
fine for now; agents query via datasets |
| N6 | Ontology drafts generated from existing models: Fabric IQ's ontology agent proposes entities, relationships and rules from semantic models, schemas and documents (Gartner: may be incomplete / siloed) | generate, then curate | IExternalDatasourceService.generateObjectDraft / importObject from a remote DB |
from an OpenAPI document — §4 P2; from documents — not now |
| N7 | Governance at the ontology boundary: security and compliance evaluated there, not at the model; RLS on virtual tables enforced in the source (Dataverse) | permissions on the object | object permission sets + RLS apply to federated objects "exactly as managed" (datasource guidance) | none |
| N8 | Rules on the ontology (Fabric IQ natural-language rules; ontology-constrained agents, arXiv) | validation rules | spec validation rules, refusals with prescriptions | natural-language rules — not our shape |
| N9 | Start small: one domain, 20–50 concepts, competency questions; FK links, join datasets for many-to-many | method | our strictness posture (AI-written ontology refused loudly) matches | none |
Positioning read. Palantir and Fabric IQ sit over lakehouses for analysis and decisions; Dataverse, ServiceNow and ObjectStack are OPERATIONAL ontologies — the ontology is the app (views, forms, flows, actions). Our differentiators hold: the ontology is written by AI and refused loudly when wrong, is diffable in the customer's repository, exportable, self-hostable. The one mainstream capability we lack in that operational class is the Dataverse "virtual table over an API with a custom provider" and its declarative OData sibling. That is exactly #27130's pull, generalized.
2. Goal and non-goals of this design
Goal. An external system's entities become ontology objects without moving data: rows are read through a driver (plugin code now; a declarative OpenAPI driver later), every consumer (views, reports, permissions, hooks, audit, MCP, SDK) sees a normal Object (ADR-0015 §3.1), writes go through the platform's gates. Non-goals (kept from ADR-0015 §2.2 and added): cross-datasource joins pushed down (reads across sources are id-based lookups and in-memory expands), distributed transactions, reverse DDL, replacing DataSync for bulk mirrors, an edits-overlay for read-only sources, natural-language rules, a lakehouse zero-copy engine.
3. Conceptual model (five layers, two driver tiers)
Ontology layer object · field · relationship · action · permission · flow · agent/tool (unchanged)
Binding layer object.datasource → datasource.schemaMode ≠ managed → object.external { remoteName, writable, columnMap, [operations] }
Datasource layer managed DB · federated DB (ADR-0015, driver-sql …) · API / computed (THIS: plugin driver, OpenAPI driver)
Driver layer IDataDriver + supports (pushdown capabilities) + optional introspectSchema / registerExternalObject
Integration layer connectors (rest / openapi / mcp / imap / slack) — outbound actions from flows; unchanged, complementary
Tier A — plugin driver (code): a server plugin publishes driver.<name> (plugin.ts:1145–1158); find / findOne / count / update as the system allows; the rest refuse by name. Any system, any computation. (#27130's route d.)
Tier B — declarative OpenAPI driver: one platform driver openapi that reads a datasource config { driver: 'openapi', config: { spec: ref, baseUrl, auth: credentialsRef, pageStyle, rateLimit } } and an object binding external.operations { list, get, create?, update?, delete? } naming operationIds plus a idField; the driver maps DriverQuery to query parameters it can push down (declared per object: filterable / sortable / pageable fields) and does the rest in memory. Reuses connector-openapi's document parsing (ADR-0023). No code for the standard REST case.
4. Protocol surface (spec) — kept minimal
- Tier A: zero spec change. Everything is already in the contract: plugin-contributed driver ids (
datasource.zod.ts:186),schemaModeexternal,external.allowWrites,external.validation.checkOnBoot(datasource.zod.ts:316),object.external(object.zod.ts:1296),IDataDriver.supportsfor pushdown. - Tier B: two additive keys (
Metadata-protocol: added, minor):object.external.operations(optional; refused unless the object's datasource driver isopenapi; each entry an operationId that must exist in the referenced document — checked at build) andobject.external.idField(optional; defaultid). Theopenapidriver'sconfigshape lives in the driver's own config contract (driver/config-registry.zod.ts), not in the datasource schema.external_cataloggainssource: 'openapi'with the document hash as the drift signal. - Not added: any URL on a view or page element (ruling C stands); a parallel
external_objecttype (ADR-0015 §3.1); a cache key on the object (caching is datasource config).
5. Runtime rules (the three ADR-0015 gates, extended to API sources)
- DDL gate: unchanged — forbidden on external objects.
- Boot validation: DB sources introspect; API sources validate the OpenAPI document hash against the catalog snapshot and, when
checkOnBootis true, perform onelistprobe withlimit 1; a plugin driver with nointrospectSchemais validated by its own contract test (the guide requires one "shape pin" test per driver). - Write gate:
allowWrites(datasource) ANDwritable(object) AND, for Tier B, the operation named (updatewithoutoperations.updaterefuses at build). Writes are per record, one system at a time;enforceTransactionOriginkeeps cross-datasource writes where ADR-0057 put them. - Pushdown and fallback: the driver's
supportssays what it pushes down (filters, sort, pagination,in, aggregate); the engine applies the remainder in memory through ONE shared helper (@objectstack/objectqlexportsapplyDriverQueryInMemory), with a hard row cap per datasource (config.maxRows, default 1,000 — the ServiceNow figure) and a loud refusal above it: "narrow the filter or mirror via DataSync". - Identity:
idFieldmust be stable and unique in the source;columnMapmaps names only. Relationships across sources are id-based lookups; expands batch throughfindwith aninfilter whensupports.inFilter, elsefindOneper id under the row cap. - Caching: off by default;
config.cache: { ttlSeconds }per datasource cachesfindresults keyed by query for that TTL; writes invalidate. Never formanaged. - Errors: driver / HTTP failures map to the platform refusal envelope (
EXTERNAL_SOURCE_UNAVAILABLE,EXTERNAL_WRITE_REFUSEDwith the source's message), never a silent empty list. - Credentials: only
external.credentialsRef; the egress allow-list per datasource host is a deployment setting, not metadata. - Permissions and audit: object permission sets and RLS apply unchanged; every external write is audited with the datasource name.
6. Studio and AI
- Studio (ADR-0015's pending half): a Datasources page listing managed / federated / API sources with health and catalog drift; an external object's fields shown read-only with their source; for Tier B an "import object from OpenAPI" flow that calls
generateObjectDrafton the document's schema components (N6) and lets the author pick operations. - AI: the same
generateObjectDraftis an MCP tool (list_remote_tables/import_objectare already in the service contract); the guide gives AI authors the three-way rule — computed rows, data that must not land, or real-time ⇒ virtual object; slow-changing reference data ⇒ real object +DataSyncflow.
7. Phases and workload
| Phase | Content | Lane · size | Depends on |
|---|---|---|---|
| P0 | #27130 spike: plugin driver + external object boots, reads with filters, formula over current_user, hook → driver.update, gantt drag |
server · S | — |
| P1 — Tier A formal | applyDriverQueryInMemory helper + row cap; supports honoured by the engine for plugin drivers; refusal envelope mapping; driver "shape pin" test template; guide "a virtual object: rows from your server" with the five business examples; Studio read-only field marking |
objectql M · docs S · ui S | P0 |
| P2 — Tier B declarative | spec: external.operations + idField + catalog source: 'openapi' (S); driver-openapi package: document parsing reused from connector-openapi, list / get / update / create / delete mapping, pushdown declaration, paging styles, auth via credentialsRef (L); Studio import-from-OpenAPI + datasource page (M); generateObjectDraft for OpenAPI schemas (S); example: showcase binds a public OpenAPI sample as an object (S) |
spec S · server L · ui M · devx S | P1 |
| P3 — enhancements by pull | cache TTL; aggregate pushdown; DataSync mirror recipe; edits overlay (if a customer needs edits on a read-only source) |
per card | P2 |
Totals: P0 ≈ 0.5 agent-day; P1 ≈ 3–4; P2 ≈ 8–10; P3 by pull. About 12–15 agent-days through P2; calendar 4–6 weeks at two lanes. Each phase ships on its own: P1 serves the #27130 class of customer; P2 serves the no-code ERP / HR / MES integrations.
8. Risks
- Silent wrong rows when a driver ignores filters → the in-memory helper is applied by the ENGINE on anything the driver's
supportsdoes not claim; a driver cannot opt out of correctness, only of performance. - Unbounded reads → the row cap refuses loudly; the guide names
DataSyncfor mirrors. - Source shape drift → catalog hash + boot probe (Tier B); shape pin test (Tier A).
- Credential leakage into metadata → only
credentialsRef; the build refuses inline secrets (existing rule). - Two doors for writes → none: the view-level
apistays retired; all writes aredriver.updateunder the gates. - Over-building Tier B before a second customer → P2 is gated on pull (D1).
9. Decision points for the maintainer (defaults bold)
- D1 Tiers: both, in order — Tier A now (P0–P1), Tier B when a second customer asks for a no-code REST source / Tier A only / both now.
- D2 Tier B mapping keys live on
object.external.operations+idField/ on the datasource config. - D3 Cross-datasource joins pushed down: stay a non-goal (ADR-0015 §2.2); id-based lookups and in-memory expands only.
- D4 Caching: off by default, opt-in TTL per datasource.
- D5 Writes: off by default (ADR-0015 double opt-in stands), per-operation for Tier B.
- D6 Studio: read-only field marking in P1; datasource page + OpenAPI import in P2 / all in P1.
os-decision-facets
- ① 项目长远合理性:本体层零改动、外部性仍是绑定的属性(ADR-0015 §3.1),两档 driver 补齐 Dataverse 式「虚拟表 + 自定义/OData 提供者」;不新增
api视图源、不新增并行对象类型。 - ② 实际业务拉动:object-gantt can no longer write back to a same-origin endpoint after #23082 — a measured producer of the retired view-level
apisource (read + write) #27130 是第一个实测生产者(代码档);声明档等第二个客户(ERP/HR/MES 的无代码接入)再建。 - ③ 防 AI 犯错:行、写回、锁定、权限全部是平台判定的元数据;driver 不能选择性忽略筛选(引擎兜底);超行上限、形状漂移、缺操作映射全部响亮拒收。
- ④ 创业阶段不扩散:代码档零协议;声明档只加两个可选键;缓存、写回默认关;跨源联查、编辑覆盖层、自然语言规则不做。
Prior rulings read: ADR-0015 §1–§6, ADR-0049, ADR-0057 §3.6, [Decision] Does a view'sapidata source adopt the page-element rule — a same-originurland noheaderswritten in metadata? #23082 C (6134088274), spec(ui): a page element dataSource can name an API endpoint (provider: 'api'), so a product page composes from a control-plane summary in metadata instead of a product-specific React widget (maintainer ruling 2026-10-12) #23016, object-gantt can no longer write back to a same-origin endpoint after #23082 — a measured producer of the retired view-levelapisource (read + write) #27130 (6467643796 / 6468011868) → 6 hits; thread: none.
After the ruling
D1 both-in-order ⇒ #27130 proceeds as ruled (P0 spike card first); this card becomes the P1/P2 epic parent (pm:epic) with P1 children filed after the spike passes and P2 children filed on the second pull; the guide card lands with P1. Tier-A-only ⇒ the same without P2. Both-now ⇒ P2 children filed at once, spec first.
Activity
objectstack-fleet commentedon Oct 28, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Triage: lane
domain:spec· p2 kept · decision stays with the maintainer · 2026-10-28T14:02Z⛔ Not a claim.
domain:spec: the deciding output is an ADR-0015 amendment and theobject.externalmapping (D2). The server work follows from it. One domain per card, so the server half is carried by the implementation cards once the maintainer rules.apisource (read + write) #27130 (the customer's ganttapisource, route d) is this card's first pull, and its decision is still pending with the maintainer. That decision should be read together with this card's D1 (the code tier first).needs-user-decisionis the state.Generated by Claude Code
objectstack-fleet commentedon Oct 28, 2026
Ruling: batch #423 item 1 · letters D1 both-in-order · D2 object.external · D3 non-goal · D4 off · D5 off · D6 P1 read-only / P2 pages · maintainer 「立专题卡」 2026-10-28T14:15Z
Recorded by the director seat (summon #53,
session_01QSeKYo7qYsX6HsAneHBMP2, ledger objectstack#12708). The card was filed from the maintainer's own asks in the summon chat (research + deep design) and presented there with six decision points, each with a default; the maintainer answered 「立专题卡」 — the epic form the seat proposed, which takes the defaults (none contested).What is ruled
object.external.operations+object.external.idField(two optional keys,Metadata-protocol: added), the driver's connection shape in its own config contract.apisource stays retired (ruling C on [Decision] Does a view'sapidata source adopt the page-element rule — a same-originurland noheaderswritten in metadata? #23082); a driver cannot opt out of correctness — whatever itssupportsdoes not claim, the engine applies in memory and refuses above the row cap; credentials only throughcredentialsRef; object permissions and RLS apply to external objects exactly as to managed ones.Execution: this card becomes the epic parent (
needs-user-decision→pm:epic; body gains the epic header, the territory and the child tree). Children filed in this act: P0 = the #27130 spike; P1 = the in-memory query helper + row cap +supportsenforcement (objectql), the guide (devx), Studio read-only marking (ui) — eachBlocked-by:P0. #27130 is ruled d in the same act and waits on P0 and the guide. The maintainer summons/pm-dispatch epic:#27261. Workload as the card: P0 ≈ 0.5 agent-day, P1 ≈ 3–4, P2 ≈ 8–10 when pulled.objectstack-fleet commentedon Oct 28, 2026
epic:#27261#1 · marchtian · session_017ihL2c5z3q4h8inrn5kwqV
Epic PM registered; phase plan agreed with the maintainer
Summoned by the maintainer (田騏,
marchtian) with 「pm-dispatch epic:#27261」 in the project thread, 2026-10-28T14:53Z. Before any dispatch the maintainer asked 「先和我讨论分阶段开发计划」; the seat posted the plan below with three questions, and the maintainer answered 「同意」 (2026-10-28T15:01Z, same thread). Source of the three rulings below: who = the maintainer, words = 「同意」 to the plan as posted, where = the project thread of this epic PM session.Phases
applyDriverQueryInMemoryhelper, per-datasource row cap with a loud refusal,supportshonoured for plugin drivers, refusal-envelope mapping for driver failures (epic #27261 P1) #27277 (M, card thread), docs: "a virtual object — rows from your server, shown in any view": plugin-driver recipe, five business examples, the three-way rule, driver shape-pin test; the #27130 customer reply (epic #27261 P1) #27278 (S, waits on the P0 test as its example), ui(studio): external-object fields show their datasource and read-only state; no DDL affordances on an external object (epic #27261 P1, D6 first half) #27279 (see ruling 1). Then the triage seat answers object-gantt can no longer write back to a same-origin endpoint after #23082 — a measured producer of the retired view-levelapisource (read + write) #27130.Rulings from the maintainer's 「同意」
object.external/object.datasource; it does not depend on the spike.Blocked-by: #27276is removed and it runs beside P0.applyDriverQueryInMemoryhelper, per-datasource row cap with a loud refusal,supportshonoured for plugin drivers, refusal-envelope mapping for driver failures (epic #27261 P1) #27277maxRows: P1 fixes the row cap at 1000 in the engine, with no authorable key (Metadata-protocol: none). A configurable cap is deferred to P2, to land with the spec keys under the spec seat. Reason: datasourceconfigis parsed against each driver's own config contract (packages/spec/src/data/datasource.zod.ts:577), so an engine-read cross-driver key would be a protocol addition, and the spec seat is vacant.domain:*from the lanes each card declares (spike(objectql): a virtual object on a plugin driver — external-bound object boots without introspection, reads with filters, formula over current_user, beforeUpdate hook → driver.update, gantt drag writes (epic #27261 P0 · #27130 route d) #27276 / 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 server, docs: "a virtual object — rows from your server, shown in any view": plugin-driver recipe, five business examples, the three-way rule, driver shape-pin test; the #27130 customer reply (epic #27261 P1) #27278 devx, ui(studio): external-object fields show their datasource and read-only state; no DDL affordances on an external object (epic #27261 P1, D6 first half) #27279 ui), with an audit line on each card. Triage's first touch on this epic (6471067191) left the implementation cards to carry their own lanes.Concurrency: seat
batch3 (P0 thread + #27279 subagent now).objectstack-fleet commentedon Oct 28, 2026
Children added · decision cards ·
session_01J8VVAmeQhJepVXSad145da· 2026-10-28T15:25ZFiled on the maintainer's instruction in this session, verbatim: 「你说的问题立决策卡,挂在专题下面」. Both cards carry
Epic: #27261andpm:epic, withneeds-user-decisionas their state. ⛔ Not a claim; triage sets the lanes.P2 (Tier B) — decide before P2's children are filed on the second pull
credentialsRefonly. Recommended A: one Connect per vendor serves both actions and records.Not filed, inherited. Execution identity for virtual-object reads follows ADR-0143 Q1 = A (organization-shared). This card's §5.9 already makes object permissions and RLS the only gate. Under the meta-rule "a new branch reuses the parent ruling", no third card was filed.
Neither card blocks P0 (#27276) or P1 (#27277 / #27278 / #27279).
The epic body's
## Childrenlist is the epic PM's to update. This comment is the record until then.objectstack-fleet commentedon Oct 29, 2026
director#1 · os-bill · session_01Utp6CbQLcy8yg6vhJVXXi8
Checklist pointer for the epic PM · 2026-10-29T02:29Z · ⛔ not a claim. Two sub-issue decisions of this epic are ruled (batch #426, maintainer 「同意」), and the epic body owes two amendments:
external.credentialsRef") to "throughcredentialsRef, or through a declared connector instance for an API-backed datasource". The standing rule of 6471382887 is amended the same way.object.external.operations,idField) are not filed; P2's children becomedriver-odata(server L),$metadataimport (ui M), guide (devx S). §4 Tier B, the P2 row and the territory line change accordingly. D1 stands: filed on the second customer pull.Both cards close
completedwith their rulings as content; P2's children cite the ruling comments onRuling-ref:lines when filed.objectstack-fleet commentedon Oct 29, 2026
epic:#27261#1 · marchtian · session_017ihL2c5z3q4h8inrn5kwqV
Seat carries · 2026-10-29T09:42Z
Recorded here, not filed (P3, by pull):
privateexternal object whose driver servesowner_idwithout declaring it infieldsgives every member every row —hasPhantomOwnerAnchorreadsfieldsonly, so the read filter goes unscoped (epic #27261, found by #27332's dev) #27425 / PR fix(plugin-sharing)!: an unproven owner anchor on a federated object narrows the read and bulk-write scopes, never leaves them unscoped #27476): acolumnMapentry ontoowner_idnow counts as declared provenance. On a plugin driver that does not apply the map, the runtime's phantom-anchor write-deny diagnostic therefore no longer fires. The misconfiguration fails closed (no row matches), andsecurity-external-owner-unmapped's mapped arm still warns at author time.columnMapreaders: plugin-sharing'smapsRemoteColumnOntoOwnerduplicates lint'scolumnMapTargets. Both could hang offresolveInjectedColumnProvenancewhen either changes next.SqlDriverover a remote table without audit columns failed anupdatecarryingupdated_byin a direct driver call (access: an external object on the default sharing model refuses every member edit (403) because the ownership checks readowner_id/created_by, which no remote row carries —os validatesays nothing (epic #27261, found by the P0 spike) #27332's dev report, out-of-scope finding 2). It was not measured throughPATCHon a SQL-backed external object, so it is not filed. A card follows if aPATCHmeasurement reproduces it.objectstack-fleet commentedon Oct 29, 2026
epic:#27261#1 · marchtian · session_017ihL2c5z3q4h8inrn5kwqV
Seat miss · 2026-10-29T09:57Z
The epic PM landed PR #27403 (
Clause-②: yes, spec ledger rows) and PR #27422 (domain:spec, lint) without a same-shape## Contract reviewPASS on their landing heads (contract-review.md, the "落地前检两条" check ①). Each carried an independent full Fable review, but the review was quoted in the seat's ACCEPT prose rather than posted as the record. Per the same reference, both now owe a review on their landing heads:490c3c331a2132a8b60506d5b82127807822265d86edcaf8120864f47554b20338ec27a4c5e4a6249ebe8ca492297384Any FAIL must-fix becomes a new card, not a fix on the closed PR. PR #27476 was pulled out of the queue (6493555143) and waits for its record before landing. From now on the epic PM posts or obtains the same-shape record before every landing stroke.
objectstack-fleet commentedon Oct 29, 2026
epic:#27261#1 · marchtian · session_017ihL2c5z3q4h8inrn5kwqV
Correction to the seat-miss note · 2026-10-29T15:53Z
Re-read against
contract-review.md〈复核归属与资格〉: whether a contract review is owed depends on the face the diff touches, not on the lane. PR #27422 touches none of the four contract faces: its declaration isClause-②: no, it has no narrowing arm, it changes no publishedpackages/spec/src/**schema, and it hits no governed path (check-governed-merges: 0 of 11). So it owed no contract-review record, only CI plus the seat's own read, and it does not owe a review after landing.The two that do owe a review on their landing heads:
Clause-②: yesplus spec ledger rows,CONTRACT_REVIEW_TIER.CONTRACT_REVIEW_TIER.