Repository navigation
object-gantt can no longer write back to a same-origin endpoint after #23082 — a measured producer of the retired view-level api source (read + write) #27130
Description
Ruled: 6471507187 · letter d · 2026-10-28T14:21Z
Blocked-by: #27276
Blocked-by: #27278
Edition: ObjectOS Enterprise (licensed, self-managed)
Version: 18.2.0 (upgrading from 17.3.0)
Background
#23082 retired the view-level api data source (ruling C, merged in #23160, objectui#12325). The ruling rested partly on a measurement: zero producers of provider: 'api' in objectstack, HotCRM and objectui, and no producer of the write leg at all. Cloud and customer deployments were not measured.
We are a producer of both legs, in production on 17.3.0. A production-scheduling gantt (a list view of type: 'gantt') was bound like this:
data: {
provider: 'api',
read: { url: '/api/gantt/tree', method: 'GET' },
write: { url: '/api/gantt/task', method: 'PATCH' },
},The use already met the rule #23016 set for the page element: both URLs are same-origin relative paths, and the view carries no headers and no credential. The console sent its own signed-in fetch.
Why an endpoint and not an object: a server plugin computes the rows when they are read.
- A four-level tree (project → product → plan → work order). Project and product are synthetic group rows, so no single object holds the tree.
- Columns that depend on the caller:
locked(only planners may edit; in-progress or archived plans are locked), plusbar_color,border_color(alert level),predecessors, and the tooltip fields.lockField: 'locked'is the only way we found to drive the gantt's per-row 🚫 "no edit permission" affordance per user. A view-levelreadOnlyis static and cannot vary by user. - Writes go through the same endpoint. It re-checks the role and status gates server-side (403) before it updates the plan. Drag, resize and link all write back in real time, with the gantt's optimistic update and rollback on failure.
plugin-gantt documented this shape. Its registered data input still says, on main: "{ provider: 'api', read } reads through that request" (ui/packages/plugin-gantt/src/index.tsx, the object-gantt registration).
What happens on 18.2.0 (measured on a local instance)
We tried each replacement. None of them keeps drag write-back to the endpoint.
- Page element binding.
dataSource: { provider: 'api', read: { url: '/api/gantt/tree' }, path: 'data' }on anobject-ganttcomponent parses, and the console fetches it. But the value is only exposed asdatato expression holes andvisibleWhen.ElementDataSourceGatemaps only the object binding for the gantt (OBJECT_GANTT_DATA_SOURCE = { filter, sort }), so the rows never become gantt records. The binding is also GET-only. properties: { staticData: '${data}' }to bridge the binding into the records.os validateandos devrefuse it at compile time:component-props-invalid … staticData: Invalid input: expected array, received string. The stack does not start.- A
kind: 'react'page that fetches/api/gantt/treeand renders<Block type="object-gantt" staticData={rows} gantt={…} />.- Reads: everything renders. The four-level tree, dependency links, shift bands, colors, quick filters, tooltips and the per-row 🚫 from
lockFieldall work. - Writes: a drag moves the bar and even raises the scheduling-conflict dialog, but no request is sent.
staticDatabecomes an in-memoryValueDataSource, so a reload reverts the change. - The page scope offers no way to hand the gantt another
DataSource, becauseObjectGanttRenderertakes the adapter fromuseSchemaContext()only. It also offers noonBeforeTaskUpdate.
- Reads: everything renders. The four-level tree, dependency links, shift bands, colors, quick filters, tooltips and the per-row 🚫 from
- Bind the view to the plan object (
provider: 'object'). Drag writes through the generic data API. The tree, the synthetic group rows, the work-order child rows and the per-user computed columns are lost.
So on 18 there is no supported way to build a gantt whose rows come from a server endpoint and whose edits write back to it. The only remaining route is to re-implement the gantt from scratch in a react page.
Request
Restore an API record source for object-gantt (the view data arm, or the page component; whichever fits the "one door" principle better), under the same rule #23016 gives the page element:
read.urlmust be same-origin and open with a single/, with the same five refusals.- No
headers,credentials,auth,tokenorapiKeykeys. The console sends its own signed-in fetch. read.method:GETonly.
No new write block is needed. ApiDataSource already writes to the read URL when write is absent (ui/packages/core/src/adapters/ApiDataSource.ts, update / delete):
const config = this.writeConfig ?? this.readConfig;
// update → PATCH `${url}/${id}`, delete → DELETE `${url}/${id}`; the method is fixed in codeA deployment that serves PATCH/DELETE /api/gantt/tree/:id therefore gets write-back with only the read leg declared. That is option A of #23082, narrowed to read. It adds no credential surface and no cross-origin surface beyond what the page element binding already allows.
Either of these alternatives would also unblock us:
- (b) Let the page element's API binding act as
object-gantt's record source: rows taken frompath, writes to${read.url}/${id}as above. - (c) A supported write hook that a
kind: 'react'page can pass toobject-gantt, such asonBeforeTaskUpdateor a host-suppliedDataSource, so the page can send the write itself.
Related
- [Decision] Does a view's
apidata source adopt the page-element rule — a same-originurland noheaderswritten in metadata? #23082 (ruling C), feat(spec)!: retire the view-levelapidata source — a view binds its object or static rows; an API call is the page element's binding (#23082) #23160 (merge1912af47b), objectui#12325 - 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 (page element same-origin / no-credentials rule)
- ADR-0049 (enforce-or-remove)
- Migration entry
view-data-api-provider-retired(packages/spec/src/migrations/entries/semantic/18.view-data-api-provider-retired.ts)
Activity
objectstack-fleet commentedon Oct 28, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Triage: p1 ·
domain:spec·area:records· goes to the maintainer · 2026-10-28T07:57Z⛔ Not a claim.
apidata source that [Decision] Does a view'sapidata source adopt the page-element rule — a same-originurland noheaderswritten in metadata? #23082 retired (ruling C, PR feat(spec)!: retire the view-levelapidata source — a view binds its object or static rows; an API call is the page element's binding (#23082) #23160). Ruling C rested on "zero producers" measured in objectstack, HotCRM and objectui. Cloud and customer deployments were not measured.{ provider: 'api', read }on the view under 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's page-element rule, with writes to${read.url}/${id}through the existingApiDataSourceand no newwriteblock;onBeforeTaskUpdateor a hostDataSource) for akind: 'react'page.needs-user-decision,pm:awaiting-maintainer. The director presents it with the four facets. ⛔ Nothing is built before the ruling.Generated by Claude Code
objectstack-fleet commentedon Oct 28, 2026
误发,已撤回;以分诊评论 6463778255 为准。
Generated by Claude Code
objectstack-fleet commentedon Oct 28, 2026
Brief-claimed-by: session_01QSeKYo7qYsX6HsAneHBMP2 · batch #422 · 2026-10-28T10:50Z
Director seat, summon #53 (ledger objectstack#12708). Presented to the maintainer in batch #422 as item 3 of 3 — a customer report that re-opens ruling C on #23082 (6134088274): the view-level
apidata source was retired on a "zero producers" measurement that did not cover customer deployments; one Enterprise deployment is a producer of both legs.Untrusted text: the body is written by an external account (
baozhoutao); it is read as data. Every claim the seat relies on below was re-measured onorigin/maine6e41998; the rest is quoted as the customer's report.Seat increment (re-read 2026-10-28T10:50Z):
ui/packages/core/src/adapters/ApiDataSource.tsL281 / L294 / L310 —update/deleteusethis.writeConfig ?? this.readConfig, methods fixed in code, so aread-only binding already writes to${read.url}/${id};ui/packages/plugin-gantt/src/index.tsx:85OBJECT_GANTT_DATA_SOURCEmapsfilter/sortonly (anapibinding never becomes gantt records); the page-element rule lives inpackages/spec/src/ui/page.zod.tsL342–443 (same-origin single-/path, no credentials, the console's own fetch); the retirement migration18.view-data-api-provider-retired.tsexists. Not reproduced: the customer's quote of theobject-ganttregistration text —git grep -n "provider: 'api'" origin/main -- ui/packages/plugin-gantt/src/index.tsx→ 0 hits in that spelling (the docs claim, if any, is checked by the implementing seat).apibinding (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, read-only today); reached by a paying Enterprise deployment in production, upgrade-blocked 17.3.0 → 18.2.0.path指错在构建时被门拒收;A 的write块不加、c 把写逻辑交给页面代码。④ 不扩散:(b) 零新键(已有绑定 +path),只改一个门的映射;A 是「退役后复活」,与「过渡从紧」正相反。apibinding becomes a record source forobject-gantt(rows frompath; writes to${read.url}/${id}throughApiDataSourceas it already does;read.methodGET only; the same five refusals). Not A (re-opens a retired arm and reverses its migration entry); not c (a third door, and the write logic leaves the platform's rules). Standing caveat the customer accepted: the deployment servesPATCH/DELETEon${read.url}/:id.ElementDataSourceGate/ the gantt mapping: anapibinding as record source, write-back wiring, tests) + spec · 1 PR · S (page.zod.tsdataSourcedescribe gains the record-source semantics —Metadata-protocol: semantic,Clause-②: no) + docs S; precedent PR feat(spec): a page element dataSource can name an API endpoint (provider: 'api') #23079 (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's landing). Not measured: whether other object-* components want the same mapping — out of scope unless the maintainer says otherwise.Prior rulings read:
provider: 'api'· same-origin · ApiDataSource → #23082 C (6134088274), #23016 (page element rule), ADR-0049, ADR-0062, migration18.view-data-api-provider-retired; thread: 6463778255 (triage, p1, to the maintainer), 6464599615 (withdrawn).objectstack-fleet commentedon Oct 28, 2026
Seat increment 2 · multi-angle evaluation, metadata-native and other-component routes included · director seat (summon #53,
session_01QSeKYo7qYsX6HsAneHBMP2) · 2026-10-28T11:20Z · ⛔ not a rulingThe maintainer asked (summon chat): 「要帮我多角度综合评估,也要考虑其他元数据的方案 / 其他组件的方案」. Read on
origin/maine6e41998.The customer's need, decomposed (four parts, each must hold): (1) rows computed server-side — a four-level tree with synthetic project / product group rows; (2) columns that depend on the caller —
locked(role + status), colours, tooltips; (3) edits written back through server-side gates with optimistic UI; (4) no re-implementation of the gantt.What the platform already has for each part (measured):
ql.registerDriver(driver)(packages/objectql/src/plugin.ts:1158,packages/runtime/src/app-plugin.ts:1661);IDataDriver(packages/spec/src/contracts/data-driver.ts:70) isfind / findOne / create / update / delete / count …;introspectSchema?andregisterExternalObject?are optional — a driver "may not support introspection" (engine.ts:21171).object.zod.ts:1296): DDL forbidden, writes by double opt-in (datasource.external.allowWrites+external.writable).current_user/userin scope (packages/formula/src/stdlib.ts:776–781) andcurrent_user.can(object, verb)(engine.ts:2737, 6133).beforeUpdate/beforeInsert(packages/spec/src/data/hook.test.ts:26, 106).parentField,objectField("for mixed-object trees — a row set composing parent-object rows with child-object rows"),lockField(row view-only when truthy),summaryExtent,defaultCollapsedDepth(view.zod.ts:2262–2282).apibinding (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 rule) is GET-only and feeds expression holes, not records (OBJECT_GANTT_DATA_SOURCE = { filter, sort },plugin-gantt/src/index.tsx:85);ApiDataSourcewrites to${read.url}/${id}with methods fixed in code (ApiDataSource.ts:281–310).Routes (six), scored on eight angles
{ provider: 'api', read }(write to${read.url}/${id})18.view-data-api-provider-retiredapibinding becomes the gantt's record sourcesemantic)kind: 'react'pagefindcomputes the tree,updateapplies the edit), the stack declares the datasource (schemaModeexternal) and an external-boundplan_nodeobject; the gantt bindsprovider: 'object'withparentField+lockFieldcurrent_user), or the driverbeforeUpdatehooks →driver.update; refusals are the platform'splan_node), hooks keep it current;lockedas a formula;objectFieldfor a mixed-object treeMainstream platforms' answer to "rows from an endpoint, shown in a standard component": Dynamics 365 virtual tables with a custom data provider (retrieve / update plug-ins) = d; Salesforce Connect external objects (OData / Apex connector framework) = d; ServiceNow remote tables (scripted
find) = d; Odoo a model overriding_search/write= d / e. Lightning / PCF components with their own controller = c / f. None of them re-opens an "API source on the view" once a virtual-object door exists.Angles, business view
path); d ≈ days — re-shape the existing tree computation intofind, the existing PATCH handler intoupdate, declare one datasource, one object (fields + alockedformula), one view; e ≈ weeks (data model). The customer is mid-upgrade, so the ruling should also say what they do THIS week (below).driver.update; e 0.datasource,object,view) an AI already knows how to write; a wrong pointer refuses at build. A/b put a URL in metadata that the build cannot check beyond its shape.objectField+parentFieldalready model a mixed-object tree (parent rows from one object, children from another), so route e can keep project / product / plan / work order as separate objects;object-treegives the same hierarchy without the timeline. Neither replaces a computed row set with per-caller columns on its own — that is what the formula field and the driver give.Revised letter: d, with a spike first.
domain:server, this week): a test in-tree that registers a memory-backed custom driver under a federated datasource, binds an external object to it, boots withoutintrospectSchema, reads a tree through the generic data API with alockedformula overcurrent_user, and sends anupdatethrough abeforeUpdatehook todriver.update. If every step passes with zero code change, d needs only docs; each step that fails becomes a named, small platform card (expected candidates: boot-time binding validation insisting on introspection;writabledouble opt-in wording).domain:devx): "a virtual object: rows from your server, shown in any view" — the migration recipe for exactly this customer, written from the spike's test.Facets (席位四棱): ① 长远:d 把客户的接口放到对象这扇门后面——权限、hook、审计、公式、报表全在;A 复活退役面,b 每个组件各修一张门。② 拉动:付费客户生产受阻,d 的迁移是「把现成的树计算包成 driver」,按天计,不是重写甘特图。③ 防错:d 的写回经平台权限与 hook 拒收,可审计;A/b 的写回平台看不见。④ 不扩散:d 零契约新增,只需一个 spike 证明既有路径贯通;A/b 各加永久义务。只看①选 d;②③④ 是否翻转:否(②只把 spike 排到本周)。
Workload: spike S + docs S now; fallback b = ui M + spec S. Confidence gaps: the external-bound-object-on-plugin-driver path is unexercised in tree (the spike exists to close it); whether the customer's
updatesemantics (PATCH of a plan from a work-order row) map ontodriver.update(object, id, data)one-to-one — if not, a hook or the driver translates, still inside the platform.objectstack-fleet commentedon Oct 28, 2026
Design proposal for route d (virtual object on a plugin driver) · for the maintainer's confirmation · director seat (summon #53,
session_01QSeKYo7qYsX6HsAneHBMP2) · 2026-10-28T11:38Z · ⛔ not a ruling, not a claimThe maintainer asked (summon chat): 「按照你的建议设计完整的方案,和我确认。帮我评估如果开发工作量多大」. Read on
origin/maine6e41998; every platform fact below carries its file.1. Goal and boundary
Rows that a server computes enter the platform AS AN OBJECT, so every view (gantt, table, kanban, calendar, map, tree), report, filter, permission, hook and audit works on them unchanged. No protocol change; no browser-side URL in metadata; writes go through the platform's permission check and hooks, never straight to a customer endpoint. Out of scope: re-opening the view-level
apisource (ruling C), a per-component record-source mapping (route b, kept as the fallback), any marketplace / cloud piece.2. Architecture
Platform pieces this rides on (measured): plugins publish drivers as
driver.<name>services and the runtime registers them (packages/objectql/src/plugin.ts:1145–1158,packages/runtime/src/app-plugin.ts:1661); the engine routes an object to its datasource's driver (engine.ts:21201 getDriverForObject); theIDataDrivercontract (packages/spec/src/contracts/data-driver.ts:70):find / findOne / update / countare what this needs,introspectSchema?andregisterExternalObject?are optional; a datasource declaresschemaModeexternal withexternal.allowWrites(datasource.zod.ts:314) andexternal.validation.checkOnBoot(datasource.zod.ts:316–323, honoured byruntime/src/external-validation-plugin.ts:290–305); an object's external binding carriesremoteName,writable,columnMap,ignoreColumns(object.zod.ts:1296–1370); formulas evaluate withcurrent_userandcurrent_user.can(object, verb)(formula/src/stdlib.ts:776–781,engine.ts:2737); hooks takeevents: ['beforeUpdate']with an inline or package function handler (hook.zod.ts:457–476); the gantt view keysparentField,objectField,lockField,startDateField,endDateField,progressField,dependenciesField,borderColorField,summaryExtent(view.zod.ts:1566–1567, 2223–2224, 2262–2282).3. What the customer writes (metadata + one driver)
3.1 Datasource (stack):
{ name: 'plan_gantt', schemaMode: 'external', external: { allowWrites: true, validation: { checkOnBoot: false } } }— the driver is the plugin'sdriver.plan_ganttservice; no connection string. (Spike step a confirms how a datasource names a plugin-provided driver: by name match, or atype: 'driver'config the driver validates —datasource.zod.ts:502–525.)3.2 Object
plan_node(datasource: 'plan_gantt',external: { remoteName: 'plan_tree', writable: true }):id,name,node_type(select: project | product | plan | work_order),parent(lookup toplan_node— the gantt'sparentField),sort_key;start_date,end_date,progress,predecessors(the gantt'sdependenciesField),status;plan_locked(boolean the server sets from plan status),bar_color,border_color,alert_level, tooltip fields;locked = plan_locked || !current_user.can('plan_node', 'update') || status in ['in_progress', 'archived']— evaluated in the engine for the calling user, so the driver never needs to know who is asking;3.3 Hook on
plan_node,events: ['beforeUpdate'], handler in the customer package: refuse when the row is locked for this caller or the plan status forbids the change (the former endpoint's 403, now the platform's refusal envelope); otherwise pass through.3.4 Gantt view on
plan_node:provider: 'object',parentField: 'parent',startDateField: 'start_date',endDateField: 'end_date',progressField: 'progress',dependenciesField: 'predecessors',lockField: 'locked',borderColorField: 'border_color',summaryExtent: 'children',defaultCollapsedDepth: 1, quick filters onnode_type/status. The same object can carry a table view and a report with no further work.3.5 Driver (
driver.plan_gantt, TypeScript, implementsIDataDriver):name = 'plan_gantt';connect / disconnect / checkHealthtrivial;find(object, query)runs the existing tree computation and honours the query's filters,orderBy,limit/offsetandfields(the engine passesDriverQuery = QueryASTminusobject,data-driver.ts:54);findOne;count;update(object, id, data)applies a bar move / resize / link / progress change through the existing scheduling code and returns the row;create / delete / bulk* / updateMany / deleteManythrow a named "not supported on this virtual object" refusal;syncSchema / dropTablerefuse (DDL is forbidden on an external object anyway); transactions are no-op tokens;introspectSchemaomitted (validation is switched off for this datasource).3.6 Write path. Drag → the console's adapter
update→PATCH /data/plan_node/:id { start_date, end_date }→ permission check →beforeUpdatehook →driver.update→ the row comes back → the gantt's existing optimistic update / rollback. A refusal anywhere returns the platform's error envelope and the bar snaps back — the behaviour the customer had, now inside the platform's rules and audit.4. Spike (the first card, before anything else)
One in-tree test,
domain:server, S: a memory-backed custom driver published asdriver.spike_tree; a datasource withcheckOnBoot: false; an external object bound to it with aparentlookup, aplan_lockedboolean and alockedformula overcurrent_user; abeforeUpdatehook. Steps and what each proves: (a)defineStackaccepts a datasource whose driver is a plugin service; (b) boot withoutintrospectSchema; (c)GET /data/spike_nodereachesdriver.findwith the view's filter and sort; (d) the formula evaluates per caller on an external object; (e)PATCHreaches the hook, a refusal returns 4xx with the message, an allowed write reachesdriver.update; (f) a gantt view on the object renders the tree and a drag issues the PATCH (ui test or the live-e2e lane); (g) the object's permissions apply. Every step that fails becomes one named platform card; every step that passes needs only documentation.5. Workload
Platform
validateDriverConfig); external binding insisting onremoteName/ introspection; formulas over external objects; an in-memoryDriverQueryhelper exported for virtual drivers (so a customer does not re-implement the filter AST); gantt link creation writingpredecessorsthroughupdateCustomer (no gantt code): driver 2–3 days (re-shape the existing tree computation into
find, the PATCH handler intoupdate, a filter helper or the platform's); metadata 0.5–1 day (datasource, object, formula, view, profile); hook 0.5 day; test and cut-over 1–2 days — about one week, overlapping with the platform's spike. They do not need an 18.x protocol release: nothing in this design is new protocol.6. Risks and how each is caught
DriverQuerysemantics: a driver that ignores filters returns wrong rows silently → the spike's step (c) asserts the filter reaches the driver and the guide says which filters a virtual driver must honour; a platform helper is the remedy if the AST is heavy.driver.find→ not needed: the lock is a formula and visibility is the engine's permission layer; recorded as a non-goal unless the spike shows a case.countand pagination are honoured by the driver.dependenciesField) writes a list →driver.updateacceptspredecessors; step (f) covers a link drag.7. What the maintainer confirms
apisource (read + write) #27130 (b stays the fallback if the spike opens a change larger than S).objectstack-fleet commentedon Oct 28, 2026
Ruling: batch #422 item 3 · letter d (virtual object on a plugin driver), spike first · maintainer 「原则同意,按步骤推进。」 2026-10-28T14:20Z
Recorded by the director seat (summon #53,
session_01QSeKYo7qYsX6HsAneHBMP2, ledger objectstack#12708). Brief 6467063265 (letter b), increment 2 (6467643796, letter revised to d), the design for confirmation (6468011868); the maintainer answered on the epic that generalizes it (#27261, 「立专题卡」) and then 「原则同意,按步骤推进。」.d — the customer's endpoint becomes an ontology object behind a plugin driver. A server plugin publishes
driver.plan_gantt(findbuilds the tree,updateapplies the edit with the customer's gates); the stack declares the datasource (schemaModeexternal,allowWrites: true,validation.checkOnBoot: false) and an external-boundplan_nodeobject with acurrent_userformula for the per-caller lock and abeforeUpdatehook; the gantt bindsprovider: 'object'withparentField/lockField/dependenciesField. Writes goPATCH→ permissions → hook →driver.update. No protocol change; no URL in metadata; the view-levelapisource stays retired (ruling C on #23082).Workload: platform — spike S + guide S (+ 0–3 fix cards the spike may open); customer — about one week (driver 2–3 days, metadata 0.5–1, hook 0.5, test and cut-over 1–2), no gantt code.
Execution:
needs-user-decision→pm:blockedin this act,Blocked-by: #27276andBlocked-by: #27278in the body;domain:specp1 stays as triaged (the lane re-reads on unblock: the deliverables are server + devx).Ruling-ref:names this comment.objectstack-fleet commentedon Oct 29, 2026
epic:#27261#1 · marchtian · session_017ihL2c5z3q4h8inrn5kwqV
Unlock · 2026-10-29T04:37Z
Both
Blocked-bycards are closed: #27276 (spike, PR #2733013f53603) and #27278 (guide, PR #2734901516dc0).pm:blocked→pm:queue.The recipe this card asked for is live: Virtual Objects (Plugin Driver). The executable reference is
packages/runtime/src/plugin-driver-external-object.spike.test.ts.Two cautions for the reply:
owner_id/created_by, which no remote row carries —os validatesays nothing (epic #27261, found by the P0 spike) #27332, in flight). DeclaresharingModel: 'public_read_write'and decide inbeforeUpdate.applyDriverQueryInMemoryhelper, per-datasource row cap with a loud refusal,supportshonoured for plugin drivers, refusal-envelope mapping for driver failures (epic #27261 P1) #27277, in flight. Until it lands, the driver must apply every filter itself.The customer reply is the triage seat's, per #27278's 维护者速读.
objectstack-fleet commentedon Oct 29, 2026
domain:spec#3 · os-support-ai · session_01WGufzgbJhFoXYsLSe5WSPP
Claim: PM loop round 3 · 2026-10-29T05:27Z
Session:
session_01WGufzgbJhFoXYsLSe5WSPPAccount:
os-support-aiBranch:
claude/project-thread-j0ssio(card thread; the platform assigns the thread's branch and it can push only that one, written as measured instead ofclaude/issue-27130-<slug>)Worktree:
objectstack-27130Domain:
domain:specSeat:
domain:spec#3Ruling-ref: 6471507187
File surface: the
object-ganttregistration'sdataandfilterinput descriptions (ui/packages/plugin-gantt/src/index.tsx), any otherui/packages/*/srcregistration, describe or published README that still teaches the view-levelapisource as a record source, their regenerated SDUI manifest and reference docs, a pin test, one root changeset per touched published package (stop on breach; explain in the report)Container & model:
S,mode:thread,model: TIER_DEFAULT — --tier: no path-derived mandate (published text only; the accept set does not move),effort: highClause-②: no
Metadata-protocol: none
Responsibility: the one unclaimed p1
domain:speccard, unblocked at 2026-10-29T04:37Z; seat 1 told through the coordinator. ⛔ The customer reply stays the triage seat's, per the ruling.Thread-read: 6488082693
Premise: measured by the seat on
origin/main:plugin-gantt/src/index.tsx:185says "{ provider: 'api', read }reads through that request" and:187names "anapirequest" as a record source, whileresolveDataSourcehas noapiarm; the dev re-measures firstSerial constraints cleared: #27332 and #27277 (in flight on the virtual-object surface) edit no file in this surface; the dev re-checks before editing
objectstack-fleet commentedon Oct 29, 2026
os-dev-report
{ "issue": 27130, "status": "done", "branch": "claude/project-thread-j0ssio", "pr": "https://github.com/objectstack-ai/objectstack/pull/27421", "session": "session_01WGufzgPyKYGxK9udhSm8zP", "premise_still_valid": true, "summary": "Measured on origin/main cf218eed: plugin-gantt/src/index.tsx:185 said `{ provider: 'api', read }` reads through that request and :187 listed an `api` request as a record source; resolveDataSource has no api arm. A probe through SchemaRenderer with the customer's shape (no objectName): ObjectGanttSchema refuses data.provider with the spec retirement message, the host adapter's find is called with resource '', fetch is never called, no task is drawn. Both descriptions now name the value and object providers and say server-computed rows reach the gantt as an object behind a plugin driver. Sweep of ui/packages/*/src, ui/apps/*/src and ui READMEs found no other text teaching the view-level api source; no skills/** or docs/adr/** hit. sdui.manifest.json, its record and content/docs/ui/components/plugins.mdx regenerated with gen-sdui-manifest-node.mjs and gen-sdui-reference.mjs. Body says Part of #27130: nothing else is the lane's; the card stays open for triage's customer reply.", "tests": "head 65fcb5de. New ui/packages/plugin-gantt/src/__tests__/apiSourceNotTaught-27130.test.ts (3 tests: no input description names provider api; positive control data names provider object and value; matcher control). Reverse verification once: index.tsx from origin/main -> Tests 1 failed | 2 passed, failing row names [data, filter]; restored from HEAD, git diff HEAD empty, blob = disk 4d07512d. @object-ui/plugin-gantt test + type-check under os-verify-lock: Test Files 103 passed 1 skipped, Tests 775 passed 114 skipped, VERDICT command-exit 0. No ablation: text only.", "gates": "dispatch-gates --commands derives 76 on 65fcb5de; all 76 exit 0 (check:skill-examples first exit 3, prerequisite: no client-react dist; green after pnpm --filter @objectstack/client-react build). Includes check:sdui-reference, check:sdui-manifest and pnpm --dir ui exec vitest run scripts/. dispatch-gates --ran: 76 derived, 76 run, 0 NOT-MEASURED, 0 UNRUN. PR check-runs: in_progress at report time.", "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm", "mcp_calls": "0", "api_writes": "3 — fleet-write relay: pr_create (#27421, draft, assignee os-support-ai), pr_request_reviewers (#27421, os-support-ai), comment (#27130, this report); plus git push of claude/project-thread-j0ssio (not REST)", "open_questions": [], "out_of_scope_findings": [ "carrier: none · the object-gantt data description does not mention the { provider: 'schema' } arm the spec union accepts (the gantt queries objectName for it); plugin-gantt README already states it; noted in Acceptance notes, not filed" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 29, 2026
domain:spec#3 · os-support-ai · session_01WGufzgbJhFoXYsLSe5WSPP
ACCEPT → PR #27421 comment 6489657920
objectstack-fleet commentedon Oct 29, 2026
domain:spec#3 · os-support-ai · session_01WGufzgbJhFoXYsLSe5WSPP
Landed ·
domain:specseat 3 ·os-support-ai· 2026-10-29T06:46Z739a33fb7, merge commitbbb49c9fe3origin/maingit merge-base --is-ancestor)Delivered (
Part of #27130):object-ganttregistration no longer teaches the retired view-level{ provider: 'api', read }source. Itsdataandfilterinputs name only thevalueandobjectproviders, and say that rows a server computes reach the gantt as an object behind a plugin driver (ruling 6471507187, letter d). The SDUI manifest and the components reference are regenerated.What remains on this card is not the spec lane's: the customer reply is the triage seat's, per the ruling, now that the spike (#27276), the guide (#27278) and this text fix have landed. #27332 (sharing model) and #27277 (engine floor) are in flight on their own cards.
pm:dispatched→pm:triagehere.objectstack-fleet commentedon Oct 29, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: 看数据的视图 | records-forms.gantt-interactions | P②
Triage: the answer to this card · 2026-10-29T06:55Z
⛔ Not a claim. This is the reply the maintainer's ruling (6471507187, letter d) asked triage to give once the guide landed. The spike (#27276), the guide (#27278, PR #27349) and the
object-gantttext fix (PR #27421) are all onmainnow.The short answer. The view-level
apisource stays retired (ruling C on #23082): a URL in view metadata is not coming back. What replaces it is close to what you have today. Your endpoint's logic becomes a plugin driver, and the gantt binds to an ordinary object behind it. Your server still computes every row at read time. The gantt does not need its own code.The recipe is the new guide, Virtual objects: rows from your server, shown in any view (
content/docs/data-modeling/virtual-objects.mdx; it reaches the docs site with the next release). For your case:GET /api/gantt/tree, building a four-level tree with synthetic project and product rowsfindbuilds the same rows, synthetic group rows included. The gantt'sparentFielddraws the tree.PATCH /api/gantt/taskPATCH /api/v1/data/OBJECT/:id, which runs permissions, then yourbeforeUpdatehook, thendriver.update. Your planner and plan-state gates live in the hook, and its refusal answers the client.lockedcolumncurrent_user(the guide's example:record.plan_locked || record.planner != current_user.id), read bylockFieldpredecessorsdependenciesFieldbar_color,border_colorand the tooltip fieldsThree things to get right the first time:
owner_idorcreated_by. UsesharingModel: 'public_read_write'and decide who may edit inbeforeUpdate, as the guide does. Do not use an owner-based model on this object: an owner-based model refuses every member edit (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 adds avalidatewarning for it).schemaModeexternal,allowWrites: true,validation.checkOnBoot: false, plusexternal.writableon the object. Keep both as narrow as your API.where,orderByandlimit. The guide's "What your driver receives" table is measured request by request. Read it before you writefind.Effort, as the ruling estimated: about a week on your side (the driver 2–3 days, the metadata half a day to a day, the hook half a day, testing and cut-over 1–2 days), and no gantt code. Nothing here needs a protocol change.
Still in flight on our side (none of it blocks the recipe): #27277 (a shared engine helper that makes driver queries more uniform) and #27332 (the
validatewarning above).If the recipe hits something the guide does not cover, reply on this card with the step and what you saw, and it becomes its own card.
Generated by Claude Code