Skip to content

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

Open

Description

@baozhoutao

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), plus bar_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-level readOnly is 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.

  1. Page element binding. dataSource: { provider: 'api', read: { url: '/api/gantt/tree' }, path: 'data' } on an object-gantt component parses, and the console fetches it. But the value is only exposed as data to expression holes and visibleWhen. ElementDataSourceGate maps 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.
  2. properties: { staticData: '${data}' } to bridge the binding into the records. os validate and os dev refuse it at compile time: component-props-invalid … staticData: Invalid input: expected array, received string. The stack does not start.
  3. A kind: 'react' page that fetches /api/gantt/tree and 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 lockField all work.
    • Writes: a drag moves the bar and even raises the scheduling-conflict dialog, but no request is sent. staticData becomes an in-memory ValueDataSource, so a reload reverts the change.
    • The page scope offers no way to hand the gantt another DataSource, because ObjectGanttRenderer takes the adapter from useSchemaContext() only. It also offers no onBeforeTaskUpdate.
  4. 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.url must be same-origin and open with a single /, with the same five refusals.
  • No headers, credentials, auth, token or apiKey keys. The console sends its own signed-in fetch.
  • read.method: GET only.

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 code

A 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 from path, writes to ${read.url}/${id} as above.
  • (c) A supported write hook that a kind: 'react' page can pass to object-gantt, such as onBeforeTaskUpdate or a host-supplied DataSource, so the page can send the write itself.

Related

Activity

  1. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    Contributor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Triage: p1 · domain:spec · area:records · goes to the maintainer · 2026-10-28T07:57Z

    ⛔ Not a claim.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    Contributor

    误发,已撤回;以分诊评论 6463778255 为准。


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    Contributor

    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 api data 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 on origin/main e6e41998; the rest is quoted as the customer's report.

    Seat increment (re-read 2026-10-28T10:50Z):

    Prior rulings read: provider: 'api' · same-origin · ApiDataSource → #23082 C (6134088274), #23016 (page element rule), ADR-0049, ADR-0062, migration 18.view-data-api-provider-retired; thread: 6463778255 (triage, p1, to the maintainer), 6464599615 (withdrawn).

  4. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    Contributor

    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 ruling

    The maintainer asked (summon chat): 「要帮我多角度综合评估,也要考虑其他元数据的方案 / 其他组件的方案」. Read on origin/main e6e41998.

    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):

    • Drivers are pluggable from a server plugin: 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) is find / findOne / create / update / delete / count …; introspectSchema? and registerExternalObject? are optional — a driver "may not support introspection" (engine.ts:21171).
    • An object can be bound to a federated datasource (ADR-0015 external binding, object.zod.ts:1296): DDL forbidden, writes by double opt-in (datasource.external.allowWrites + external.writable).
    • Caller-dependent columns: formula fields evaluate in the engine with current_user / user in scope (packages/formula/src/stdlib.ts:776–781) and current_user.can(object, verb) (engine.ts:2737, 6133).
    • Server-side write gates: object hooks beforeUpdate / beforeInsert (packages/spec/src/data/hook.test.ts:26, 106).
    • The gantt binds an object with a tree: 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).
    • The page element's api binding (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); ApiDataSource writes to ${read.url}/${id} with methods fixed in code (ApiDataSource.ts:281–310).

    Routes (six), scored on eight angles

    Route What the customer writes (1) computed rows (2) per-caller columns (3) gated write-back (4) no re-impl Protocol change Works for other object-* blocks (table, kanban, calendar, map, tree, reports) AI-authoring / security
    A restore view-level { provider: 'api', read } (write to ${read.url}/${id}) the old view ✅ endpoint ✅ endpoint ✅ endpoint, platform sees nothing ✅ re-opens a retired arm + reverses migration 18.view-data-api-provider-retired ✅ every view type, but every one re-gains the second door ruling C closed the endpoint is outside every platform rule (permissions, hooks, audit); #23016's refusals keep credentials out
    b page element api binding becomes the gantt's record source a page + endpoint ✅ ✅ ✅ (fixed methods) ✅ describe-level (semantic) ❌ per-block gate mapping, one PR per block same as A on the server side; the console's own fetch
    c host write hook for a kind: 'react' page a react page + endpoint + glue ✅ ✅ ✅ in page code ◐ page glue ui API growth ❌ per component prop write logic leaves metadata entirely
    d virtual object: a server plugin registers a driver (find computes the tree, update applies the edit), the stack declares the datasource (schemaMode external) and an external-bound plan_node object; the gantt binds provider: 'object' with parentField + lockField a driver (≈ their endpoint handler, re-shaped) + one object + one view ✅ driver ✅ formula fields on the object (current_user), or the driver ✅ generic data API → permissions → beforeUpdate hooks → driver.update; refusals are the platform's ✅ none measured (spike needed, below) ✅ automatically — an object is what every block, report, filter and permission already understands best: rows, writes, locks are metadata the platform judges; audit and permissions apply
    e real objects + hooks: materialize the tree into stored rows (project / product / plan / work_order, or one plan_node), hooks keep it current; locked as a formula; objectField for a mixed-object tree a data-model rework ✅ stored ✅ formula ✅ hooks ✅ none ✅ best, but the largest customer change (synthetic rows become real rows)
    f react page + own gantt block a gantt — — — ❌ none n/a the customer's "only remaining route" today

    Mainstream 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

    • Architecture / one door (①): ruling C's reason was "one operation, two doors; the governed door wins". The governed door for RECORDS is the object: permissions, hooks, audit, formulas, reports all hang on it. d puts the customer's endpoint behind that door; A and b keep records that no platform rule can see (the server-side gates are the customer's code, invisible to permissions and audit); c moves them into page code.
    • Customer effort / time to unblock: A ≈ 0 (the old view returns); b ≈ small (a page + path); d ≈ days — re-shape the existing tree computation into find, the existing PATCH handler into update, declare one datasource, one object (fields + a locked formula), one view; e ≈ weeks (data model). The customer is mid-upgrade, so the ruling should also say what they do THIS week (below).
    • Platform effort: A spec+ui M (and a retirement reversal); b ui M + spec S; c ui M; d 0 protocol — but no in-tree precedent of an external-bound object on a non-SQL plugin driver, so one spike (S) proves boot without introspection and a gantt drag reaching driver.update; e 0.
    • Security boundary: A/b/c leave writes to the customer's endpoint with the console's cookie; d/e run writes through the platform's permission check and hooks, with audit; the same-origin rule becomes irrelevant (no browser-side URL at all).
    • Generality across components: d/e serve every block today and every future one; b needs a mapping per block; A re-adds the arm to every list-view type.
    • AI authoring (③): d/e are plain metadata (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.
    • Reversibility / debt (④): A reverses a retirement (the worst shape under 「过渡从紧」); b adds a per-block mapping that must be maintained; d/e add nothing to the contract.
    • Other-component composition: the gantt's objectField + parentField already 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-tree gives 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.

    1. Spike (S, 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 without introspectSchema, reads a tree through the generic data API with a locked formula over current_user, and sends an update through a beforeUpdate hook to driver.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; writable double opt-in wording).
    2. Docs (S, 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.
    3. This week for the customer: the spike's recipe is the answer to their report; until it lands they can keep 17.3.0's view on 17.3.0 (no data change) — nothing in d requires an 18.x protocol release, so they can migrate as soon as the recipe is published.
    • Not A (reverses a retirement to re-open the ungoverned door); not b as the primary (per-block mapping, records outside permissions / hooks / audit) — b stays the fallback if the spike finds that d needs a protocol change larger than S; not c / f.

    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 update semantics (PATCH of a plan from a work-order row) map onto driver.update(object, id, data) one-to-one — if not, a hook or the driver translates, still inside the platform.

  5. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    Contributor

    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 claim

    The maintainer asked (summon chat): 「按照你的建议设计完整的方案,和我确认。帮我评估如果开发工作量多大」. Read on origin/main e6e41998; 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 api source (ruling C), a per-component record-source mapping (route b, kept as the fallback), any marketplace / cloud piece.

    2. Architecture

    console: object-gantt on a view bound provider:'object'
       │  generic data API   GET /data/plan_node?…   PATCH /data/plan_node/:id
       ▼
    ObjectQL engine: object permissions → formula fields (current_user in scope) → hooks (beforeUpdate) → route by object.datasource
       ▼
    customer server plugin publishes service `driver.plan_gantt` (an IDataDriver):
       find() builds the four-level tree rows · update() applies the plan edit with the customer's role / status gates
       ▼
    customer's scheduling code and tables (unchanged)
    

    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); the IDataDriver contract (packages/spec/src/contracts/data-driver.ts:70): find / findOne / update / count are what this needs, introspectSchema? and registerExternalObject? are optional; a datasource declares schemaMode external with external.allowWrites (datasource.zod.ts:314) and external.validation.checkOnBoot (datasource.zod.ts:316–323, honoured by runtime/src/external-validation-plugin.ts:290–305); an object's external binding carries remoteName, writable, columnMap, ignoreColumns (object.zod.ts:1296–1370); formulas evaluate with current_user and current_user.can(object, verb) (formula/src/stdlib.ts:776–781, engine.ts:2737); hooks take events: ['beforeUpdate'] with an inline or package function handler (hook.zod.ts:457–476); the gantt view keys parentField, 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's driver.plan_gantt service; no connection string. (Spike step a confirms how a datasource names a plugin-provided driver: by name match, or a type: 'driver' config the driver validates — datasource.zod.ts:502–525.)

    3.2 Object plan_node (datasource: 'plan_gantt', external: { remoteName: 'plan_tree', writable: true }):

    • identity and tree: id, name, node_type (select: project | product | plan | work_order), parent (lookup to plan_node — the gantt's parentField), sort_key;
    • schedule: start_date, end_date, progress, predecessors (the gantt's dependenciesField), status;
    • server-computed display columns: plan_locked (boolean the server sets from plan status), bar_color, border_color, alert_level, tooltip fields;
    • per-caller lock as a FORMULA: 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;
    • permissions: a profile grants read to all planners' roles and update to planners only; the formula and the hook read the same facts.

    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 on node_type / status. The same object can carry a table view and a report with no further work.

    3.5 Driver (driver.plan_gantt, TypeScript, implements IDataDriver): name = 'plan_gantt'; connect / disconnect / checkHealth trivial; find(object, query) runs the existing tree computation and honours the query's filters, orderBy, limit / offset and fields (the engine passes DriverQuery = QueryAST minus object, 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 / deleteMany throw a named "not supported on this virtual object" refusal; syncSchema / dropTable refuse (DDL is forbidden on an external object anyway); transactions are no-op tokens; introspectSchema omitted (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 → beforeUpdate hook → 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 as driver.spike_tree; a datasource with checkOnBoot: false; an external object bound to it with a parent lookup, a plan_locked boolean and a locked formula over current_user; a beforeUpdate hook. Steps and what each proves: (a) defineStack accepts a datasource whose driver is a plugin service; (b) boot without introspectSchema; (c) GET /data/spike_node reaches driver.find with the view's filter and sort; (d) the formula evaluates per caller on an external object; (e) PATCH reaches the hook, a refusal returns 4xx with the message, an allowed write reaches driver.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

    Item Lane Size Note
    Spike test (4) server S one subagent, about half a day
    Fix cards the spike may open server / spec / ui 0–3 × S–M likely candidates: a datasource must name a plugin driver without a connection config (validateDriverConfig); external binding insisting on remoteName / introspection; formulas over external objects; an in-memory DriverQuery helper exported for virtual drivers (so a customer does not re-implement the filter AST); gantt link creation writing predecessors through update
    Guide "a virtual object: rows from your server, shown in any view" + the spike test as the executable example devx S the reply to this customer
    Studio: external objects show their fields read-only (verify only) ui 0–S
    Total about 1 agent-day best case, 3–5 agent-days if two or three fix cards open; calendar about one week

    Customer (no gantt code): driver 2–3 days (re-shape the existing tree computation into find, the PATCH handler into update, 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

    • DriverQuery semantics: 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.
    • The caller's context not reaching 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.
    • Tree computed per request → the driver caches per plan version; the engine's count and pagination are honoured by the driver.
    • Link creation (dependenciesField) writes a list → driver.update accepts predecessors; step (f) covers a link drag.
    • A formula on an external object might be refused by a build rule → step (d); if refused, that is a small spec card.

    7. What the maintainer confirms

    1. Direction d as the ruled route for 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 (b stays the fallback if the spike opens a change larger than S).
    2. The spike card (server, p1, S) is filed and dispatched first; the guide card (devx, p1, S) follows it.
    3. The reply to the customer carries the recipe once the spike passes; until then nothing is promised beyond "no protocol release needed".
  6. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    Contributor

    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 (find builds the tree, update applies the edit with the customer's gates); the stack declares the datasource (schemaMode external, allowWrites: true, validation.checkOnBoot: false) and an external-bound plan_node object with a current_user formula for the per-caller lock and a beforeUpdate hook; the gantt binds provider: 'object' with parentField / lockField / dependenciesField. Writes go PATCH → permissions → hook → driver.update. No protocol change; no URL in metadata; the view-level api source 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:blocked in this act, Blocked-by: #27276 and Blocked-by: #27278 in the body; domain:spec p1 stays as triaged (the lane re-reads on unblock: the deliverables are server + devx). Ruling-ref: names this comment.

  7. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    Contributor

    epic:#27261#1 · marchtian · session_017ihL2c5z3q4h8inrn5kwqV
    Unlock · 2026-10-29T04:37Z

    Both Blocked-by cards are closed: #27276 (spike, PR #27330 13f53603) and #27278 (guide, PR #27349 01516dc0). 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:

    The customer reply is the triage seat's, per #27278's 维护者速读.

  8. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    Contributor

    domain:spec#3 · os-support-ai · session_01WGufzgbJhFoXYsLSe5WSPP

    Claim: PM loop round 3 · 2026-10-29T05:27Z
    Session: session_01WGufzgbJhFoXYsLSe5WSPP
    Account: os-support-ai
    Branch: claude/project-thread-j0ssio (card thread; the platform assigns the thread's branch and it can push only that one, written as measured instead of claude/issue-27130-<slug>)
    Worktree: objectstack-27130
    Domain: domain:spec
    Seat: domain:spec#3
    Ruling-ref: 6471507187
    File surface: the object-gantt registration's data and filter input descriptions (ui/packages/plugin-gantt/src/index.tsx), any other ui/packages/*/src registration, describe or published README that still teaches the view-level api source 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: high
    Clause-②: no
    Metadata-protocol: none
    Responsibility: the one unclaimed p1 domain:spec card, 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:185 says "{ provider: 'api', read } reads through that request" and :187 names "an api request" as a record source, while resolveDataSource has no api arm; the dev re-measures first
    Serial constraints cleared: #27332 and #27277 (in flight on the virtual-object surface) edit no file in this surface; the dev re-checks before editing

  9. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    Contributor

    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

  10. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    Contributor

    domain:spec#3 · os-support-ai · session_01WGufzgbJhFoXYsLSe5WSPP

    ACCEPT → PR #27421 comment 6489657920

  11. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    Contributor

    domain:spec#3 · os-support-ai · session_01WGufzgbJhFoXYsLSe5WSPP

    Landed · domain:spec seat 3 · os-support-ai · 2026-10-29T06:46Z

    reading value
    PR #27421 seat ACCEPT (6489657920), merged through the merge queue at 2026-10-29T06:45Z, head 739a33fb7, merge commit bbb49c9fe3
    origin/main contains the merge commit (git merge-base --is-ancestor)

    Delivered (Part of #27130):

    • The object-gantt registration no longer teaches the retired view-level { provider: 'api', read } source. Its data and filter inputs name only the value and object providers, 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:triage here.

  12. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    Contributor

    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-gantt text fix (PR #27421) are all on main now.

    The short answer. The view-level api source 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:

    What you have today Where it goes
    GET /api/gantt/tree, building a four-level tree with synthetic project and product rows The driver's find builds the same rows, synthetic group rows included. The gantt's parentField draws the tree.
    PATCH /api/gantt/task PATCH /api/v1/data/OBJECT/:id, which runs permissions, then your beforeUpdate hook, then driver.update. Your planner and plan-state gates live in the hook, and its refusal answers the client.
    The per-caller locked column A formula field using current_user (the guide's example: record.plan_locked || record.planner != current_user.id), read by lockField
    predecessors dependenciesField
    bar_color, border_color and the tooltip fields Ordinary fields the driver returns

    Three things to get right the first time:

    1. Sharing. Remote rows carry no owner_id or created_by. Use sharingModel: 'public_read_write' and decide who may edit in beforeUpdate, 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 read owner_id / created_by, which no remote row carries — os validate says nothing (epic #27261, found by the P0 spike) #27332 adds a validate warning for it).
    2. The datasource: schemaMode external, allowWrites: true, validation.checkOnBoot: false, plus external.writable on the object. Keep both as narrow as your API.
    3. Your driver is called with the object name, not the remote name. It receives the engine's where, orderBy and limit. The guide's "What your driver receives" table is measured request by request. Read it before you write find.

    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 validate warning 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:specpriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions