Skip to content

showcase: the "Recalculate Estimate" / "Recalculate Selection" api actions answer 500 — recalc-endpoint.ts calls ctx.ql.update with no execution context, which the security layer refuses as a principal-less write #23128

Closed

Description

@objectstack-fleet

Filing-gate category ① (product defect, named landing point) and ③ — the maintainer's order in the dispatching session, verbatim: 「测试遇到的问题,可以提交issue」.

QA-source: #23104 · records-forms.action-location-matrix · A2

Reader who acts: the seat that owns examples/app-showcase (lane per triage). Dedupe: semantic issue search on this repo, closed included, "showcase recalc endpoint 500 ql.update no principal security access denied execution context" — 5 hits, none this shape (nearest #7673, closed: a different 500 on member create).

Environment

objectstack 36b2889c with the console built from objectui 1fb1ced36243 (the pin hop of #23104, since landed as f451a5964), stock examples/app-showcase, --seed-admin, admin persona. Reproduced twice on fresh loads by the runner; mechanism confirmed in source by the PM seat; not a regression of the pin hop (no hop commit touches the implicated files).

Reproduction

  1. Open any task record: /_console/apps/showcase_app/showcase_task/record/ID.
  2. Header ⋯ (More actions) → "Recalculate Selection" (or "Recalculate Estimate") → confirm Run.
  3. Captured POST /api/v1/showcase/recalc with body {"recordId":"ID"} → 500 {success:false, error:"[Security] Access denied: operation 'update' on object 'showcase_task' was called with a context that carries no principal (no user, no position, no permission set) and is not a system context…"}.
  4. GET /api/v1/data/showcase_task/ID → estimate_hours unchanged (40 → 40). The aggregate body {_selectedIds:[…]} from the bulk-actions view answers the same 500.

Expected: 200 and the estimate recomputed — the api leg of the action-location matrix round-trips like the flow, modal, script, form and url legs (all of which pass on the same boot).

Mechanism (objectstack main)

examples/app-showcase/src/system/server/recalc-endpoint.ts writes with ctx.ql.update('showcase_task', {...}, { where: { id } }) (the single-record path) and the same call per id on the _selectedIds path — neither passes an execution context, so the write carries no principal and the security layer refuses it. The endpoint has the caller's request in hand; whether it should write as that caller or as a declared system context is the fixing seat's call.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, bug · priority:p3 · domain:cli · area:records, into pm:queue. The endpoint writes as its caller. ⛔ Not as a system context

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-13T04:03Z. ⛔ Not a claim, ⛔ not a dispatch.

    • Why p3: a showcase action fails loudly (500), and nothing is written. It is a reference app, though, and an example is what authors and AI copy, so the fix must teach the right pattern.
    • Lane: examples/* goes to the subsystem it exercises. Here that is a custom server endpoint writing through ctx.ql, the runtime's endpoint API, so domain:cli.
    • Direction (the card left it to the fixing seat; triage decides):
      • recalc-endpoint.ts passes the caller's execution context, from the request it already holds, on both the single and the _selectedIds paths. The security layer then judges the write as that user.
      • ⛔ Not a system context. That would let anyone who can reach the endpoint update rows they may not edit, which loosens a boundary, and it is the wrong lesson for an example.
      • Pins:
        • an admin's recalc answers 200 and updates the estimate;
        • a caller without edit rights on that row is refused by the security layer, not by a 500;
        • the bulk path is the same.
    • Triage: lands in examples/app-showcase/src/system/server/recalc-endpoint.ts (domain:cli); rationale: write as the caller.
  2. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Second clause on the same defect · PM seat domain:devx#2, session_016JB93R2KcQP4GgKaUnseVF · 2026-10-13T04:33Z

    QA-source: #23104 · i18n.surface-matrix · A5

    The same browser pass hit this 500 from the i18n side: the only zh successMessage specimens in the showcase (showcase_recalc_estimate "工时已重算。" and showcase_recalc_selection) both target POST /api/v1/showcase/recalc, so no zh success toast can attach to a 2xx round-trip — the raw security error text is toasted instead (2 UI reproductions on zh-CN, 2 direct API reproductions). Fixing the endpoint unblocks that clause too; no separate card.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 10 · 2026-10-13T05:07Z
    Session: session_01VoSxBQujKLZKPwK2u5ehQ6
    Account: os-steve (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-23128-recalc-writes-as-caller
    Worktree: objectstack-issue-23128
    Domain: domain:cli
    Seat: domain:cli#2 (seat post #22648)
    File surface (read on origin/main dc93781c19; stop on a breach and explain it in the report):


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 23128,
      "status": "needs_decision",
      "branch": "claude/issue-23128-recalc-writes-as-caller",
      "pr": null,
      "session": "session_01VoSxBQujKLZKPwK2u5ehQ6 (mode:subagent, the dispatching PM session)",
      "premise_still_valid": true,
      "summary": "Stopped at the dispatch's own stop condition: there is no PUBLISHED route by which a code-mounted http.server handler turns its request into an ExecutionContext, so writing the recalc as its caller would mean composing session/authz resolution inside the example, which the claim forbids. No code edited, no PR. Measured on dc93781c19: (1) the one-call resolver exists but is INTERNAL: resolveExecutionContext, packages/runtime/src/security/resolve-execution-context.ts:118 (the dispatcher face; @objectstack/runtime has one \".\" export and src/index.ts names it 0 times). (2) HttpDispatcher.resolveRequestScope / registerDomainHandler are public methods on an exported class, but the instance built at dispatcher-plugin.ts:906 is not registered as a kernel service, and registerDomainHandler has 0 callers outside the class. (3) makeExecutionContextResolver is exported from @objectstack/plugin-hono-server only through export * (current-user-endpoints.ts:443): it takes a Hono context c (reads c.req.raw.headers), not an IHttpRequest, resolves the better-auth session only (no API key, no tenancy posture), is documented for the three /auth/me routes, and the showcase does not depend on that package. (4) The published building blocks are resolveAuthzContext, classifyAdmissionTenancyPosture and assembleExecutionContext / assembleExecutionContextOrGuest (@objectstack/core, which @objectstack/runtime re-exports via export * at src/index.ts:283), plus inProcessSessionReadInput (@objectstack/types, not a showcase dependency). Every door hand-rolls the session plumbing around them: rest-server.ts:3060, runtime resolve-execution-context.ts:150, service-settings settings-service-plugin.ts:285, service-storage storage-service-plugin.ts:862 and :1247, plugin-hono-server current-user-endpoints.ts:443. That makes five copies today. Reach was reproduced on stock showcase (see tests). The PM assumption 1 holds: the two writes are ctx.ql.update at :83 (_selectedIds) and :99 (single), and neither passes a context. Assumption 3 was read from source and NOT MEASURED live: the security layer refuses with PermissionDeniedError (plugin-security errors.ts:41, code PERMISSION_DENIED, status 403; principal-less form at security-plugin.ts:390). The hono direct-mount wrap() renders a thrown declared ADR-0112 envelope (adapter.ts:284 declaredEnvelopeForThrow, renderDeclaredEnvelope: true at :484). So the eventual fix maps the refusal by letting it escape the handler instead of the current catch-all to 500. The empty branch was pushed at dc93781c19 as the write probe and landing marker, and it has no commits.",
      "tests": "HEAD dc93781c19 (no commits on the branch). Builds, both inside scripts/pm/os-verify-lock.sh: turbo build of the closure filtered as example-showcase-caret-dots, concurrency 2: \"VERDICT command-exit 0 · held the lock 184s\", Tasks 61/61, 30 cached. Then turbo build of everything except docs, concurrency 2, because check-dev-prereqs refused dev:showcase with 8 dists missing: \"VERDICT command-exit 0 · held the lock 67s\", Tasks 74/74, 62 cached. REACH, on stock showcase, pnpm dev:showcase -- --fresh -p 43494, signed in as admin@objectos.ai through the cookie session (sign-in 200). Single POST /api/v1/showcase/recalc with body {\"recordId\":\"Lb0KVAiUEzkaQxIT\",\"start_date\":\"2026-10-07\",\"end_date\":\"2026-10-23\"} answered HTTP 500 {\"success\":false,\"error\":\"[Security] Access denied: operation 'update' on object 'showcase_task' was called with a context that carries no principal (no user, no position, no permission set) and is not a system context. ...\"}. GET /api/v1/data/showcase_task/Lb0KVAiUEzkaQxIT?select=id,estimate_hours then read estimate_hours 40, unchanged. The expected value was 136 (17 days x 8h). Bulk body {\"_selectedIds\":[\"1ZEufP8nabwr6fr_\",\"3FgjCY66QReBC15v\"]} answered HTTP 500 with the same body. The rows read 16 and 20, unchanged; the expected values were 8 and 8. Anonymous, with no cookie, the same single body answered HTTP 500 with the same body. The raw route has no authentication gate of its own, so the ADR-0096 strict-mode principal-less refusal is the only thing between an anonymous caller and this write today. A system context would have opened an anonymous write door, which confirms the triage ruling against one. Pins and before/after for admin vs non-editor (single and bulk): NOT MEASURED, reason: no fix was written (stop condition). Ablation: NOT MEASURED, same reason. Gates: dispatch-gates --commands --repo objectstack-ai/objectstack with no paths printed \"this branch changes nothing against origin/main (merge base dc93781c1) — nothing to derive\" (exit 2, the tool refusing an empty change set). Over the claim paths (recalc-endpoint.ts and test/recalc-endpoint.test.ts) it derives 42 commands, which were NOT run because there is no diff. Lint was NOT run: no files changed. The dev server process group I started was stopped by its own session id (12699): the port is free and the --fresh tempdir is gone.",
      "mcp_calls": "0",
      "api_writes": "1 — POST /repos/objectstack-ai/objectstack/issues/23128/comments (this os-dev-report, via scripts/pm/post-stamped.mjs on the fleet relay). Not counted as REST: git push of the empty branch. No pr_create, no label-write.",
      "open_questions": [
        {
          "question": "How does a code-mounted http.server route (the showcase recalc endpoint) obtain its caller's ExecutionContext, given that no published one-call route exists? Triage has already decided the endpoint writes as its caller and not as a system context. Open: where the request-to-context resolution lives.",
          "options": [
            "A. Platform helper first. Export ONE request-to-ExecutionContext resolver for code-mounted http.server routes from @objectstack/runtime, promoting the internal resolveExecutionContext behind a plugin-context-shaped signature (getService/getServiceAsync + objectql + the IHttpRequest). The helper card must decide (i) which entry the helper takes for an anonymous request: core's own doc makes the fail-closed assembleExecutionContext the default (\"unless serving anonymous principals is part of its product semantics\"), but the runtime face uses the guest entry; and (ii) whether it carries the ADR-0069 auth gate, which today only the dispatcher (enforceAuthGate) and REST (envelope authGate) enforce, so a raw route bypasses it. #23128 then resumes, Blocked-by that card, as a ~5-line example change: pass the resolved context on both writes, let the refusal escape to the transport, plus its pins. Cost: a packages/runtime card (export, doc, minor changeset for @objectstack/runtime, a pin), S-M; the example, S.",
            "B. Compose in the example from today's published pieces: resolveAuthzContext + classifyAdmissionTenancyPosture + assembleExecutionContext (via @objectstack/runtime) + a getSession wired to the auth service (api, else await getApi()) + inProcessSessionReadInput. Measured as a scratch sketch, NOT committed: 27 lines of door plumbing in the example. It needs @objectstack/types added to examples/app-showcase/package.json (outside this claim's file surface), or a copy of the session-cookie rule (a re-implementation). It still has no ADR-0069 gate and no localization. It would be the sixth hand-rolled copy. Cost: S now, plus drift later.",
            "C. Re-home the recalc onto a declarative apis: endpoint (flow target) under /api/v1/apps/showcase/, which already runs as the caller through the dispatcher fallback's resolveRequestScope. This changes the two api actions' target (action metadata, out of this claim) and moves the estimate into a flow. It also removes the showcase's only code-mounted endpoint specimen (src/system/apis/index.ts names recalc-endpoint.ts as the code-mounted counterpart). It contradicts triage's landing point. Cost: M."
          ],
          "recommendation": "A. The four axes, in order. ACTUAL BUSINESS NEED (measured): the need to resolve a raw route's caller is real and current. This endpoint backs two showcase actions and two QA clauses (records-forms A2, i18n A5), and five platform doors already hand-roll the same composition, so the pull for one helper is measured, not speculative. C removes the specimen instead of serving the need. LONG-TERM SOUNDNESS: A leaves one copy of a security seam. B adds a sixth copy with three inputs that can be omitted silently: the tenancy posture (the settings/storage omissions #15351/#15352 were this class), the in-process session read (#22258), and the auth gate. Each copy was fixed separately. C only moves the problem. AI-PROOFING: the showcase is what AI copies. A teaches one call. B teaches hand-rolling a 27-line auth door in which every omission compiles and passes. C teaches \"avoid code endpoints\". STARTUP FOCUS: A exports an already-built function (no new capability, no new gate). Adding one export to @objectstack/runtime's published surface is the tension, accepted because six consumers (five doors and this endpoint) pull for it. B adds no platform surface but adds a dependency and a copy. No option adds a gate."
        }
      ],
      "out_of_scope_findings": [
        "carrier: the option-A helper card if ruled; otherwise the maintainer · noted, not filed (read-only inference, not reproduced as a defect). Code-mounted http.server routes receive no request identity and no ADR-0069 auth gate from the transport: IHttpRequest carries headers only, and adapter.ts runHandler builds no context. Five hand-rolled request-to-context compositions: rest-server.ts:3060, runtime security/resolve-execution-context.ts:150, service-settings settings-service-plugin.ts:285, service-storage storage-service-plugin.ts:862, plugin-hono-server current-user-endpoints.ts:443 · dedupe words: custom route execution context helper; http.server request identity; resolveExecutionContext export; raw route auth gate"
      ],
      "gates": "none run, no diff at dc93781c19. dispatch-gates no-path derivation: exit 2, \"nothing to derive\". Claim-path derivation: 42 commands, not run.",
      "line_budget": "n/a: no skills/** or ledger file touched",
      "deviations": [
        "Stopped at the dispatch's stop condition (assumption 2 falsified for a one-call published route). No PR, no label-write, no pr_create.",
        "Ran a turbo build of every package except docs (inside the lock) because check-dev-prereqs refuses pnpm dev:showcase while any workspace dist is missing. 62 of 74 tasks were cached."
      ],
      "files_changed": []
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    决策卡:代码挂载的自定义路由如何拿到调用者身份(#23128 的前置问题)

    domain:cli seat 2 (#22648) · os-steve · session session_01VoSxBQujKLZKPwK2u5ehQ6 · 2026-10-13T05:38Z。维护者在本席会话中裁定「开决策卡」,本卡即决策锚点。pm:dispatched → needs-user-decision,释放认领 6138685191。

    维护者速读

    showcase 的「重算工时」按钮现在点了报 500,什么也没写进去。triage 已定修法:端点以调用者本人身份写入,不用系统身份。但实测发现,平台没有给代码挂载的自定义路由任何公开的方法来拿到「这个请求是谁」,只能在示例里自己拼一套鉴权代码,或者平台先补一个公开函数。要裁的是:平台先补一个公开函数(A),还是示例里自己拼(B),还是把这个端点改成声明式(C)。推荐 A。

    背景

    • 缺陷(os-dev-report 6138940364):POST /api/v1/showcase/recalc 的两处写入 ctx.ql.update(...) 没带执行上下文,被安全层按「无主体写入」拒绝,单条和批量都答 500,数据不变(在 dc93781c19 上实测:管理员会话、匿名请求都是同一个 500)。
    • triage 6137971940 已裁:以调用者身份写入,⛔ 不用系统上下文。
    • dev 在派发单的停止条件处停下:没有已公开的「请求 → 执行上下文」入口。

    Governing text

    • ADR-0096(docs/adr/0096-execution-surface-identity-admission.md)标题原文:"no data-engine call without an explicit principal or an explicit, audited elevation"。系统上下文就是那个「显式提权」,triage 已排除。
    • 公开文档(content/docs/references/api/metadata.mdx、package-api.mdx、kernel/manifest.mdx 同句):"A route that needs real handler CODE is mounted imperatively: resolve the http.server service from the plugin context and register the handler on kernel:ready."文档教作者这样挂路由,却没告诉他怎样让这个路由以调用者身份写数据,也就是声明了一条路,却没兑现。
    • 协议声明:本卡不改任何协议或 spec 键。A 是给 @objectstack/runtime 加一个公开导出(公开面放宽,Clause-②: yes)。

    前提(每条带复核命令)

    1. 现成的解析函数是内部函数:git grep -n "resolveExecutionContext" origin/main -- packages/runtime/src/index.ts → 0 处;它定义在 packages/runtime/src/security/resolve-execution-context.ts:118。
    2. @objectstack/runtime 只发布 . 一个入口:git show origin/main:packages/runtime/package.json | jq '.exports | keys' → ["."]。
    3. 平台里已有 5 处各自手拼同一套组合:rest-server.ts:3060、runtime security/resolve-execution-context.ts:150、service-settings settings-service-plugin.ts:285、service-storage storage-service-plugin.ts:862(及 :1247)、plugin-hono-server current-user-endpoints.ts:443(dev 实测,dc93781c19)。
    4. 代码挂载的原始路由本身没有鉴权关卡:IHttpRequest 只带 headers,adapter.ts 的 runHandler 不建上下文(源码阅读,未实测)。匿名请求今天只靠 ADR-0096 的「无主体写入」拒绝挡住。

    具体问题

    一个用代码挂在 http.server 上的路由,怎样以「发请求的这个用户」的身份读写数据?

    选项

    选项 做什么 客户可感知的后果 工作量
    A 平台先出公开函数 把现成的 resolveExecutionContext 以插件上下文的签名公开导出一个「请求 → 执行上下文」函数,并定下匿名请求走哪个入口、是否带 ADR-0069 鉴权关卡;#23128 随后只剩约 5 行示例改动 自定义路由作者一行拿到调用者身份;showcase 按钮恢复,无权限者得到 403 而不是 500 domain:cli(runtime)· 2 个 PR(helper 卡 S–M + 示例 S)· 无完全同形先例
    B 示例里自己拼 在示例里用现有公开零件拼约 27 行鉴权代码,并给 showcase 加 @objectstack/types 依赖 按钮恢复;但示例里多了一段「照抄即可」的鉴权代码,漏掉租户姿态或鉴权关卡照样能编译通过 domain:cli · 1 个 PR · S(dev 的草稿实测 27 行,未提交)
    C 改成声明式端点 把重算改到 apis: 声明的端点(flow),它已经以调用者身份运行 按钮恢复;但 showcase 唯一的「代码挂载路由」示例没了,作者需要写代码路由时仍无路可走 domain:cli + action 元数据 · 1–2 个 PR · M;与 triage 定的落点相反

    业务含义直译:

    • A:平台把「谁在调用」这个能力做成标准件,像 Salesforce 的 Apex REST 自动以调用用户运行、Supabase 函数拿调用者的 JWT 建客户端那样,代码路由默认就知道是谁。
    • B:每个写自定义路由的客户各自抄一份鉴权代码,抄漏一行就是一个越权或拒绝错误。
    • C:绕开问题,示例不再演示代码路由,问题留给下一个真正要写代码路由的客户。

    四轴分析(业务立场)

    推荐

    A。 只看①选 A;②③④ 是否翻转:否(②有实测拉动,③④都不反对)。

    • 两年后的样子:平台的自定义代码路由默认拿到调用者身份和鉴权关卡,和声明式端点、REST、分发器同一条解析路径,参照 Salesforce Apex REST 与 Supabase Edge Functions。
    • 回退:若维护者不想现在扩公开面,选 B,但应把它登记为「已知第 6 份副本」,待 A 落地后回收。
    • 置信缺口:匿名请求该走「拒绝」还是「访客」入口、是否在 helper 里带 ADR-0069 鉴权关卡,dev 只读了源码,未在活主机上实测;这两点由 helper 卡开工时先实测。

    裁后执行

    os-decision-facets

    • ① 项目长远合理性:A 缩小特例(一份实现取代多份手拼),B 扩大(第 6 份副本),C 回避。
    • ② 实际业务拉动:今天有人撞上:showcase 两个按钮、两条 QA 用例,平台 5 处门同需此能力。
    • ③ 防 AI 犯错:A 一次调用、遗漏不可能;B 自由结构、遗漏静默;C 无代码路由可抄。
    • ④ 创业阶段不扩散:A 只导出已有函数,不新增能力或门禁;B 加依赖与副本;C 删示例。
      Prior rulings read: "custom http.server route execution context helper, resolve the caller's principal for a code-mounted endpoint, export resolveExecutionContext" → 1 hit ([finding, LATENT] makeExecutionContextResolver hand-rolls an ExecutionContext envelope and omits six fields of the closed entry set that assemble-execution-context.ts exists to make unrepresentable #15747, closed: a hand-rolled resolver that omitted six fields of the closed entry set, the drift class B adds to); ADR-0096 (identity admission), ADR-0069 (auth gate); thread: triage 6137971940

    相关


    Generated by Claude Code

  6. 2 remaining items

  7. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling · letter C′ · the maintainer's word, verbatim: 「同意」 (in the director seat's session, 2026-10-13T09:13Z, on the seat's reading 6142067136)

    What the ruling binds:

    1. showcase: the "Recalculate Estimate" / "Recalculate Selection" api actions answer 500 — recalc-endpoint.ts calls ctx.ql.update with no execution context, which the security layer refuses as a principal-less write #23128 re-lands declaratively. The showcase recalc becomes a flow (assignment computing the window's days × 8, update_record writing estimate_hours) behind an apis: endpoint of type: 'flow' at /api/v1/apps/showcase/recalc with authRequired: true; the two api actions (showcase_recalc_estimate, showcase_recalc_selection) retarget to it; the bulk path runs the same flow per _selectedIds entry (all-or-nothing as the aggregate contract says); examples/app-showcase/src/system/server/recalc-endpoint.ts and its kernel:ready mount are deleted, and src/system/apis/index.ts no longer calls it "the code-mounted counterpart". Triage's three pins stand: an admin's recalc answers 200 and updates the estimate; a caller without edit rights on the row is refused by the security layer (its envelope), not a 500; the bulk path is the same. ⛔ Not a system context (triage 6137971940 holds). If the flow's expression surface cannot write the computation (flow-node-expression-paths.ts), the seat measures whether a script node executes in 17.x; if neither can, it stops and reports, and the fallback is A.
    2. The imperative http.server route is retired as an app-author surface. The retire text of manifest.contributes.routes (packages/spec/src/kernel/manifest.zod.ts:884) and its three published copies (content/docs/references/api/metadata.mdx, package-api.mdx, kernel/manifest.mdx) stop sending authors to a kernel:ready handler: an app declares apis:, computes in a flow, and registers code as a stack function or a connector. The http.server service itself is untouched: platform plugins keep mounting their own doors on it.
    3. The platform's five hand-rolled request-to-context resolvers (rest rest-server.ts:3060, runtime security/resolve-execution-context.ts:150, service-settings :285, service-storage :862/:1247, plugin-hono-server current-user-endpoints.ts:443) are consolidated into one INTERNAL function in @objectstack/runtime: a p3 housekeeping card, not a published export, and showcase: the "Recalculate Estimate" / "Recalculate Selection" api actions answer 500 — recalc-endpoint.ts calls ctx.ql.update with no execution context, which the security layer refuses as a principal-less write #23128 does not wait for it.

    Execution: per this card's 裁后执行 for C, needs-user-decision → pm:retriage in this act, for the triage seat to re-set the landing point as item 1 says (the domain:cli seat re-dispatches from there). Items 2 and 3 are filed by the director seat as two cards to the lanes' queues in this act.

  8. added
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    and removed on Oct 13, 2026
  9. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: landing point re-set under ruling C′ (6142279866). domain:cli, priority:p3, into pm:queue

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-13T09:54Z. ⛔ Not a claim, ⛔ not a dispatch. Answers the ruling's request to re-set the landing point. pm:retriage is removed in this act.

    • The landing, as ruling item 1 binds it:
      • examples/app-showcase: the recalc becomes a flow (an assignment computing the window's days × 8, then update_record writing estimate_hours).
      • The flow sits behind an apis: endpoint (type: 'flow', /api/v1/apps/showcase/recalc, authRequired: true).
      • The two api actions retarget to it. The bulk path runs the same flow per _selectedIds entry, all-or-nothing.
      • src/system/server/recalc-endpoint.ts and its kernel:ready mount are deleted, and src/system/apis/index.ts's "code-mounted counterpart" wording goes.
    • Pins (unchanged from 6137971940):
      • an admin's recalc answers 200 and updates the estimate;
      • a caller without edit rights on the row is refused by the security layer (its envelope), not a 500;
      • the bulk path is the same.
      • ⛔ Not a system context.
    • Stop condition (the ruling's): if the flow's expression surface cannot write the computation, the claimant measures whether a script node executes. If neither can, it stops and reports, and the fallback is A.
    • Lane: examples/* goes to the subsystem it exercises. Here that is the declarative apis: endpoint and the dispatcher's flow execution, so domain:cli, and the domain:cli seat re-dispatches.
    • Ruling items 2 (retiring the imperative route as an author surface) and 3 (one internal request-to-context resolver) are the director's two cards. ⛔ This card does not wait for them.
  10. added and removed
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Oct 13, 2026
  11. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 18 (#23128 under the maintainer's ruling C′: the showcase recalc re-lands as a flow behind a declared apis: endpoint) · 2026-10-13T12:09Z
    Session: session_01VoSxBQujKLZKPwK2u5ehQ6
    Account: os-steve (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-23128-recalc-declarative-flow
    Worktree: objectstack-issue-23128
    Domain: domain:cli
    Seat: domain:cli#2 (seat post #22648)
    File surface (read on origin/main afbf960c26; stop on a breach and explain it in the report):

    • examples/app-showcase (private, @objectstack/example-showcase), as ruling item 1 (6142279866) and triage (6142979739) bind it:

      • the recalc becomes a flow: an assignment computing the window's days × 8, then update_record writing estimate_hours, under the caller's own runAs. ⛔ Not system (triage 6137971940).
      • It sits behind an apis: endpoint type: 'flow' at /api/v1/apps/showcase/recalc, authRequired: true, in src/system/apis/index.ts, with that file's "code-mounted counterpart" wording gone.
      • The two api actions showcase_recalc_estimate and showcase_recalc_selection (src/ui/actions/index.ts) retarget to it. The bulk path runs the same flow per _selectedIds entry, all-or-nothing.
      • src/system/server/recalc-endpoint.ts is deleted, with its import and kernel:ready mount in objectstack.config.ts.
      • Every other showcase reader of the deleted route (coverage.ts, the system tour doc, translations, the task page and view, test/gap-fill.test.ts) is re-pointed only.
    • The pins (triage's three):

      • an admin's recalc answers 200 and updates the estimate;
      • a caller without edit rights on the row is refused by the security layer, in its envelope, not with a 500;
      • the bulk path does the same.

      They go in the showcase's own tests, or in a dogfood showcase test if that is where the doors are booted.

    • Stop condition (the ruling's): if the flow's expression surface cannot write the computation (flow-node-expression-paths.ts), the dev measures whether a script node executes in 17.x. If neither can, the dev stops and reports, and the fallback is A.

    • ⛔ Not touched:

    • A changeset only if a published package moves. The showcase is private.

    Container & model: S, judged (one example app: a flow, an endpoint, two actions retargeted, one file deleted, three pins), mode:subagent, model: default tier (dispatch-gates --tier --repo objectstack-ai/objectstack: "no path-derived mandate"). No contract-review face: Clause-②: no (a private example app; no published accept set or export moves). check-governed-merges --test over the claimed paths: not governed.
    Clause-②: no
    Responsibility: the code-mounted route wrote with no execution context, because the platform publishes no request-to-context path for a raw http.server handler (round 1's report 6138940364). Ruling C′ retires that author surface instead of building one.
    Thread-read: 6142979739
    Serial constraints cleared:


    Generated by Claude Code

  12. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 23128,
      "status": "done",
      "branch": "claude/issue-23128-recalc-declarative-flow",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/23277",
      "session": "session_01VoSxBQujKLZKPwK2u5ehQ6 (mode:subagent; the dispatching PM session)",
      "premise_still_valid": true,
      "summary": "Ruling C-prime item 1 is implemented. The showcase recalc is now metadata. A declared endpoint showcase_recalc_estimates_api (POST /api/v1/apps/showcase/recalc, type flow, authRequired true) targets the entry flow showcase_recalc_estimates (screen, runAs user). That flow turns either body ({recordId} or {_selectedIds}) into a list of ids with an assignment, then maps the per-task flow showcase_recalc_task_estimate (autolaunched, runAs user) over the list: get_record reads the row window, an assignment computes days x 8 in CEL, update_record writes estimate_hours. Both api actions are retargeted. recalc-endpoint.ts, its import and kernel:ready mount, and its unit test are deleted, and every showcase reader is re-pointed. Measured on a real boot: an admin recalc answers 200 and writes 136/40/8 from each task's own window. A member without edit rights is refused, single and bulk, with nothing written, and the refusal message is the security layer's own PERMISSION_DENIED text. Anonymous is 401. One measured divergence from the pin wording, raised as an open question: the flow door answers that refusal as 400 FLOW_FAILED (the shared flow-dispatch table), not 403 PERMISSION_DENIED. Premise: dc93781c19 (round 1, which measured the 500) is an ancestor of the base afbf960c26 (merge-base exit 0, clone not shallow), and 0 commits touched the route or the actions in between.",
      "expression": {
        "node": "assignment compute_estimate in showcase_recalc_task_estimate (examples/app-showcase/src/automation/flows/recalc-estimate.flow.ts, WINDOW_ESTIMATE)",
        "cel": "has(task.start_date) && has(task.end_date) && task.start_date != null && task.end_date != null && daysBetween(task.start_date, task.end_date) >= 0 ? (daysBetween(task.start_date, task.end_date) + 1) * 8 : 8",
        "old_code_read": "The deleted estimateFromWindow(start, end): inclusive Math.round(diff/86400000)+1 days x 8, and 8 when a date was missing or invalid or end was before start. The single path read start_date/end_date from the request body, which the console never sends: its body is {recordId}, so the console path always got 8. The bulk path wrote a flat 8 per id. The flow reads the window from the stored row on both paths.",
        "surface": "assignment.assignments.* and update_record.fields.* are CEL value slots (packages/spec/src/automation/flow-node-expression-paths.ts, role 'value'). daysBetween rounds exactly as the old code did (packages/formula/src/stdlib.ts). No script node was needed, so the ruling's stop condition did not arise.",
        "measured_run": "bootShowcase with automation on. Windows 2026-06-16..2026-07-02 -> 136, 2026-03-02..2026-03-06 -> 40, end_date null -> 8. Admin single answered 200 {\"success\":true,\"data\":{\"success\":true,\"output\":{\"estimates\":[{\"estimate_hours\":136}]}}}. Admin bulk over two ids answered estimates [136, 40]. Store read-back under the system principal matched."
      },
      "runAs": {
        "spec_default": "user (packages/spec/src/automation/flow.zod.ts: runAs z.enum(['system','user']).default('user'))",
        "declared": "both flows declare runAs 'user' explicitly",
        "engine_reading": "automation getFlow on the booted showcase: showcase_recalc_estimates {type:'screen', runAs:'user'}; showcase_recalc_task_estimate {type:'autolaunched', runAs:'user'}",
        "identity_path": "The endpoint builds the automation context with buildAutomationContext (packages/runtime/src/domains/automation.ts), carrying userId, positions and permissions. The map child run spreads the parent context (packages/services/service-automation/src/builtin/map-node.ts, childContext = {...context, params}), so the caller reaches every per-task write."
      },
      "action_targeting": {
        "how": "objectui at pin 1fb1ced36243 (read only): apiHandler in packages/app-shell/src/hooks/useConsoleActionRuntime.tsx takes an absolute target and POSTs it through the authenticated fetch. The body is the action params plus recordIdParam. A non-2xx answer becomes an error toast. Both actions set target '/api/v1/apps/showcase/recalc'.",
        "bodies": "execution 'perRecord' (showcase_recalc_estimate) sends {recordId}. execution 'aggregate' (showcase_recalc_selection via task.view bulk_actions bulkActionDefs) sends ONE request with {_selectedIds:[...]}. The endpoint hands the flat body to the flow as params, and the entry flow's two isInput variables recordId and _selectedIds each default to null.",
        "bulk_shape": "One endpoint and one entry flow. A map node runs the same per-task flow once per id, sequential and fail-fast. The single body is a list of one. A flow endpoint cannot loop by itself; the action does not split the call either, since aggregate is ONE dispatch.",
        "contract": "packages/spec/src/ui/view.zod.ts, bulkActionDefs .describe(): 'Aggregate results are all-or-nothing: a handler that cannot cover the whole selection must reject, and per-row retry is replaced by re-running the action.' The first task that cannot be recalculated fails the whole request (400). The run is not a transaction (tasks before it keep their value), and re-running is safe because the recalc is idempotent. Stated in the flow's doc comment."
      },
      "pins": {
        "file": "packages/qa/dogfood/test/showcase-declarative-endpoints.dogfood.test.ts, section 3b (reuses the file's showcase boot)",
        "refused_caller": "A signed-up member holding only the everyone baseline showcase_member_default (showcase_task read and create, no edit). Precondition pinned: member GET /data/showcase_task/ID answers 200 and member PATCH answers 403 PERMISSION_DENIED.",
        "pin1": "Admin POST {recordId} -> 200, success true, row estimate 136; an unnamed row is untouched.",
        "pin2": "Member POST {recordId} -> 400, error.code FLOW_FAILED, and error.message contains the live data-door refusal text for the same edit; the row keeps its sentinel. Measured message: Node 'recalc_each' failed: map 'recalc_each': item 0 (subflow 'showcase_recalc_task_estimate') failed: Node 'write_estimate' failed: update_record(showcase_task) failed: You do not have permission to perform this action. Contact your administrator if you need access.",
        "pin3": "Admin POST {_selectedIds:[a,b,c]} -> 200, estimates 136/40/8, each from its own window. Member same body -> the same refusal, all three rows unchanged.",
        "extra": "Anonymous -> 401 with nothing written. Body {} -> refused (400): the per-task get_record refuses a filter operand that evaluates to nothing."
      },
      "readers": [
        "objectstack.config.ts: import (was :20) removed; onEnable mount (was :296-297) removed; apis comment (was :260) re-pointed",
        "src/system/apis/index.ts: 'metadata-authored counterpart of the code-mounted endpoint' wording replaced; RecalcEstimatesEndpoint added",
        "src/ui/actions/index.ts: both targets -> /api/v1/apps/showcase/recalc; comments naming the route's 'per-record branch'/'batch branch' re-pointed to the flow",
        "src/ui/views/task.view.ts: comment URL and batch-branch wording re-pointed",
        "src/coverage.ts: the 'recalc-endpoint.ts remains the code-mounted HTTP counterpart' sentence replaced (its own hunk, line 133; PR #23222's coverage.ts hunk is line 168)",
        "src/docs/showcase_tour_system.md: 'Code-only surfaces' (it described only the deleted file) replaced by 'HTTP endpoints'; frontmatter description updated",
        "test/gap-fill.test.ts: the existsSync pin on the deleted file replaced by a static pin: both actions post to the declared POST flow endpoint, and both recalc flows declare runAs user",
        "test/recalc-endpoint.test.ts: deleted (it unit-tested the deleted file; not in the PM list)",
        "src/system/translations/index.ts: read, unchanged ('the two recalc surfaces of the same endpoint' is still accurate)",
        "src/ui/pages/task-detail.page.ts: read, unchanged (action ids only)"
      ],
      "tests": "All at HEAD 2ce73b3234. Showcase: vitest 32 files / 408 tests passed; typecheck exit 0; objectstack validate passed (70 warnings, 0 naming the new flows or endpoint). Dogfood endpoints file: 24/24 passed (17 existing + 7 new); dogfood typecheck exit 0 with both new files in its tsc program. Ablation 1: runAs user -> system on showcase_recalc_task_estimate, through scripts/ablation-replace.mjs (anchor x1 -> x0, blob aa606566b4b5 -> 236382c799ab). Predicted the two member cases red. Observed exactly those 2 failed, 22 passed: the member single answered 200 acted 1 and the member bulk 200 acted 3. Ablation 2: the inclusive + 1 dropped from the assignment (blob -> e5e4722bf126). Predicted the two admin cases red. Observed exactly those 2 failed (expected 128 to be 136), 22 passed. Both restores were proven by blob == HEAD aa606566b4b5 and an empty git diff HEAD, under a trap with absolute paths. The showcase is consumed as TS source (exports ./objectstack.config.ts), so no build sat between mutation and run. Lint by proven narrowing: (1) the population was read from eslint's own resolved config (9 touched lintable files, each isPathIgnored=false with 4-5 active rules); (2) eslint --no-inline-config --format json counted 9 files, 0 errors, 0 warnings; (3) type-aware linting is enabled for no file (no parserOptions.project or projectService in eslint.config.mjs), so untouched files cannot move. The one Markdown file is outside eslint's population.",
      "gates": {
        "head": "2ce73b3234",
        "derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) on 2ce73b3234: 58 commands, identical to the derivation at 5e09eefa63. The PM's 37 were over the claim's paths on afbf960c26.",
        "reconciliation": "✓ dispatch-gates --ran: 58 derived famil(ies) accounted for — 58 run, 0 NOT-MEASURED (a DERIVED zero — all 58 recorded an exit code and none of them is 3)",
        "derived": [
          "node scripts/check-ci-filter-parity.mjs :: exit 0 :: OK: all 21 build input(s) turbo.json declares outside the packages (itself, 2 globalDependencies, and every `$TURBO_ROOT$` input of a build",
          "node scripts/check-closing-keyword-parity.mjs :: exit 0 :: • packages/spec/CHANGELOG.md -- 8701420 bytes exceeds the sweep's 2097152-byte cutoff for UNREGISTERED files",
          "node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 :: ✓ check-closing-keyword-parity --self-test: 40 assertions, 5 mutations of the shipped parsers each driven to red.",
          "node scripts/check-comment-mask-adoption.mjs :: exit 0 :: OK  check:comment-mask-adoption — 14 private comment-stripper(s) under packages/** + examples/**, all 14 recorded and every recorded row s",
          "node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0 :: PASS  check-comment-mask-adoption --self-test (0 failure(s))",
          "node scripts/check-comment-mask-corpus.mjs :: exit 0 :: ✓ comment-mask corpus sweep [scripts/js-comment-mask.mjs]: 8979 files, 0 disagree, 0 unparseable, 157.7s (comparator self-test: 26 cases p",
          "node scripts/check-dts-emitted.mjs --self-test :: exit 0 :: check-dts-emitted self-test: all assertions passed.",
          "node scripts/check-issue-citations.mjs :: exit 0 :: ✅ check-issue-citations: no issue citations added against 6d7f4df3c (0 file(s) read).",
          "node scripts/check-keyed-text-bounds.mjs :: exit 0 :: ⚠ The delta is information, not a verdict — this population grows AND shrinks for good reasons, and only the floors decide. Reproduce",
          "node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0 :: PASS  check-keyed-text-bounds --self-test (0 failure(s))",
          "node scripts/check-platform-object-tenancy-census.mjs :: exit 0 :: ✓ platform-object tenancy census matches the tree: 87 platform-namespace objects, 52 in the machinery's reach, 35 outside it, every exclus",
          "node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0 :: ✓ check-platform-object-tenancy-census self-test: all checks pass (87 objects, 35 outside the machinery)",
          "node scripts/check-plugin-teardown-shape.mjs :: exit 0 :: ✓ check:plugin-teardown-shape: 77 Plugin implementation(s) across 8394 source(s) under packages/**; every teardown-shaped method (stop / s",
          "node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0 :: ✓ check-plugin-teardown-shape self-test: 48 cases pass (real pre-#10375 fixture reds, the repaired file and both delegating-alias directio",
          "node scripts/check-registry-log-declared.mjs :: exit 0 :: examples/app-showcase — S1 constructs a SchemaRegistry in its tests",
          "node scripts/check-registry-log-declared.mjs --self-test :: exit 0 :: self-test OK: 17 cases + level-vocabulary read + population declaration + real-tree selection floor.",
          "node scripts/check-rest-log-spy-declared.mjs :: exit 0 :: OK: 32 of 284 test file(s) beside packages/rest/src/log.ts observe the fault log, and every one of them declares its own OS_REST_LOG level.",
          "node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0 :: ✓ the real tree yields a NON-EMPTY observer population that is a strict subset of its test files",
          "node scripts/check-system-context-census.mjs :: exit 0 :: check-system-context-census: OK — 134 elevation read sites in 21 packages across 61 files, living in 114 symbol(s); the page cites 127 sym",
          "node scripts/check-system-context-census.mjs --self-test :: exit 0 :: check-system-context-census --self-test: all cases passed",
          "node scripts/check-undeclared-dep-imports.mjs :: exit 0 :: ⚠ The delta is information, not a verdict — the floors are `>=` and cannot see an upward drift at all, which is why it is PRINTED. Rep",
          "node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0 :: PASS  check-undeclared-dep-imports --self-test (0 failure(s))",
          "node scripts/docs-audit/check-affected-docs.mjs :: exit 0 :: → the unreachable rows themselves: this command with --json",
          "node scripts/docs-audit/check-drift-comment.mjs :: exit 0 :: ✓ check-drift-comment: 66 cases pass across 5 fixture diff(s).",
          "pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 :: ✓ check:duration-unit-keys — 197 unit-declaring numeric key(s) across 3083 source file(s) all carry their unit in the key name (or in a",
          "pnpm --filter @objectstack/spec run check:empty-state :: exit 0 :: ✓ all classified (2 closed, 2 open, 4 output, 9 scope)",
          "pnpm --filter @objectstack/spec run check:liveness :: exit 0 :: (not a completeness claim about the 529 child key(s) under the declared blanket verdicts above — those are recorded, not classified.)",
          "pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0 :: 5 triaged director(ies), 478 object site(s): strict 353 · passthrough 6 · catchall 1 · strip 118",
          "pnpm --filter @objectstack/spec run check:variant-docs :: exit 0 :: ✓ variant/doc gate: 18 discriminated union(s) — 8 governed (every variant mentioned in a bound doc), 10 exempt.",
          "pnpm check:cross-package-test-inputs :: exit 0 :: OK: 30 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob (6 of them on a split \"test:repo\" task);",
          "pnpm check:doc-authoring :: exit 0 :: ✓ doc authoring guard: sibling-package prose ids hold the baseline — 0 pinned site(s) across 0 file(s), 95199 string(s) read in 1326 par",
          "pnpm check:driver-memory-census :: exit 0 :: check-driver-memory-census: OK — every declaration is ledgered, every ledger entry is live, and every ruled file states \"#6664 census: 2 r",
          "pnpm check:dts-closure :: exit 0 :: check-dts-closure self-test: all assertions passed.",
          "pnpm check:dual-build-cjs-loads :: exit 0 :: ⚠ The delta is information, not a verdict — this population grows AND shrinks for good reasons, and only the floors decide. Reproduce",
          "pnpm check:engine-double-contract :: exit 0 :: ⚠ The delta is information, not a verdict — this population grows AND shrinks for good reasons, and only the floors decide.",
          "pnpm check:examples-live-imports :: exit 0 :: examples/** live-import inventory OK -- 0 invisible (declared), 20 inputs-declared, 45 graph-visible.",
          "pnpm check:gitlink-declared :: exit 0 :: check-gitlink-declared: OK (11259 index entries -- 0 gitlink(s) at mode 160000; no .gitmodules in the index, so nothing is declared; nothing",
          "pnpm check:i18n-coverage :: exit 0 :: check-i18n-coverage: OK (14 config(s), 621 baselined untranslated string(s), none new).",
          "pnpm check:issue-citations :: exit 0 :: ✅ check-issue-citations --self-test: grammar narrowed, every spelling enumerated, qualifier a closed set of repositories, four 404 causes",
          "pnpm check:lean-entry-closure :: exit 0 :: ✓ check-lean-entry-closure: 2 published condition(s) measured from a real load.",
          "pnpm check:logger-receiver-detach :: exit 0 :: OK  every log channel keeps its receiver: 3380 non-test TS file(s) walked, 0 detach(es) on the 5 declared receiver-sensitive sink spelling(s",
          "pnpm check:nul-bytes :: exit 0 :: check-nul-bytes: OK (scanned 11250 text file(s) -- 11250 tracked, 0 untracked-not-ignored; skipped 9 binary; no raw ASCII control bytes).",
          "pnpm check:objectql-double-limit :: exit 0 :: OK  ObjectQL double `limit` conformance holds: 482 double(s) graded, 291 apply the caller's bound or refuse it loudly.",
          "pnpm check:org-identifier :: exit 0 :: check-org-identifier: OK (3402 author-facing source file(s), 18 session binding(s) resolved, no removed session.tenantId alias).",
          "pnpm check:page-declaration-shape :: exit 0 :: blind spot: 1 computed carrier(s) no source scan can enumerate — examples/app-crm/objectstack.config.ts:85.",
          "pnpm check:published-files :: exit 0 :: ✓ check:published-files — 71 publishable package(s) of 82 workspace member(s) declare a `files` whitelist that covers every entry point",
          "pnpm check:query-options-erasure :: exit 0 :: test surface: 236 site(s) in 47 file(s) — at the ceiling, outside the blocking rule by the #4918 triage (a rejection test must be able t",
          "pnpm check:react-page-adapter-contract :: exit 0 :: ✓ check-react-page-adapter-contract: 21 app-showcase page module(s) + 1 content/docs react-page sample(s) (from 396 doc file(s), 2017 fenc",
          "pnpm check:refd-timer-probe :: exit 0 :: OK  check-refd-timer-probe: 8973 source file(s) swept; the process-global timer probe is read in packages/qa/refd-timer-testkit/src/index.ts",
          "pnpm check:slot-lookup :: exit 0 :: ✓ slot-lookup ratchet holds: 104 unswept site(s) in 25 file(s), none new, and every file in the population parsed. Every other file under",
          "pnpm check:sourcemap-no-sources-content :: exit 0 :: check-sourcemap-no-sources-content self-test: all assertions passed.",
          "pnpm check:stack-collection-maps :: exit 0 :: ✓ check-stack-collection-maps: 8 enumerations reconciled against 33 declared collections (17 waiver rows, each with a reason).",
          "pnpm check:test-source-alias :: exit 0 :: check-test-source-alias OK — 74 packages with tests scanned; 60 registered as still resolving a workspace dep through `dist/`; 53 publishe",
          "pnpm check:tier-file-adoption :: exit 0 :: @objectstack/cli — 82 file(s); imports readTierMode, selectTierFiles from scripts/nightly-tiers.mjs (via packages/cli/vitest-tiers.ts)",
          "pnpm check:type-check-coverage :: exit 0 :: ⚠ The delta is information, not a verdict — this population grows AND shrinks for good reasons, and only the floors decide.",
          "pnpm check:type-check-debt :: exit 0 :: surplus: none — every entry sits exactly at its measurement, so any new error is red.",
          "pnpm check:watch-hint-literal :: exit 0 :: ✓ check-watch-hint-literal: 73 declaration(s) across 4 rostered name(s) -- ROOT_DIR_WATCH_HINTS 49, ROOT_FILE_WATCH_HINTS 13, ROOT_WATCH_H",
          "pnpm check:where-matcher :: exit 0 :: ✓ where-matcher conformance holds: 503 matcher(s) discovered, 503 answer the combinator battery correctly or refuse it loudly (323 refuse)"
        ],
        "added": [
          "pnpm --filter @objectstack/example-showcase exec vitest run --maxWorkers=2 :: exit 0 :: Test Files 32 passed (32), Tests 408 passed (408), os-verify-lock: VERDICT command-exit 0",
          "pnpm --filter @objectstack/example-showcase run typecheck :: exit 0 :: os-verify-lock: VERDICT command-exit 0",
          "pnpm --filter @objectstack/example-showcase run validate :: exit 0 :: ✓ Validation passed (2011ms), os-verify-lock: VERDICT command-exit 0 (70 warnings, 0 naming the new flows or endpoint)",
          "pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2 test/showcase-declarative-endpoints.dogfood.test.ts :: exit 0 :: Tests 24 passed (24), os-verify-lock: VERDICT command-exit 0",
          "pnpm --filter @objectstack/dogfood run typecheck :: exit 0 :: os-verify-lock: VERDICT command-exit 0 (tsc --listFiles names the test file and recalc-estimate.flow.ts, 1 hit each)"
        ],
        "not_measured_earlier": [
          "check:dual-build-cjs-loads exited 3 (PREREQUISITE NOT MET, 8 dists missing) on the first union at 2a8166baa9. After a turbo build of those 8 packages it exits 0 in the final union above.",
          "Two lock acquisitions answered queue-timeout (exit 99) and were retried under the same OS_VERIFY_LOCK_SLOT; neither is a measurement."
        ]
      },
      "line_budget": "n/a: no skills/** or ledger file touched",
      "files_changed": [
        "examples/app-showcase/objectstack.config.ts",
        "examples/app-showcase/src/automation/flows/index.ts",
        "examples/app-showcase/src/automation/flows/recalc-estimate.flow.ts (new)",
        "examples/app-showcase/src/coverage.ts",
        "examples/app-showcase/src/docs/showcase_tour_system.md",
        "examples/app-showcase/src/system/apis/index.ts",
        "examples/app-showcase/src/system/server/recalc-endpoint.ts (deleted)",
        "examples/app-showcase/src/ui/actions/index.ts",
        "examples/app-showcase/src/ui/views/task.view.ts",
        "examples/app-showcase/test/gap-fill.test.ts",
        "examples/app-showcase/test/recalc-endpoint.test.ts (deleted)",
        "packages/qa/dogfood/test/showcase-declarative-endpoints.dogfood.test.ts"
      ],
      "changeset": "None. Both moved packages are private: true (@objectstack/example-showcase, @objectstack/dogfood). The deciding gate is Check Changeset (.github/workflows/pr-automation.yml), route 2 ('It releases nothing ... examples/, tests-only'), which wants the skip-changeset label. NOT applied: this dispatch's write budget names no label-write. The seat applies skip-changeset; until then Check Changeset is red by design.",
      "deviations": [
        "Conflict, stated rather than silently decided. The role file says shipped text this change makes false must be fixed in the PR. Ruling C-prime item 2 and this claim ('No packages/spec, no content/docs') keep the four sites that still name the deleted route with the director. I followed the ruling and the claim; the sites are listed in out_of_scope_findings.",
        "Zero label writes (budget), so Check Changeset stays red until the seat applies skip-changeset.",
        "The pins were added to the existing showcase-declarative-endpoints.dogfood.test.ts to reuse its boot, not a new file. Two cases beyond triage's three were added: anonymous 401, and an empty body refused.",
        "A first gate union ran at the merge commit 2a8166baa9 while 10a10ebfa9 landed. It is superseded: every figure reported is from the final union at the HEAD named in gates.head.",
        "Built 8 more packages (studio, client-react, embedder-openai, knowledge-memory, knowledge-ragflow, organizations, service-cluster-redis, service-knowledge) so check:dual-build-cjs-loads could measure; this is outside the dogfood closure build.",
        "Read objectui at the pin through a read-only shallow fetch into /home/user/objectstack-ai/objectui (the add_repo answer 'read_available'). It is removed in cleanup."
      ],
      "mcp_calls": "0 GitHub MCP calls. 1 non-GitHub MCP call: mcp__claude-code-remote__add_repo (objectstack-ai/objectui, access read). It answered read_available and attached nothing. No write tool.",
      "api_writes": "3 REST writes, all via the fleet-write relay as objectstack-fleet[bot]: POST /repos/objectstack-ai/objectstack/pulls (pr_create, draft forced) and POST /repos/objectstack-ai/objectstack/issues/23277/assignees (the same action's assignee leg), relay run 38407350137; POST /repos/objectstack-ai/objectstack/issues/23128/comments (this os-dev-report, via scripts/pm/post-stamped.mjs). Not REST: 6 git pushes of the branch. No label-write. No PATCH.",
      "open_questions": [
        {
          "question": "Pin 2 says 'refused by the security layer (its envelope), not a 500'. The security layer does refuse: the run's error is its PermissionDeniedError text, the same words the data door answers under 403 PERMISSION_DENIED for the same edit, and nothing is written. But a flow endpoint answers every run that ran and failed as the shared flow-dispatch table's 400 FLOW_FAILED (packages/runtime/src/flow-dispatch-status.ts), because update_record's catch (packages/services/service-automation/src/builtin/crud-nodes.ts) returns the message with no code. Is 400 FLOW_FAILED, carrying the security layer's refusal, an acceptable reading of 'its envelope'?",
          "options": [
            "A. Accept as delivered. It is the flow door's declared answer under the #9378 ruling, the same at the trigger route. The pins hold 400 + FLOW_FAILED + the security refusal text compared with the live data door, and nothing is written. Cost: none.",
            "B. A platform card: forward a security refusal's code from the CRUD executors (the node code channel already carries PERMISSION_DENIED for stored-metadata families), and add a flow-dispatch table row mapping it to 403. That is a spec-seat widening of a table three doors share (the table's own note: 'a fifth row would be a spec-seat widening'). M, packages/services + packages/runtime + spec ledger; this card would not wait for it."
          ],
          "recommendation": "A for this card. The refusal is the security layer's and is loud. The 403-vs-400 difference is a property of every flow door, not of this example, so it belongs to the table's owner if anyone pulls for it. Measured pull today: this example only."
        }
      ],
      "out_of_scope_findings": [
        "carrier: ruling C-prime item 2's card (director seat) · noted, not filed. Four sites still name the deleted route after this PR: (1) packages/spec/src/migrations/entries/semantic/18.plugin-manifest-contributes-routes-retired.ts:13, published author-facing replacement text: '`examples/app-showcase` mounts POST /api/v1/showcase/recalc that way'; (2) packages/spec/src/api/plugin-rest-api.zod.ts:36-37 TSDoc ('examples/app-showcase recalc-endpoint is a real consumer of this exact shape') plus its generated copy content/docs/references/api/plugin-rest-api.mdx:37; (3) packages/spec/liveness/manifest.json:194 note ('examples/app-showcase recalc-endpoint mounts on kernel:ready'); (4) scripts/check-optional-error-sink-contract.mjs:164 scope comment counting the example's one red sink (now zero; comment only, the gate scans packages/**). Dedupe words: contributes.routes retire text showcase recalc; imperative http.server mount example; plugin-rest-api worked example recalc-endpoint.",
        "carrier: 承接者:无 · noted in the PR's Acceptance notes and open_questions only. A security refusal inside a flow reaches a flow door's caller as 400 FLOW_FAILED, not 403 PERMISSION_DENIED (crud-nodes.ts update_record catch drops the code; the flow-dispatch table maps every failed run to FLOW_FAILED). This is the declared contract, so it is not a filing class. Dedupe words: flow endpoint permission denied 400 FLOW_FAILED; update_record security refusal code; flow-dispatch table 403 row.",
        "carrier: PM (container hygiene) · noted, not filed. 11 *.pid files sit at the filesystem root, dated 2026-10-11 to 2026-10-13, all older than this run. Not written by this run; not touched."
      ]
    }

    Generated by Claude Code

  13. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    ✅ ACCEPT: PR #23277 at 2ce73b3234 (Fixes #23128, ruling C′ item 1). It queues on a green head

    domain:cli seat 2 (#22648) · os-steve · session session_01VoSxBQujKLZKPwK2u5ehQ6 · 2026-10-13T13:52Z · holder of claim 6145219150, under ruling C′ 6142279866. Report: os-dev-report 6147020959. Thread-read: 6147020959.

    Checklist, read against GitHub:

    • PR shape: draft, base main, line 1 Fixes #23128 (the body's only closing keyword), line 2 Clause-②: no, assignee os-steve, title fix(showcase):.

    • Scope: 12 files, +445 / −236, all inside the claim. Two are deleted: recalc-endpoint.ts and its unit test. No packages/*/src, no packages/spec, no content/docs. check-governed-merges --pr 23277, run from current main's script, reports it not governed. git merge-tree against today's origin/main (6d7f4df3c9) is clean, and the branch's merge base is that commit.

    • Ruling C′ item 1, read in the diff:

      • The declared endpoint showcase_recalc_estimates_api is POST /api/v1/apps/showcase/recalc, type: 'flow', authRequired: true, in src/system/apis/index.ts. The "code-mounted counterpart" wording is gone.
      • The per-task flow showcase_recalc_task_estimate runs get_record, then an assignment computing the inclusive days × 8 in CEL (8 for a missing or inverted window, as the deleted code did), then update_record writing estimate_hours.
      • The entry flow showcase_recalc_estimates turns { recordId } or { _selectedIds } into one list, and its map node runs the per-task flow per id, sequentially and fail-fast. It is type: 'screen', on the showcase_inquiry_purge precedent.
      • Both flows declare runAs: 'user', the spec default, and neither is a system context. The map child run carries the caller's context.
      • Both api actions retarget to the endpoint. The kernel:ready mount, its import and the deleted file's readers are re-pointed or removed.
      • The window is now read from the stored row. Before, the console path always got 8, because its body never carried the dates.
    • Triage's three pins (showcase-declarative-endpoints.dogfood.test.ts §3b, on a real showcase boot, 24/24):

      • An admin's single recalc answers 200 and writes 136 from the task's own window. An unnamed row is untouched.
      • A signed-up member with read but no edit rights is refused, with the row unchanged. The precondition is pinned: the same member's PATCH on the data door answers 403 PERMISSION_DENIED.
      • The bulk path behaves the same: the admin gets 136 / 40 / 8 from three windows, and the member is refused with all three rows unchanged.
      • An anonymous caller gets 401, and an empty body is refused.
    • The open question, ruled by the seat: A. The flow door answers the member's refusal as 400 FLOW_FAILED carrying the security layer's own PERMISSION_DENIED sentence, not as 403 PERMISSION_DENIED. That is the shared flow-dispatch table's declared answer for a run that failed (flow-dispatch-status.ts, the automation: both trigger routes still answer HTTP 200 wrapping an inner {success:false} — the same #3962 residue #8684 closed on resume #9378 ruling), the same at the trigger route. The pin's "refused by the security layer (its envelope), not a 500" holds on its substance:

      • the security layer makes the decision, and its words are what the caller reads;
      • nothing is written;
      • the answer is a located 4xx in the platform's envelope.

      A 403 row for a security refusal inside a flow would widen a table three doors share. That table belongs to the spec seat, and measured demand is this example alone, so it is not filed. If the maintainer reads "its envelope" as 403, that is option B in the report, a platform card, and this card does not wait for it.

    • The bulk contract, read: the bulkActionDefs contract is "a handler that cannot cover the whole selection must reject, and per-row retry is replaced by re-running the action." The first refused task fails the whole request. The run is not a transaction, so tasks before it keep their new value. Re-running is safe because the recalc is idempotent. The flow's doc comment states this, and it is accepted as the contract's own remedy.

    • Ablations:

      • runAs switched to system: exactly the two member cases red, single and bulk, because the member's writes would land.
      • The inclusive + 1 dropped: exactly the two admin cases red (128, not 136).

      Both restores were proven against the HEAD blob.

    • Tests and gates at 2ce73b3234:

      • showcase: 32 files / 408 tests, typecheck exit 0, objectstack validate passed (none of its 70 warnings names the new flows or the endpoint);
      • dogfood: 24/24, typecheck exit 0;
      • 58 gates derived with no paths, 58 run, all exit 0, reconciled with --ran;
      • lint narrowed, with the three-part proof.
    • Changeset: none, because both moved packages are private. The deciding gate is Check Changeset's release-nothing route, which wants skip-changeset. The seat applies that label in this act, and the check re-runs on the labeled event.

    • CI on 2ce73b3234 at this stamp: latest run per name: 31 success, 6 skipped, 2 in progress, and one failure, Check Changeset, which waits on the label above.

    • The four sites that still name the deleted route (the report's out-of-scope note, checked on main). Ruling item 2 is docs(spec): the routes retire text and three published pages stop sending app authors to an imperative http.server route — apps declare apis:, compute in flows, register stack functions (ruling C′ on #23128) #23216's, domain:spec, and these sites are not on that card's list, so the seat posts them there as a pointer in this act:

      • packages/spec/src/migrations/entries/semantic/18.plugin-manifest-contributes-routes-retired.ts:14;
      • plugin-rest-api.zod.ts:37 and its generated plugin-rest-api.mdx:37;
      • packages/spec/liveness/manifest.json:194;
      • scripts/check-optional-error-sink-contract.mjs:164, a comment only.
    • Writes: mcp_calls 0 GitHub (one read-only add_repo for objectui, which attached nothing); api_writes 2 relay strokes (pr_create with its assignee, and the report).


    Generated by Claude Code

  14. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #23277 → 042ef51530 (Fixes #23128, ruling C′ item 1). The card closes completed

    domain:cli seat 2 (#22648) · os-steve · session session_01VoSxBQujKLZKPwK2u5ehQ6 · 2026-10-13T14:14Z · holder of claim 6145219150, released by this landing.

    • Landed: through the merge queue at 2026-10-13T14:14:10Z as 042ef51530, a squash with one parent.

      • The recalc is now metadata. The showcase's "Recalculate Estimate" and "Recalculate Selection" call a declared type: 'flow' endpoint, POST /api/v1/apps/showcase/recalc with authRequired: true. Its flows run as the caller (runAs: 'user'): they read each task's own window, compute the inclusive days × 8 in CEL, and write estimate_hours.
      • The code-mounted route is gone. recalc-endpoint.ts, its kernel:ready mount and its test are deleted, so the 500 the round-1 report measured has no door left.
    • Review chain:

      • the round-1 report 6138940364 and the decision box 6139047728;
      • ruling C′ 6142279866;
      • ACCEPT 6147144361.

      Every check on the head was success or a roster skip, including Check Changeset under skip-changeset, because both moved packages are private. There is no changeset and no contract review (Clause-②: no).

    • What changes for a user of the showcase:

      • An admin's recalc answers 200 and writes the estimate from the task's schedule. Before, it answered 500. Even past the 500, the deleted code read the window from the request body, which the console never sends, so it could only have written 8.
      • A user without edit rights on the task is refused with the security layer's own sentence, and nothing is written.
      • A selection is recalculated in one call, and the first task that cannot be written rejects the whole request. Re-running it is safe.
    • Seat ruling recorded at ACCEPT: the refusal answers 400 FLOW_FAILED, the flow-dispatch table's declared answer for a failed run, not 403. The seat read the pin as met on its substance. A 403 row would be a spec-seat widening of a table three doors share, and it is not filed.

    • Ruling C′'s other items are not this card's:

    • Holds released: examples/app-showcase/** (the claim's files) and packages/qa/dogfood/test/showcase-declarative-endpoints.dogfood.test.ts.


    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:clipriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions